用Browserify构建antd-mobile应用

antd-mobile是蚂蚁金服出的移动端设计指南和前端框架,它提供了一套基于React的移动端组件库,可以很方便地用来开发体验良好的移动应用。

使用antd-mobile遇到的问题:react-native模块找不到

在阅读了antd-mobile的介绍说明后,使用这一组件库似乎很简单,要做的只是安装和引入组件而已:

安装

$ npm install antd-mobile --save

引入组件

import { Button } from 'antd-mobile/lib/button';  
ReactDOM.render(<Button>按钮</Button>, mountNode);  

antd-mobile的介绍说明中推荐使用babel-plugin-import插件来按需加载类库,但为了减少初期使用antd-mobile所面临的复杂度,以上代码采用了最简单的组件引入写法(显式指定组件的路径antd-mobile/lib/button,并在HTML文件中单独引入CSS样式文件antd-mobile/dist/antd-mobile.min.css)。

在安装完antd-mobile模块并引入需要的组件后,接下来的一步便是构建整个移动应用。此时,如果项目不是React Native应用,而是Web应用的话,构建过程会报错,显示react-native模块找不到(Error: Cannot resolve module 'react-native'...)。这个错误无疑是非常令人困惑的:当前所开发的是一个普通的移动端Web项目,与react-native没有任何关系,为什么需要react-native模块?事实上,即使根据报错提示安装react-native模块,在后续的构建过程中也会报一些别的错误,导致构建失败。

进一步的调查发现,问题出在antd-mobile的组件模块设计上。由于antd-mobile被设计为同时支持React Native应用开发和Web应用开发,因此所有的组件都暴露为2个模块文件:index.jsindex.web.js。其中,index.js是给React Native开发使用的,而index.web.js则是给Web开发使用的。由于Browserify和Webpack等打包工具在解析JavaScript模块引入操作时(requireimport语句),会优先查找.js后缀名的文件(当不指定模块文件名时,默认文件名即为index.js),因此即使当前项目与React Native无关,组件模块的引入操作也会导致对react-native的依赖。

找到问题的原因后,解决方案初步考虑有2种:

  1. 引入模块时,显式指定模块文件的文件名(import { Button } from 'antd-mobile/lib/button/index.web'; )。
  2. 对Browserify或Webpack等打包工具进行配置,更改其模块引入操作时的后缀名优先级,使得.web.js文件得以优先使用。

第一种方案比较简单,对代码的改动量也很小。但事实证明,这一方案是行不通的:antd-mobile的组件代码中存在内部组件依赖(如List组件依赖ListItem组件,在List组件的index.web.js文件中,会出现require('./ListItem')这样的代码),而这些引入内部组件的操作并未指定具体的模块文件名,因此还是会产生require('./ListItem/index.js')这样的效果,并最终导致对react-native的依赖。

对于第二种方案,如果是用Webpack打包,则antd-mobile社区有现成的解决方法 — 设定extensions选项的值,并将.web.js放在.js之前即可。但在Browserify中,这一问题该如何解决呢?

使用Browserify遇到的问题:如何自定义模块文件后缀名的优先级?

和Webpack一样,Browserify也提供了一个叫extensions的配置选项,用于设定模块文件的后缀名及其优先级。但和Webpack不同的是,Browserify中默认的2个模块文件后缀名(.js.json)永远具有最高优先级,即使在extensions配置选项中设定.web.js.js具有更高的优先级(extensions: ['.web.js', '.js', ...])也无济于事。原因在于Browserify源代码中的以下这一行:

mopts.extensions = [ '.js', '.json' ].concat(mopts.extensions || []);  

可以看到,无论设定的extensions值为何,.js.json永远具有最高优先级。那么,在这种情况下如何设定比.js优先级还要高的模块文件后缀名呢?

在经过一些思索后,发现这个问题只能用比较hack的方式来解决:对于上述计算最终extensions值的操作,修改JavaScript中数组的concat行为,让mopts.extensions[ '.js', '.json' ] 数组之前插入,而不是在其后添加。具体代码为:

var origin_concat = Array.prototype.concat;

Array.prototype.concat = function() {  
 if (this.length === 2 && this[0] === '.js' && this[1] === '.json') {
   return origin_concat.apply(arguments[0], this);
 }
 return origin_concat.apply(this, arguments);
};

运行以上代码后,就可以通过配置extensions: ['.web.js', ...]来用Browserify打包antd-mobile开发的Web应用了。

模块抽象:browserify-high-priority-extensions

为了方便使用,上述hack Browserify的代码被抽象为一个模块:browserify-high-priority-extensions ,其意为”让Browserify的extensions选项值具有比默认的后缀名更高的优先级“。使用该模块非常简单:

安装

$ npm install browserify-high-priority-extensions --save-dev

启用extensions高优先级设定

var hpe = require('browserify-high-priority-extensions');  
hpe.enable();  

启用后,即可通过配置extensions: ['.web.js', ...]来用Browserify打包antd-mobile开发的Web应用。

取消extensions高优先级设定 当不需要配置extensions选项高优先级时,可以用以下语句恢复到默认状态:

hpe.disable();