前言
前文介绍了在 Web 开发中,模块化是一项重要的技术,比如:AMD、RequireJS、CommonJS、ES6 Module。模块化的贯彻执行离不开相应的约定,即规范。这是能够进行模块化工作的重中之重。实现模块化的规范有很多,比如除了之前介绍的以外:CMD、SeaJS、UMD、ES6 Module。还有,IIFE(立即执行函数)也是实现模块化的一种方案。本文将会对其他的几种模块化技术进行说明。
一. IIFE
在 ECMAScript 6 之前,模块并没有被内置到 JavaScript 中,因为 JavaScript 最初是为小型浏览器脚本设计的。这种模块化的缺乏,导致在代码的不同部分使用了共享全局变量。
比如,对于以下代码:
var name = 'JavaScript';
var age = 20;
当上面的代码运行时,name
和 age
变量会被添加到全局对象中。因此,应用中的所有 JavaScript 脚本都可以访问全局变量 name
和 age
,这就很容易导致代码错误,因为在其他不相关的单元中也可以访问和修改这些全局变量。除此之外,向全局对象添加变量会使全局命名空间变得混乱并增加了命名冲突的机会。
所以,我们就需要一种封装变量和函数的方法,并且只对外公开定义的接口。因此,为了实现模块化并避免使用全局变量,可以使用如下方式来创建模块:
(function () {
// 声明私有变量和函数
return {
// 声明公共变量和函数
}
})();
上面的代码就是一个返回对象的闭包,这就是我们常说的 IIFE(Immediately Invoked Function Expression),即立即调用函数表达式。在该函数中,就创建了一个局部范围。这样就避免了使用全局变量(IIFE 是匿名函数),并且代码单元被封装和隔离。
可以这样来使用 IIFE 作为一个模块:
var module = (function(){
var age = 20;
var name = 'JavaScript'
var fn1 = function(){
console.log(name, age)
};
var fn2 = function(a, b){
console.log(a + b)
};
return {
age,
fn1,
fn2,
};
})();
module.age; // 20
module.fn1(); // JavaScript 20
module.fn2(128, 64); // 192
在这段代码中,module
就是我们定义的一个模块,它里面定义了两个私有变量 age
和 name
,同时定义了两个方法 fn1
和 fn2
,其中 fn1
中使用 module
中定义的私有变量,fn2
接收外部传入参数。最后,module 向外部暴露了age
、fn1
、fn2
。这样就形成了一个模块。
当试图在 module
外部直接调用fn1
时,就会报错:
fn1(); // Uncaught ReferenceError: fn1 is not defined
当试图在 module
外部打印其内部的私有变量name
时,得到的结果是 undefined
:
module.name; // undefined
上面的 IIFE 的例子是遵循模块模式的,具备其中的三部分,其中 age、name、fn1、fn2 就是模块内部的代码实现,返回的 age、fn1、fn2 就是导出的内容,即接口。调用 module
方法和变量就是导入使用。
二. CMD
CMD 全称为 Common Module Definition,即通用模块定义。CMD 规范整合了 CommonJS 和 AMD 规范的特点。sea.js 是 CMD 规范的一个实现 。
CMD 定义模块也是通过一个全局函数 define
来实现的,但只有一个参数,该参数既可以是函数也可以是对象:
define(factory);
如果这个参数是对象,那么模块导出的就是对象;如果这个参数为函数,那么这个函数会被传入 3 个参数:
define(function(require, exports, module) {
//...
});
这三个参数分别如下:(1)require
:一个函数,通过调用它可以引用其他模块,也可以调用 require.async
函数来异步调用模块;(2)exports
:一个对象,当定义模块的时候,需要通过向参数 exports
添加属性来导出模块 API;(3)module
是一个对象,它包含 3 个属性:
-
uri
:模块完整的 URI 路径; -
dependencies
:模块依赖; -
exports
:模块需要被导出的 API,作用同第二个参数exports
。
下面来看一个例子,定义一个 increment
模块,引用 math
模块的 add
函数,经过封装后导出成 increment
函数:
define(function(require, exports, module) {
var add = require('math').add;
exports.increment = function(val) {
return add(val, 1);
};
module.id = "increment";
});
CMD 最大的特点就是懒加载,不需要在定义模块的时候声明依赖,可以在模块执行时动态加载依赖。除此之外,CMD 同时支持同步加载模块和异步加载模块。
AMD 和 CMD 的两个主要区别如下:
-
AMD 需要异步加载模块,而 CMD 在加载模块时,可以同步加载(
require
),也可以异步加载(require.async
)。 -
CMD 遵循依赖就近原则,AMD 遵循依赖前置原则。也就是说,在 AMD 中,需要把模块所需要的依赖都提前在依赖数组中声明。而在 CMD 中,只需要在具体代码逻辑内,使用依赖前,把依赖的模块
require
进来。
三. UMD
UMD 全程为 Universal Module Definition,即统一模块定义。其实 UMD 并不是一个模块管理规范,而是带有前后端同构思想的模块封装工具。
UMD 是一组同时支持 AMD 和 CommonJS 的模式,它旨在使代码无论执行代码的环境如何都能正常工作,通过 UMD 可以在合适的环境选择对应的模块规范。比如在 Node.js 环境中采用 CommonJS 模块管理,在浏览器环境且支持 AMD 的情况下采用 AMD 模块,否则导出为全局函数。
一个 UMD 模块由两部分组成:
-
**立即调用函数表达式 (IIFE)**:它会检查使用模块的环境。其有两个参数:
root
和factory
。root
是对全局范围的 this 引用,而factory
是定义模块的函数。 -
匿名函数: 创建模块,此匿名函数被传递任意数量的参数以指定模块的依赖关系。
UMD 的代码实现如下:
(function (root, factory) {
if (typeof define === 'function' && define.amd) {
define([], factory);
} else if (typeof exports === 'object') {
module.exports,
module.exports = factory();
} else {
root.returnExports = factory();
}
}(this, function () {
// 模块内容定义
return {};
}));
它的执行过程如下:
-
先判断是否支持 Node.js 模块格式(exports 是否存在),存在则使用 Node.js 模块格式;
-
再判断是否支持 AMD(define 是否存在),存在则使用 AMD 方式加载模块;
-
若两个都不存在,则将模块公开到全局(Window 或 Global)。
UMD 的特点如下:
① UMD 的优点:
-
小而简洁;
-
适用于服务器端和客户端。
② UMD 的缺点:
-
不容易正确配置。