兼容前后端共用模块代码(摘自《深入浅出Node.js》)

模块的侧重点

前后端JavaScript分别搁置在HTTP的两端,它们扮演的角色并不同。浏览器端的JavaScript需要经历从同一个服务器端分发到多个客户端执行,而服务器端JavaScript则是相同的代码需要多次执行。前者的瓶颈在于带宽,后者的瓶颈则在于CPU和内存等资源。前者需要通过网络加载代码,后者从磁盘中加载,两者的加载速度不在一个数量级上。

纵观Node的模块引入过程,几乎全都是同步的。尽管与Node强调异步的行为有些相反,但它是合理的。但是如果前端模块也采用同步的方式来引入,那将会在用户体验上造成很大的问题。UI在初始化过程中需要花费很多时间来等待脚本加载完成。

鉴于网络的原因,CommonJS为后端JavaScript制定的规范并不完全适合前端的应用场景。经过一段争执之后,AMD规范最终在前端应用场景中胜出。它的全称是Asynchronous Module Definition,即是“异步模块定义”,详见https://github.com/amdjs/amdjs-api/wiki/AMD。除此之外,还有玉伯定义的CMD规范。

AMD规范

AMD规范是CommonJS模块规范的一个延伸,它的模块定义如下:

1
define(id?, dependencies?, factory);

它的模块id依赖是可选的,与Node模块相似的地方在于factory的内容就是实际代码的内容。下面的代码定义了一个简单的模块:

1
2
3
4
5
6
7
define(function() {
var exports = {};
exports.sayHello = function() {
alert('Hello from module: ' + module.id);
};
return exports;
});

不同之处在于AMD模块需要用define来明确定义一个模块,而在Node实现中是隐式包装的,它们的目的是进行作用域隔离,仅在需要的时候被引入,避免掉过去那种通过全局变量或者全局命名空间的方式,以免变量污染和不小心被修改。另一个区别则是内容需要通过返回的方式实现导出。

CMD规范

CMD规范由国内的玉伯提出,与AMD规范的主要区别在于定义模块和依赖引入的部分。AMD需要在声明模块的时候指定所有的依赖,通过形参传递依赖到模块内容中:

1
2
3
define(['dep1', 'dep2'], function (dep1, dep2) {
return function () {};
});

与AMD模块规范相比,CMD模块更接近于Node对CommonJS规范的定义:

1
define(factory);

在依赖部分,CMD支持动态引入,示例如下:

1
2
3
define(function(require, exports, module) {
// The module code goes here
});

require、exportsmodule通过形参传递给模块,在需要依赖模块时,随时调用require()引入即可。

兼容多种模块规范

为了让同一个模块可以运行在前后端,在写作过程中需要考虑兼容前端也实现了模块规范的环境。为了保持前后端的一致性,类库开发者需要将类库代码包装在一个闭包内。以下代码演示如何将hello()方法定义到不同的运行环境中,它能够兼容NodeAMDCMD以及常见的浏览器环境中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
;(function (name, definition) {
// 检测上下文环境是否为AMD或CMD
var hasDefine = typeof define === 'function',
// 检查上下文环境是否为Node
hasExports = typeof module !== 'undefined' && module.exports;

if (hasDefine) {
// AMD环境或CMD环境
define(definition);
} else if (hasExports) {
// 定义为普通Node模块
module.exports = definition();
} else {
// 将模块的执行结果挂在window变量中,在浏览器中this指向window对象
this[name] = definition();
}
})('hello', function () {
var hello = function () {};
return hello;
});

摘自《深入浅出Node.js》: 2.7 前后端共用模块