整理自他人博客,原文地址为 Javascript 模块规范CommonJS 的模块系统Javascript 模块化开发

CommonJS

规范

  1. module 拥有 id, uri 属性;在 module 中,有 require, exports, module 三个自由变量
  2. module 可通过 require 引入外部 module. 通过 exports 等方式向外部提供 api

示例

// module a
exports.add = function(n, m) {
return n + m;
};
// module b
var add = require('a').add;
exports.increment = function(n) {
return add(n, 1);
};
// module main:
var inc = require('b').increment;
inc(1);
module.id = 'main';

模块传送

Modules/1.1.1 规范里,只定义了模块的基本特性,并没有定义模块的存在形态。比如上面例子中的 module a, 可以是文件系统中的 a.js, 也可以是数据库中的某个字段,或者仅是封装在闭包里的一段代码。在服务器端,最常见的场景是一个模块一个文件

在服务器端,文件读取操作是同步的,模块的通讯不会很复杂,而浏览器端的则不适用。为了让模块能在不同的环境下都适用,CommonJS 需要定义 Module/Transport 规范。Module/Transport(模块传送),可以同步也可以异步

同步方案示例

<script type="text/javascript" src="require.js">
<script type="text/javascript" src="mod-a.js">
<script type="text/javascript" src="mod-b.js">
<script type="text/javascript" src="mod-main.js">

首先引入 require.js, 实现模块定义和模块加载等方法,比如 declareModule 方法。
然后在服务器端,部署时,将模块自动转换为:

// mod-a.js:
declareModule(function(require, exports, module) {
exports.add = function(n, m) {
return n + m;
};
});
// mod-b.js:
declareModule(function(require, exports, module) {
var add = require('a').add;
exports.increment = function(n) {
return add(n, 1);
};
});

将上面的代码文档化,就能定义出一个模块同步传送规范。

AMD

CommonJS 的 Module/Transport 规范里,目前认可度最高的提议是 Modules/AsynchronousDefinition(简称 AMD)。AMD 定义了用于异步加载的一种模块定义方式

define(id?, dependencies?, factory)

同步方案中,依赖关系由页面中引入的静态 script 来保障。异步方案中,依赖关系管理就不那么简单了。对于模块a, 对应文件 a.js, 其加载执行过程可分解为:

  • 脚本的下载过程:浏览器将 a.js 从服务器下载到本地。
  • 脚本的解析(parse)和执行(execute)过程:浏览器解析脚本,并执行 define 函数。
  • 模块的 attach 过程:执行模块的 factory 函数。

AMD 规定 dependencies 中的模块,可以作为 factory 的参数,这就隐性要求在执行 factory 前,所有 dependencies 的 factory 都必须已执行,这种方式可称之为 execution 模式

// a.js:
define({
add: function(n, m) {
return n + m;
}
});
// b.js:
define(['a'], function(a) {
return {
increment: function(n) {
return a.add(n, 1);
}
};
});

b.js 的写法可以有很多种,下面是另一种很常见的写法:

define(['require', 'exports', 'a'], function(require, exports) {
var add = require('a').add;
exports.increment = function(n) {
return add(n, 1);
};
});

但下面这种写法是不允许的:

define(['require', 'exports'], function(require, exports) {
var add = require('a').add;
exports.increment = function(n) {
return add(n, 1);
};
});

当 dependencies 参数存在时,模块 b 依赖的模块,必须全部显式指定。在上面的例子中,模块 b 明显还依赖模块 a, 但在 dependencies 中没有,因此不符合 AMD 规范。

但很多时候,开始书写模块代码时,我们并不能很明确的知道需要依赖哪些模块。每次调用某个依赖模块时,需要跳转到模块顶部,手动添加下 dependencies。这对开发者来说,不太友好。因此 AMD 允许以下写法:

define(function(require, exports) {
var add = require('a').add;
exports.increment = function(n) {
return add(n, 1);
};
});

当 define 只有 factory 参数时,dependencies 无需开发者提前指定,define 会调用 factory.toString 方法,通过正则匹配,自动找出需要依赖的模块

除了define外,AMD还保留一个关键字require。require 作为规范保留的全局标识符,可以实现为 module loader,也可以不实现

理解了 AMD, RequireJS 的 api 也就很容易上手了,RequireJS 是遵循 AMD 规范的。实际上,RequireJS 的作者 James Burke, 为 AMD 规范贡献了很多 idea

Wrappings

AMD 规范已经很不错,RequireJS 也很流行,jQuery 近期也加入了对 AMD 规范的支持。然而,CommonJS 社区近期有件不大不小的事,有人提出了另一种异步加载模块的定义方式:Modules/Wrappings

规范

  • 定义模块用module变量,它有一个方法declare
  • declare接受一个函数类型的参数,如称为factory
  • factory有三个参数分别为require、exports、module
  • factory使用返回值和exports导出API
  • factory如果是对象类型,则将该对象作为模块输出

示例

module.declare(factory)
// module.declare(id?, dependencies?, factory)

注意:wiki 的当前版本是 module.declare(factory). 但在这篇讨论里 AMD vs Wrappings 里,已经有了更完善的方案。

从表面上看,AMD 和 Wrappings 唯一的不同是 define 还是 module.declare 的命名差异。如果仅是这点差异的话,实在不值得新增加一个提议。Wrappings 和 AMD 最大的不同,在于 Wrappings 方案里,factory 的参数更简单,和 dependencies 无对应关系。也就是说,可以如下写代码:

module.declare(['a'], function(require, exports) {
var add = require('a').add;
exports.increment = function(n) {
return add(n, 1);
};
});

这个看似非常小的差异,可以让下面的代码合理存在并达到预期目的:

module.declare(function(require, exports) {
...
var a;
if(someCondition) {
a = require('a1');
} else {
a = require('a2');
}
...
});

AMD 里的 download/parse/execute/attach,在 Wrappings 里,attach 过程可以延后,可以等到第一次 require 时,才调用 factory. 这种模式称之为 availability 模式

availability 模式存在的问题是,需要声明变量引用模块后才能使用,否则会报错

RequireJS

终于说到 RequireJS 了。RequireJS 很优秀,用户群也不少。从目前的特性和功能来看,感觉有以下不如意:

  • 文件太大,用 google closure compiler 压缩后,12.2k. 这是在页头必须引入的脚本,还是希望越小越好
  • 功能太多。这本是优点,比如能够在各种环境下跑。但对于真实的 web 应用来说,还是希望用情专一,尽量无无用代码
  • 给 require 方法赋予了双重含义。一重含义是 CommonJS/Modules/1.1.1 规范里定义的 require, 另一重是 RequireJS 里用来加载模块和调用回调。这导致 require 的 dependencies 参数的格式,和 define 中的 dependencies 参数的格式不一致
  • 目前不支持 availability 模式
  • require.js 代码里,有 only for jQuery 的代码

CMD

AMD 是 RequireJS 在推广过程中对模块定义的规范化产出。CMD 是 SeaJS 在推广过程中对模块定义的规范化产出。类似的还有 CommonJS Modules/2.0 规范,是 BravoJS 在推广过程中对模块定义的规范化产出。这些规范的目的都是为了 JavaScript 的模块化开发,特别是在浏览器端的。目前这些规范的实现都能达成浏览器端模块化开发的目的

与 AMD 的区别

  • 对于依赖的模块,AMD 是提前执行,CMD 是延迟执行。不过 RequireJS 从 2.0 开始,也改成可以延迟执行(根据写法不同,处理方式不同)。CMD 推崇 as lazy as possible
  • CMD 推崇依赖就近,AMD 推崇依赖前置。看下面的代码说明。虽然 AMD 也支持 CMD 的写法,同时还支持将 require 作为依赖项传递,但 RequireJS 的作者默认是最喜欢上面的写法,也是官方文档里默认的模块定义写法。CMD 推崇依赖就近需要遍历所有的require关键字,找出后面的依赖。具体做法是将function toString后,用正则匹配出require关键字后面的依赖。显然,这是一种牺牲性能来换取更多开发便利的方法
  • AMD 的 API 默认是一个当多个用,CMD 的 API 严格区分,推崇职责单一。比如 AMD 里,require 分全局 require 和局部 require,都叫 require。CMD 里,没有全局 require,而是根据模块系统的完备性,提供 seajs.use 来实现模块系统的加载启动。CMD 里,每个 API 都简单纯粹
// CMD
define(function(require, exports, module) {
var a = require('./a');
a.doSomething();
// 此处略去 100 行
var b = require('./b'); // 依赖可以就近书写
b.doSomething();
});
// AMD 默认推荐的是
define(['./a', './b'], function(a, b) { // 依赖必须一开始就写好
a.doSomething()
b.doSomething()
});

SeaJS

  1. SeaJS未实现全部的 Modules 1.1.1。如require函数的main,paths属性在SeaJS中没有。但SeaJS给require添加了async、resolve、load、constructor
  2. SeaJS没有使用 Modules/Wrappings 中的module.declare定义模块,而是使用define函数(看起来象AMD中的define,实则不然)

与 RequireJS 的异同

相同之处:

RequireJS 和 Sea.js 都是模块加载器,倡导模块化开发理念,核心价值是让 JavaScript 的模块化开发变得简单自然。

不同之处:

  • 定位有差异。RequireJS 想成为浏览器端的模块加载器,同时也想成为 Rhino / Node 等环境的模块加载器。Sea.js 则专注于 Web 浏览器端,同时通过 Node 扩展的方式可以很方便跑在 Node 环境中。
  • 遵循的规范不同。RequireJS 遵循 AMD(异步模块定义)规范,Sea.js 遵循 CMD (通用模块定义)规范。规范的不同,导致了两者 API 不同。Sea.js 更贴近 CommonJS Modules/1.1 和 Node Modules 规范。
  • 推广理念有差异。RequireJS 在尝试让第三方类库修改自身来支持 RequireJS,目前只有少数社区采纳。Sea.js 不强推,采用自主封装的方式来“海纳百川”,目前已有较成熟的封装策略。
  • 对开发调试的支持有差异。Sea.js 非常关注代码的开发调试,有 nocache、debug 等用于调试的插件。RequireJS 无这方面的明显支持。
  • 插件机制不同。RequireJS 采取的是在源码中预留接口的形式,插件类型比较单一。Sea.js 采取的是通用事件机制,插件类型更丰富。

总之,如果说 RequireJS 是 Prototype 类库的话,则 Sea.js 致力于成为 jQuery 类库。

UMD

AMD以浏览器为第一(browser-first)的原则发展,选择异步加载模块。它的模块支持对象(objects)、函数(functions)、构造器(constructors)、字符串(strings)、JSON等各种类型的模块。因此在浏览器中它非常灵活。

CommonJS module以服务器端为第一(server-first)的原则发展,选择同步加载模块。它的模块是无需包装的(unwrapped modules)且贴近于ES.next/Harmony的模块格式。但它仅支持对象类型(objects)模块。

这迫使一些人又想出另一个更通用格式 UMD(Universal Module Definition)。希望提供一个前后端跨平台的解决方案

UMD 定义那些既能在客户端又能在服务器端工作的模块,这样的模块同时也能和目前可用的主流脚本加载器一同工作。

实现

UMD的实现很简单,先判断是否支持Node.js模块格式(exports是否存在),存在则使用Node.js模块格式。

再判断是否支持AMD(define是否存在),存在则使用AMD方式加载模块。前两个都不存在,则将模块公开到全局(window或global)。

// eventUtil.js
(function (root, factory) {
if (typeof exports === 'object') {
module.exports = factory();
} else if (typeof define === 'function' && define.amd) {
define(factory);
} else {
root.eventUtil = factory();
}
})(this, function() {
// module
return {
addEvent: function(el, type, handle) {
//...
},
removeEvent: function(el, type, handle) {
},
};
});
(function (root, factory) {
'use strict';
if(type exports === 'object') {
// CommonJS
factory(exports);
} else if(typeof define === 'function' && define.amd) {
// AMD. Register as an anonymous module.
define(['exports'], factory);
} else {
// Browser globals
var Foo = {};
root.Foo = Foo;
factory(Foo);
}
})((typeof window === 'object' && window) || this, function (Foo) {
'use strict';
Foo.a = function() {
console.log('a');
}
Foo.b = function() {
console.log('b');
}
});

ES Harmony/Modules

ECMAScript的下一个版本Harmony已经考虑到了模块化的需求,目前还在努力指定中。

在 ES.next 中,用import和export导入和导出模块:

import声明把某个模块的导出绑定为本地变量,并可以重命名来避免命名冲突。
export声明声明了某个模块的本地绑定是外部可见的,这样其它模块就能够读取它们但却无法进行修改。有趣的是,模块可以导出子模块,却无法导出已经在别处定义过的模块。你同样可以给导出重命名来让它们不同于本地的名字。
定义模块

使用module关键字来定义一个模块

module math {
export function sum(x, y) {
return x + y;
}
export var pi = 3.141593;
}

使用import关键字来加载外部模块

// we can import in script code, not just inside a module
import {sum, pi} from math;
alert("2π = " + sum(pi, pi));
// 引入所有API
import * from math;
alert("2π = " + sum(pi, pi));
// 使用另一个引用作为别名
// a static module reference
module M = math;
// reify M as an immutable "module instance object"
alert("2π = " + M.sum(M.pi, M.pi));
//局部重命名
import { draw: drawShape } from shape;
import { draw: drawGun } from cowboy;
// 嵌套模块
module widgets {
export module button { ... }
export module alert { ... }
export module textarea { ... }
...
}
import { messageBox, confirmDialog } from widgets.alert;
...
// 从服务器上请求的模块
<script type="harmony">
// loading from a URL
module JSON at 'http://json.org/modules/json2.js';
alert(JSON.stringify({'hi': 'world'}));
// 动态载入一个模块
Loader.load('http://json.org/modules/json2.js', function(JSON) {
alert(JSON.stringify([0, {a: true}]));
});

除此之外,还可以远程载入的模块、异步加载模块等,请参考使用 AMD、CommonJS 及 ES Harmony 编写模块化的 JavaScript