2.4 加载其他类型的模块
前面我们介绍的主要是CommonJS和ES6 Module,除此之外,在开发中我们还有可能遇到其他类型的模块,目前AMD、UMD等模块使用的场景已经不多,但当遇到这类模块时我们仍然需要知道如何处理。
2.4.1 非模块化文件
非模块化文件指的是并不遵循任何一种模块标准的文件。如果你维护的是一个几年前的项目,那么里面极有可能有非模块化文件,最常见的就是在script标签中引入的jQuery及其各种插件。
如何使用Webpack打包这类文件呢?其实只要直接引入即可,如:
import './jquery.min.js';
这句代码会直接执行jquery.min.js。一般来说,jQuery这类库都是将其接口绑定在全局,因此无论是从script标签引入,还是使用Webpack打包的方式引入,其最终效果是一样的。
但假如我们引入的非模块化文件是以隐式全局变量声明的方式暴露其接口的,则会发生问题。如:
// 通过在顶层作用域声明变量的方式暴露接口 var calculator = { // ... }
由于Webpack在打包时会为每一个文件包装一层函数作用域来避免全局污染,上面的代码将无法把calculator对象挂在全局,因此需要格外注意这种隐式全局变量声明。
2.4.2 AMD
AMD(Asynchronous Module Definition,异步模块定义)是由JavaScript社区提出的专注于支持浏览器端模块化的标准。从名字就可以看出它与CommonJS和ES6 Module最大的区别在于它加载模块的方式是异步的。下面的例子展示了如何定义一个AMD模块。
define('getSum', ['calculator'], function(math) { return function(a, b) { console.log('sum: ' + calculator.add(a, b)); } });
在AMD中使用define函数来定义模块,它可以接收3个参数。第1个参数是当前模块的id,相当于模块名;第2个参数是当前模块的依赖,比如上面我们定义的getSum模块需要引入calculator模块作为依赖;第3个参数用来描述模块的导出值,可以是函数或对象。如果是函数则导出的是函数的返回值;如果是对象则直接导出对象本身。
和CommonJS类似,AMD也使用require函数来加载模块,只不过采用异步的形式。
require(['getSum'], function(getSum) { getSum(2, 3); });
require的第1个参数指定了加载的模块,第2个参数是当加载完成后执行的回调函数。
通过AMD这种形式定义模块的好处在于其模块加载是非阻塞性的,当执行到require函数时并不会停下来去执行被加载的模块,而是继续执行require后面的代码,使得模块加载操作并不会阻塞浏览器。
尽管AMD的设计理念很好,但与同步加载的模块标准相比其语法要更加冗长。另外其异步加载的方式没有同步清晰,并且容易造成回调地狱(callback hell)。目前AMD在实际中已经用得越来越少,大多数开发者还是会选择CommonJS或ES6 Module的形式。
2.4.3 UMD
我们已经介绍了很多模块形式,如CommonJS、ES6 Module、AMD及非模块化文件,在很多时候工程中会用到其中两种形式甚至更多。有时对于一个库或者框架的开发者来说,如果面向的使用群体足够庞大,就需要考虑支持各种模块形式。
严格来说,UMD并不是一种模块标准,而是一组模块形式的集合。UMD的全称是Universal Module Definition,也就是通用模块标准,它的目标是使一个模块能运行在各种环境下,不论是CommonJS、AMD,还是非模块化的环境(当时ES6 Module还未被提出)。
请看下面的例子:
// calculator.js (function (global, main) { // 根据当前环境采取不同的导出方式 if (typeof define === 'function' && define.amd) { // AMD define(...); } else if (typeof exports === 'object') { // CommonJS module.exports = ...; } else { // 非模块化环境 global.add = ...; } }(this, function () { // 定义模块主体 return {...} }));
可以看出,UMD其实就是根据当前全局对象中的值判断目前处于哪种模块环境。当前环境是AMD,就以AMD的形式导出;当前环境是CommonJS,就以CommonJS的形式导出。
需要注意的是,UMD模块一般都最先判断AMD环境,也就是全局下是否有define函数,而通过AMD定义的模块是无法使用CommonJS或ES6 Module的形式正确引入的。在Webpack中,由于它同时支持AMD及CommonJS,也许工程中的所有模块都是CommonJS,而UMD标准却发现当前有AMD环境,并使用了AMD方式导出,这会使得模块导入时出错。当需要这样做时,我们可以更改UMD模块中判断的顺序,使其以CommonJS的形式导出。
2.4.4 加载npm模块
与Java、C++、Python等语言相比,JavaScript是一种缺乏标准库的语言。当开发者需要解决URL处理、日期解析这类很常见的问题时,很多时候只能自己动手来封装工具接口。而npm提供了这样一种方式,可以让开发者在其平台上找到由他人所开发和发布的库并安装到项目中,从而快速地解决问题,这就是npm作为包管理器为开发者带来的便利。
很多语言都有包管理器,比如Java的Maven,Ruby的gem。目前,JavaScript最主流的包管理器有两个——npm和yarn。两者的仓库是共通的,只是在使用上有所区别。截至目前,npm平台上已经有几十万个模块(也可称为包),并且这个数字每天都在增加,各种主流的框架类库都可以在npm平台上找到。作为开发者,每个人也都可以自己封装模块并上传到npm,通过这种方式来与他人共享代码。
那么如何从我们的本地工程安装和加载一个外部的npm模块呢?首先我们需要初始化一个npm工程,并通过npm来获取模块。下面以lodash这个库为例:
# 项目初始化 npm init –y # 安装lodash npm install lodash
执行了上面的命令之后,npm会将lodash安装在工程的node_modules目录下,并将对该模块的依赖信息记录在package.json中。
在使用时,加载一个npm模块的方式很简单,只需要引入包的名字即可。
// index.js import _ from 'lodash';
Webpack在解析到这条语句时会自动去node_modules中寻找名为lodash的模块,而不需要我们写出从源文件index.js到node_modules中lodash的路径。
现在我们知道,在导入一个npm模块时,只要写明它的名字就可以了。那么在实际打包的过程中具体加载的是npm模块中的哪个JS文件呢?
每一个npm模块都有一个入口。当我们加载一个模块时,实际上就是加载该模块的入口文件。这个入口被维护在模块内部package.json文件的main字段中。
比如对于前面的lodash模块来说,它的package.json内容如下:
// ./node_modules/underscore/package.json { "name": "lodash", …… "main": "lodash.js" }
当加载该模块时,实际上加载的是node_modules/lodash/lodash.js。
除了直接加载模块以外,我们也可以通过<package_name>/<path>的形式单独加载模块内部的某个JS文件。如:
import all from 'lodash/fp/all.js'; console.log('all', all);
这样,Webpack最终只会打包node_modules/lodash/fp/all.js这个文件,而不会打包全部的lodash库,进而减小打包资源的体积。