SeaJS 所为何

模块加载器使社会和谐、生活幸福、家庭美满

SeaJS 文摘

为何会有文摘?

RequireJS 其一

为何用 Web 模块的方式?

本文讨论异步模块规范(Asynchronous Module Definition,AMD)API,即 RequireJS 所支持的模块 API, 的设计驱使与使用方式。另有页面讨论 Web 模块化的基本方式

模块目的

什么是 JavaScript 模块?它们的目的是什么?

  • 定义:如何把一段代码封装成一个有用的单元,以及如何注册此模块的能力、输出的值
  • 依赖引用:如何引用其它代码单元

现今 Web

(function () {
    var $ = this.jQuery;

    this.myExample = function () {};
}());

现如今 JavaScript 代码段是如何定义的呢?

  • 通过立即执行的工厂函数定义。
  • 使用 HTML script 标签加载模块,通过全局变量来引用依赖。
  • 模块间依赖的声明很弱:开发者需要知道正确的依赖顺序。例如,包含 Backbone 的文件,不能放在 jQuery 标签之前。
  • 优化部署的时候,需要用额外的工具来把一系列 script 标签替换成一个。

这些都会使大型项目变得难以管理,尤其是当脚本们的诸多依赖还可能重叠、嵌套的时候。 手工写 script 标签可不怎么灵活,而且这么做就没法搞按需加载了。

CommonJS

var $ = require('jquery');
exports.myExample = function () {};

最初的 CommonJS 小组 的参与者们决定弄一份于时下的 JavaScript 编程语言有效,但不必束缚于浏览器 JS 环境的限制,的模块规范。开始的愿景是在浏览器里使用一些权宜之计, 并希望能借此影响浏览器厂商,促使它们为这种模块规范的原生支持提供解决方案。权宜之计有:

  • 要么使用一个服务来转译 CJS 模块成浏览器中可用的代码
  • 要么使用 XMLHttpRequest(XHR)以文本形式加载模块,再在浏览器中做文本变换、解析的工作

CJS 模块规范仅允许每文件一个模块,所以为优化、打包,可使用某种“转换格式”将多个模块合并到单个文件。

通过这种方式,CommonJS 小组搞定了依赖引用、如何处理循环依赖,以及如何获得当前模块的某些属性等问题。 但是,他们并没能接纳浏览器环境里不可改变、并且仍将影响模块设计的某些特性:

  • 网络加载
  • 异步继承

这也同时意味着他们为了实现这个规范,将负担更多地放到了 Web 开发者身上,而这些权宜之计也使调试变得更麻烦。 调试 eval 的代码,或者调试多个文件合并之后的单个文件,都有实际使用时的坏处。 这些缺点或许在未来某天会被浏览器调试工具解决掉,但结论仍然是:在最普遍的 JS 环境,浏览器中,使用 CommonJS 模块并不是最好的办法

AMD

define(['jquery'] , function ($) {
    return function () {};
});

AMD 规范的缘起是,我们需要一个比时下那种“写一堆 script 标签、手工按序指明依赖”要好,并且容易在浏览器中直接使用的模块格式。 某种不需要服务端工具配合,又能够易于调试的模块格式。它超脱于 Dojo 使用 XHR+eval 的现实经验, 并且要规避 Dojo 的方式的缺点。

它比时下 Web 开发中“全局变量+script 标签”的方式要好,因为:

  • 应用 CommonJS 实践,使用字符串 ID 来声明依赖。使得依赖声明清晰,并且避免了使用全局变量。
  • 模块 ID 可以被映射到不同的路径。从而允许切换模块实现。这对创建单元测试模型很有帮助。 在上例中,代码实际期待的不过是某个实现了 jQuery API 与行为的模块而已。并不是非要 jQuery 本身不可。
  • 封装了模块定义。使你能够避免污染全局命名空间。
  • 清楚地定义模块输出。可以用 return value;,也可以用 CommonJS 的 exports 范式。 后者在循环依赖的时候很有用。

它是 CommonJS 模块规范的改善,因为:

  • 它在浏览器里跑得更好,它的坑是最少的。其他的解决方案,都有调试、跨域、CDN 使用,file:// 协议以及依赖服务端工具等问题。
  • 定义了将多个模块包含进单个文件的方式。在 CommonJS 的条款里,有个“传输规范”来做这个事情, 但 CommonJS 小组还没就此达成一致
  • 允许将函数作为返回值。这在模块返回值是构造函数的时候尤其有用。在 CommonJS 中就有点尴尬了, 永远要通过给 exports 对象设置属性来输出。Node 支持了 module.exports = function() {}, 不过这还没在 CommonJS 规范里面。

模块定义

使用 JavaScript 的函数进行封装,已经有 文档 约定了:

(function () {
   this.myGlobal = function () {};
}());

这种模块依赖于给全局对象附加属性来输出模块值,并且用这种模型很难声明模块依赖。 预设是,模块的依赖在执行此函数之前就已经是立即可用的。这限制了加载依赖的策略。

AMD 通过如下手段解决了这些问题:

  • 调用 define() 来注册工厂函数,而不是立即执行该函数。
  • 以字符串数组的形式将依赖传递进来,而不是直接从全局对象上取。
  • 在所有依赖都被加载、执行完毕之后,才执行工厂函数。
  • 将依赖的模块作为执行工厂函数的参数。
//Calling define with a dependency array and a factory function
define(['dep1', 'dep2'], function (dep1, dep2) {

    //Define the module value by returning a value.
    return function () {};
});

命名的模块

请注意,上例中的模块并没有给自己取名。这使得这个模块可以随便移动。它允许开发者将模块放到不同的目录, 从而管它们叫不同的名字。AMD 加载器会根据它被其他脚本引用的方式来给这个模块标记 ID。

但是,为了提高性能,打包多个模块的工具需要有个给单个文件中的每个模块命名的方式。对此需求,AMD 允许以字符串作为 define() 的第一个参数:

//Calling define with module ID, dependency array, and factory function
define('myModule', ['dep1', 'dep2'], function (dep1, dep2) {

    //Define the module value by returning a value.
    return function () {};
});

你应该避免自行命名模块,并且在开发时保持每文件一个模块。只不过,在注重性能的阶段, 得有个模块规范以在编译后的资源中命名模块。

语法糖

上面的 AMD 例子在所有浏览器中都能跑。但是,有搞错模块数组与参数顺序的风险,尤其是当你的模块依赖了好多的时候:

define([ "require", "jquery", "blade/object", "blade/fn", "rdapi",
         "oauth", "blade/jig", "blade/url", "dispatch", "accounts",
         "storage", "services", "widgets/AccountPanel", "widgets/TabButton",
         "widgets/AddAccount", "less", "osTheme", "jquery-ui-1.8.7.min",
         "jquery.textOverflow"],
function (require,   $,        object,         fn,         rdapi,
          oauth,   jig,         url,         dispatch,   accounts,
          storage,   services,   AccountPanel,           TabButton,
          AddAccount,           less,   osTheme) {

});

简而为之,同时也让封装 CommonJS 模块更容易,还支持这种形式的 define() (有时这种封装还被称作“简化 CommonJS 封装”):

define(function (require) {
    var dependency1 = require('dependency1'),
        dependency2 = require('dependency2');

    return function () {};
});

AMD 加载器会使用 Function.prototype.toString() 来解析出所有的 require('') 调用, 然后在内部将上述 define 调用转换成这种形式:

define(['require', 'dependency1', 'dependency2'], function (require) {
    var dependency1 = require('dependency1'),
        dependency2 = require('dependency2');

    return function () {};
});

如此,加载器会异步加载依赖一(dependency1)与依赖二(dependency2),执行这些依赖,然后执行这个工厂函数。

并非所有的浏览器都给出可用的 Function.prototype.toString() 结果。自2011年10月, PS3 和老旧的 Opera Mobile 浏览器的返回结果就不准确。这些浏览器也更可能因网络与设备的限制, 需要编译优化模块代码,所以直接用个懂得如何解析、转换正确的依赖数组的优化工具来打包优化一下就好, 例如 RequireJS 优化工具

因为不支持 toString() 解析的浏览器相当少,用这种语法糖形式来编写你的模块是安全可靠的。 特别是当你喜欢把依赖按照它们的名字排列整齐的时候。

CommonJS 兼容性

虽然这个语法糖形式被称作“简化 CommonJS 封装”,但它并非与 CommonJS 模块 100% 兼容。不过,CommonJS 模块一般认为依赖是同步加载的,完全兼容它的话,在浏览器里就会挂掉啦。

绝大部分 CJS(CommonJS)模块,根据个人经验(不太科学地毛估估)大约有 95%,是与简化 CommonJS 封装完美兼容的。

不兼容的模块,都是那些依赖是动态计算的,调用 require() 的时候不用字符串字面量的,以及其他不像个声明方式调用 require() 的。 所以,下边这种会挂:

// 不好
var mod = require(someCondition ? 'a' : 'b');

// 不好
// 译注:这种应该不会挂,但是不能达到选择加载的效果,会在请求、加载完 a 和 a1 之后,
// 才执行模块代码
if (someCondition) {
    var a = require('a');
} else {
    var a = require('a1');
}

这些使用方式,在 require 回调 中支持, 即 AMD 加载器中提供的全局的

require([moduleName], function() {});

AMD 的执行模式比较向 ECMAScript 和谐版(Harmoney)的模块规范所约定的看齐。 CommonJS 模块规范在 AMD 中的不兼容部分在和谐版(Harmoney)模块也一样不兼容。 AMD 的代码执行逻辑的未来兼容性更好。

冗长与可用性

对 AMD 的批评其一,至少与 CJS 模块规范相比,是它要求一层缩进,以及一个函数封装。

但事实很简单:要用 AMD 就觉得需要多打点字并多缩进一层,其实这一点都没关系。你编程的时间是这么花的:

  • 思考问题
  • 阅读代码

绝大部分编程的时间是花在思考而非敲代码上的。虽然代码一般越短越好,但所能付出的代价终归是有限的, 何况用 AMD 要再打些的字其实也没那么多。

而且大部分 Web 开发者本来就在用匿名函数封装了,目的是避免污染全局变量。看到逻辑代码外边包了匿名函数,其实是很普遍的, 并不会给阅读模块代码带来困扰。

同时,CommonJS 格式还有些潜在代价:

  • 依赖工具的代价
  • 某些边界用例在浏览器中会挂,比如跨域访问
  • 调试更差,此代价随着时间增加,会越来越大

AMD 模块规范需要的工具更少,边界用例问题更少,调试支持也更好。

重要的是:能够真正与其他人分享代码。AMD 规范是达此目标更轻松的方式。

拥有一个可用的,易于调试,并且还在现如今的浏览器中能跑的模块系统,意味着在创造未来的 JavaScript 中最好的模块系统的同时,得到现实世界的体验。

AMD 和它相关的 API 们,已经帮助展示了任何未来 JS 的模块系统都具备的以下特性:

  • 返回一个函数作为模块值,尤其是构造函数,促使更好的 API 设计。Node 有 module.exports 来支持此特性, 但能够用 return function() {} 的话会更简洁。这意味着不需要持有 modulemodule.exports, 并且代码表达也更清晰。
  • 动态代码加载(AMD 模块系统中通过 require[],function(){}) 来实现) 是基本要求。CJS 小组谈到了这个问题,提了一些议案,但它并没有广为接受。Node 并不支持这个需求,转而依赖 require() 的同步行为, 而这在 Web 端是无从实现的。
  • 加载器插件 相当有用。它帮助避免基于回调的编程中常遇到的嵌套缩进的问题。
  • 选择映射某个模块 从另一个地址加载,使得提供模块模拟对象以供测试变得容易。
  • 每个模块最多一个 IO 行为,并且这个 IO 行为应该简单直接。Web 浏览器受不了找个模块还有多次 IO 查找。 这和 Node 里现在的多次寻址是相悖的,并且避免使用一个有 main 属性的 package.json。 用能够很容易地根据项目目录结构映射到某个地址的模块名,一个合理的不需要冗长的配置的约定就够了,不过要允许必要的简单配置。
  • 最好有个“选择加入”调用,用来使旧 JS 代码快速参与新的模块系统之中。

如果一个 JS 模块系统不能搞定以上特性,那么与 AMD 和它相关的 API 们,例如 callback-require加载器插件 和基于路径的模块 ID,相较之下,高下立见。

AMD 使用情况

截至 2011年10月,AMD 已经在 Web 上广为使用:

你可以做什么

如果你是写应用的:

如果你是脚本库开发者:

  • 如果可用,条件调用 define()。妙处在于不依靠 AMD 你仍然可以编写你的库,只要可用的时候参与一下就可以了。 这使得你的模块用户可以:
    • 避免往页面全局变量里头塞东西
    • 代码加载、延迟加载等更加有选择
    • 用现有的 AMD 工具来优化它们的项目
    • 参与到当今浏览器中可用的 JS 模块系统中去

如果你为 JavaScript 加载器、引擎、环境编写代码:

  • 实现 AMD API。有个 讨论列表兼容性测试。通过实现 AMD 模块规范,你可以减少多模块系统的无谓竞争, 帮助为一个 Web 端可用的 JavaScript 模块系统证明。这也能反馈到 ECMAScript 工作组,以求更好的本地模块系统支持。
  • 也要支持 callback-require加载器插件。 加载器插件是个很好的减轻回调、异步风格的代码中常见的嵌套回调症的方式。

RequireJS 其二

为何用 AMD 规范?

本文讨论异步模块规范(Asynchronous Module Definition,AMD)API,即 RequireJS 所支持的模块 API, 的设计驱使与使用方式。另有页面讨论 Web 模块化的基本方式

模块目的

什么是 JavaScript 模块?它们的目的是什么?

  • 定义:如何把一段代码封装成一个有用的单元,以及如何注册此模块的能力、输出的值
  • 依赖引用:如何引用其它代码单元

现今 Web

(function () {
    var $ = this.jQuery;

    this.myExample = function () {};
}());

现如今 JavaScript 代码段是如何定义的呢?

  • 通过立即执行的工厂函数定义。
  • 使用 HTML script 标签加载模块,通过全局变量来引用依赖。
  • 模块间依赖的声明很弱:开发者需要知道正确的依赖顺序。例如,包含 Backbone 的文件,不能放在 jQuery 标签之前。
  • 优化部署的时候,需要用额外的工具来把一系列 script 标签替换成一个。

这些都会使大型项目变得难以管理,尤其是当脚本们的诸多依赖还可能重叠、嵌套的时候。 手工写 script 标签可不怎么灵活,而且这么做就没法搞按需加载了。

CommonJS

var $ = require('jquery');
exports.myExample = function () {};

最初的 CommonJS 小组 的参与者们决定弄一份于时下的 JavaScript 编程语言有效,但不必束缚于浏览器 JS 环境的限制,的模块规范。开始的愿景是在浏览器里使用一些权宜之计, 并希望能借此影响浏览器厂商,促使它们为这种模块规范的原生支持提供解决方案。权宜之计有:

  • 要么使用一个服务来转译 CJS 模块成浏览器中可用的代码
  • 要么使用 XMLHttpRequest(XHR)以文本形式加载模块,再在浏览器中做文本变换、解析的工作

CJS 模块规范仅允许每文件一个模块,所以为优化、打包,可使用某种“转换格式”将多个模块合并到单个文件。

通过这种方式,CommonJS 小组搞定了依赖引用、如何处理循环依赖,以及如何获得当前模块的某些属性等问题。 但是,他们并没能接纳浏览器环境里不可改变、并且仍将影响模块设计的某些特性:

  • 网络加载
  • 异步继承

这也同时意味着他们为了实现这个规范,将负担更多地放到了 Web 开发者身上,而这些权宜之计也使调试变得更麻烦。 调试 eval 的代码,或者调试多个文件合并之后的单个文件,都有实际使用时的坏处。 这些缺点或许在未来某天会被浏览器调试工具解决掉,但结论仍然是:在最普遍的 JS 环境,浏览器中,使用 CommonJS 模块并不是最好的办法

AMD

define(['jquery'] , function ($) {
    return function () {};
});

AMD 规范的缘起是,我们需要一个比时下那种“写一堆 script 标签、手工按序指明依赖”要好,并且容易在浏览器中直接使用的模块格式。 某种不需要服务端工具配合,又能够易于调试的模块格式。它超脱于 Dojo 使用 XHR+eval 的现实经验, 并且要规避 Dojo 的方式的缺点。

它比时下 Web 开发中“全局变量+script 标签”的方式要好,因为:

  • 应用 CommonJS 实践,使用字符串 ID 来声明依赖。使得依赖声明清晰,并且避免了使用全局变量。
  • 模块 ID 可以被映射到不同的路径。从而允许切换模块实现。这对创建单元测试模型很有帮助。 在上例中,代码实际期待的不过是某个实现了 jQuery API 与行为的模块而已。并不是非要 jQuery 本身不可。
  • 封装了模块定义。使你能够避免污染全局命名空间。
  • 清楚地定义模块输出。可以用 return value;,也可以用 CommonJS 的 exports 范式。 后者在循环依赖的时候很有用。

它是 CommonJS 模块规范的改善,因为:

  • 它在浏览器里跑得更好,它的坑是最少的。其他的解决方案,都有调试、跨域、CDN 使用,file:// 协议以及依赖服务端工具等问题。
  • 定义了将多个模块包含进单个文件的方式。在 CommonJS 的条款里,有个“传输规范”来做这个事情, 但 CommonJS 小组还没就此达成一致
  • 允许将函数作为返回值。这在模块返回值是构造函数的时候尤其有用。在 CommonJS 中就有点尴尬了, 永远要通过给 exports 对象设置属性来输出。Node 支持了 module.exports = function() {}, 不过这还没在 CommonJS 规范里面。

模块定义

使用 JavaScript 的函数进行封装,已经有 文档 约定了:

(function () {
   this.myGlobal = function () {};
}());

这种模块依赖于给全局对象附加属性来输出模块值,并且用这种模型很难声明模块依赖。 预设是,模块的依赖在执行此函数之前就已经是立即可用的。这限制了加载依赖的策略。

AMD 通过如下手段解决了这些问题:

  • 调用 define() 来注册工厂函数,而不是立即执行该函数。
  • 以字符串数组的形式将依赖传递进来,而不是直接从全局对象上取。
  • 在所有依赖都被加载、执行完毕之后,才执行工厂函数。
  • 将依赖的模块作为执行工厂函数的参数。
//Calling define with a dependency array and a factory function
define(['dep1', 'dep2'], function (dep1, dep2) {

    //Define the module value by returning a value.
    return function () {};
});

命名的模块

请注意,上例中的模块并没有给自己取名。这使得这个模块可以随便移动。它允许开发者将模块放到不同的目录, 从而管它们叫不同的名字。AMD 加载器会根据它被其他脚本引用的方式来给这个模块标记 ID。

但是,为了提高性能,打包多个模块的工具需要有个给单个文件中的每个模块命名的方式。对此需求,AMD 允许以字符串作为 define() 的第一个参数:

//Calling define with module ID, dependency array, and factory function
define('myModule', ['dep1', 'dep2'], function (dep1, dep2) {

    //Define the module value by returning a value.
    return function () {};
});

你应该避免自行命名模块,并且在开发时保持每文件一个模块。只不过,在注重性能的阶段, 得有个模块规范以在编译后的资源中命名模块。

语法糖

上面的 AMD 例子在所有浏览器中都能跑。但是,有搞错模块数组与参数顺序的风险,尤其是当你的模块依赖了好多的时候:

define([ "require", "jquery", "blade/object", "blade/fn", "rdapi",
         "oauth", "blade/jig", "blade/url", "dispatch", "accounts",
         "storage", "services", "widgets/AccountPanel", "widgets/TabButton",
         "widgets/AddAccount", "less", "osTheme", "jquery-ui-1.8.7.min",
         "jquery.textOverflow"],
function (require,   $,        object,         fn,         rdapi,
          oauth,   jig,         url,         dispatch,   accounts,
          storage,   services,   AccountPanel,           TabButton,
          AddAccount,           less,   osTheme) {

});

简而为之,同时也让封装 CommonJS 模块更容易,还支持这种形式的 define() (有时这种封装还被称作“简化 CommonJS 封装”):

define(function (require) {
    var dependency1 = require('dependency1'),
        dependency2 = require('dependency2');

    return function () {};
});

AMD 加载器会使用 Function.prototype.toString() 来解析出所有的 require('') 调用, 然后在内部将上述 define 调用转换成这种形式:

define(['require', 'dependency1', 'dependency2'], function (require) {
    var dependency1 = require('dependency1'),
        dependency2 = require('dependency2');

    return function () {};
});

如此,加载器会异步加载依赖一(dependency1)与依赖二(dependency2),执行这些依赖,然后执行这个工厂函数。

并非所有的浏览器都给出可用的 Function.prototype.toString() 结果。自2011年10月, PS3 和老旧的 Opera Mobile 浏览器的返回结果就不准确。这些浏览器也更可能因网络与设备的限制, 需要编译优化模块代码,所以直接用个懂得如何解析、转换正确的依赖数组的优化工具来打包优化一下就好, 例如 RequireJS 优化工具

因为不支持 toString() 解析的浏览器相当少,用这种语法糖形式来编写你的模块是安全可靠的。 特别是当你喜欢把依赖按照它们的名字排列整齐的时候。

CommonJS 兼容性

虽然这个语法糖形式被称作“简化 CommonJS 封装”,但它并非与 CommonJS 模块 100% 兼容。不过,CommonJS 模块一般认为依赖是同步加载的,完全兼容它的话,在浏览器里就会挂掉啦。

绝大部分 CJS(CommonJS)模块,根据个人经验(不太科学地毛估估)大约有 95%,是与简化 CommonJS 封装完美兼容的。

不兼容的模块,都是那些依赖是动态计算的,调用 require() 的时候不用字符串字面量的,以及其他不像个声明方式调用 require() 的。 所以,下边这种会挂:

// 不好
var mod = require(someCondition ? 'a' : 'b');

// 不好
// 译注:这种应该不会挂,但是不能达到选择加载的效果,会在请求、加载完 a 和 a1 之后,
// 才执行模块代码
if (someCondition) {
    var a = require('a');
} else {
    var a = require('a1');
}

这些使用方式,在 require 回调 中支持, 即 AMD 加载器中提供的全局的

require([moduleName], function() {});

AMD 的执行模式比较向 ECMAScript 和谐版(Harmoney)的模块规范所约定的看齐。 CommonJS 模块规范在 AMD 中的不兼容部分在和谐版(Harmoney)模块也一样不兼容。 AMD 的代码执行逻辑的未来兼容性更好。

冗长与可用性

对 AMD 的批评其一,至少与 CJS 模块规范相比,是它要求一层缩进,以及一个函数封装。

但事实很简单:要用 AMD 就觉得需要多打点字并多缩进一层,其实这一点都没关系。你编程的时间是这么花的:

  • 思考问题
  • 阅读代码

绝大部分编程的时间是花在思考而非敲代码上的。虽然代码一般越短越好,但所能付出的代价终归是有限的, 何况用 AMD 要再打些的字其实也没那么多。

而且大部分 Web 开发者本来就在用匿名函数封装了,目的是避免污染全局变量。看到逻辑代码外边包了匿名函数,其实是很普遍的, 并不会给阅读模块代码带来困扰。

同时,CommonJS 格式还有些潜在代价:

  • 依赖工具的代价
  • 某些边界用例在浏览器中会挂,比如跨域访问
  • 调试更差,此代价随着时间增加,会越来越大

AMD 模块规范需要的工具更少,边界用例问题更少,调试支持也更好。

重要的是:能够真正与其他人分享代码。AMD 规范是达此目标更轻松的方式。

拥有一个可用的,易于调试,并且还在现如今的浏览器中能跑的模块系统,意味着在创造未来的 JavaScript 中最好的模块系统的同时,得到现实世界的体验。

AMD 和它相关的 API 们,已经帮助展示了任何未来 JS 的模块系统都具备的以下特性:

  • 返回一个函数作为模块值,尤其是构造函数,促使更好的 API 设计。Node 有 module.exports 来支持此特性, 但能够用 return function() {} 的话会更简洁。这意味着不需要持有 modulemodule.exports, 并且代码表达也更清晰。
  • 动态代码加载(AMD 模块系统中通过 require[],function(){}) 来实现) 是基本要求。CJS 小组谈到了这个问题,提了一些议案,但它并没有广为接受。Node 并不支持这个需求,转而依赖 require() 的同步行为, 而这在 Web 端是无从实现的。
  • 加载器插件 相当有用。它帮助避免基于回调的编程中常遇到的嵌套缩进的问题。
  • 选择映射某个模块 从另一个地址加载,使得提供模块模拟对象以供测试变得容易。
  • 每个模块最多一个 IO 行为,并且这个 IO 行为应该简单直接。Web 浏览器受不了找个模块还有多次 IO 查找。 这和 Node 里现在的多次寻址是相悖的,并且避免使用一个有 main 属性的 package.json。 用能够很容易地根据项目目录结构映射到某个地址的模块名,一个合理的不需要冗长的配置的约定就够了,不过要允许必要的简单配置。
  • 最好有个“选择加入”调用,用来使旧 JS 代码快速参与新的模块系统之中。

如果一个 JS 模块系统不能搞定以上特性,那么与 AMD 和它相关的 API 们,例如 callback-require加载器插件 和基于路径的模块 ID,相较之下,高下立见。

AMD 使用情况

截至 2011年10月,AMD 已经在 Web 上广为使用:

你可以做什么

如果你是写应用的:

如果你是脚本库开发者:

  • 如果可用,条件调用 define()。妙处在于不依靠 AMD 你仍然可以编写你的库,只要可用的时候参与一下就可以了。 这使得你的模块用户可以:
    • 避免往页面全局变量里头塞东西
    • 代码加载、延迟加载等更加有选择
    • 用现有的 AMD 工具来优化它们的项目
    • 参与到当今浏览器中可用的 JS 模块系统中去

如果你为 JavaScript 加载器、引擎、环境编写代码:

  • 实现 AMD API。有个 讨论列表兼容性测试。通过实现 AMD 模块规范,你可以减少多模块系统的无谓竞争, 帮助为一个 Web 端可用的 JavaScript 模块系统证明。这也能反馈到 ECMAScript 工作组,以求更好的本地模块系统支持。
  • 也要支持 callback-require加载器插件。 加载器插件是个很好的减轻回调、异步风格的代码中常见的嵌套回调症的方式。
comments powered by Disqus

SeaJS 模块预处理

如何正确压缩、打包你的模块们

RequireJS 其一

为何用 Web 模块的方式?

本文讨论异步模块规范(Asynchronous Module Definition,AMD)API,即 RequireJS 所支持的模块 API, 的设计驱使与使用方式。另有页面讨论 Web 模块化的基本方式

模块目的

什么是 JavaScript 模块?它们的目的是什么?

  • 定义:如何把一段代码封装成一个有用的单元,以及如何注册此模块的能力、输出的值
  • 依赖引用:如何引用其它代码单元

现今 Web

(function () {
    var $ = this.jQuery;

    this.myExample = function () {};
}());

现如今 JavaScript 代码段是如何定义的呢?

  • 通过立即执行的工厂函数定义。
  • 使用 HTML script 标签加载模块,通过全局变量来引用依赖。
  • 模块间依赖的声明很弱:开发者需要知道正确的依赖顺序。例如,包含 Backbone 的文件,不能放在 jQuery 标签之前。
  • 优化部署的时候,需要用额外的工具来把一系列 script 标签替换成一个。

这些都会使大型项目变得难以管理,尤其是当脚本们的诸多依赖还可能重叠、嵌套的时候。 手工写 script 标签可不怎么灵活,而且这么做就没法搞按需加载了。

CommonJS

var $ = require('jquery');
exports.myExample = function () {};

最初的 CommonJS 小组 的参与者们决定弄一份于时下的 JavaScript 编程语言有效,但不必束缚于浏览器 JS 环境的限制,的模块规范。开始的愿景是在浏览器里使用一些权宜之计, 并希望能借此影响浏览器厂商,促使它们为这种模块规范的原生支持提供解决方案。权宜之计有:

  • 要么使用一个服务来转译 CJS 模块成浏览器中可用的代码
  • 要么使用 XMLHttpRequest(XHR)以文本形式加载模块,再在浏览器中做文本变换、解析的工作

CJS 模块规范仅允许每文件一个模块,所以为优化、打包,可使用某种“转换格式”将多个模块合并到单个文件。

通过这种方式,CommonJS 小组搞定了依赖引用、如何处理循环依赖,以及如何获得当前模块的某些属性等问题。 但是,他们并没能接纳浏览器环境里不可改变、并且仍将影响模块设计的某些特性:

  • 网络加载
  • 异步继承

这也同时意味着他们为了实现这个规范,将负担更多地放到了 Web 开发者身上,而这些权宜之计也使调试变得更麻烦。 调试 eval 的代码,或者调试多个文件合并之后的单个文件,都有实际使用时的坏处。 这些缺点或许在未来某天会被浏览器调试工具解决掉,但结论仍然是:在最普遍的 JS 环境,浏览器中,使用 CommonJS 模块并不是最好的办法

AMD

define(['jquery'] , function ($) {
    return function () {};
});

AMD 规范的缘起是,我们需要一个比时下那种“写一堆 script 标签、手工按序指明依赖”要好,并且容易在浏览器中直接使用的模块格式。 某种不需要服务端工具配合,又能够易于调试的模块格式。它超脱于 Dojo 使用 XHR+eval 的现实经验, 并且要规避 Dojo 的方式的缺点。

它比时下 Web 开发中“全局变量+script 标签”的方式要好,因为:

  • 应用 CommonJS 实践,使用字符串 ID 来声明依赖。使得依赖声明清晰,并且避免了使用全局变量。
  • 模块 ID 可以被映射到不同的路径。从而允许切换模块实现。这对创建单元测试模型很有帮助。 在上例中,代码实际期待的不过是某个实现了 jQuery API 与行为的模块而已。并不是非要 jQuery 本身不可。
  • 封装了模块定义。使你能够避免污染全局命名空间。
  • 清楚地定义模块输出。可以用 return value;,也可以用 CommonJS 的 exports 范式。 后者在循环依赖的时候很有用。

它是 CommonJS 模块规范的改善,因为:

  • 它在浏览器里跑得更好,它的坑是最少的。其他的解决方案,都有调试、跨域、CDN 使用,file:// 协议以及依赖服务端工具等问题。
  • 定义了将多个模块包含进单个文件的方式。在 CommonJS 的条款里,有个“传输规范”来做这个事情, 但 CommonJS 小组还没就此达成一致
  • 允许将函数作为返回值。这在模块返回值是构造函数的时候尤其有用。在 CommonJS 中就有点尴尬了, 永远要通过给 exports 对象设置属性来输出。Node 支持了 module.exports = function() {}, 不过这还没在 CommonJS 规范里面。

模块定义

使用 JavaScript 的函数进行封装,已经有 文档 约定了:

(function () {
   this.myGlobal = function () {};
}());

这种模块依赖于给全局对象附加属性来输出模块值,并且用这种模型很难声明模块依赖。 预设是,模块的依赖在执行此函数之前就已经是立即可用的。这限制了加载依赖的策略。

AMD 通过如下手段解决了这些问题:

  • 调用 define() 来注册工厂函数,而不是立即执行该函数。
  • 以字符串数组的形式将依赖传递进来,而不是直接从全局对象上取。
  • 在所有依赖都被加载、执行完毕之后,才执行工厂函数。
  • 将依赖的模块作为执行工厂函数的参数。
//Calling define with a dependency array and a factory function
define(['dep1', 'dep2'], function (dep1, dep2) {

    //Define the module value by returning a value.
    return function () {};
});

命名的模块

请注意,上例中的模块并没有给自己取名。这使得这个模块可以随便移动。它允许开发者将模块放到不同的目录, 从而管它们叫不同的名字。AMD 加载器会根据它被其他脚本引用的方式来给这个模块标记 ID。

但是,为了提高性能,打包多个模块的工具需要有个给单个文件中的每个模块命名的方式。对此需求,AMD 允许以字符串作为 define() 的第一个参数:

//Calling define with module ID, dependency array, and factory function
define('myModule', ['dep1', 'dep2'], function (dep1, dep2) {

    //Define the module value by returning a value.
    return function () {};
});

你应该避免自行命名模块,并且在开发时保持每文件一个模块。只不过,在注重性能的阶段, 得有个模块规范以在编译后的资源中命名模块。

语法糖

上面的 AMD 例子在所有浏览器中都能跑。但是,有搞错模块数组与参数顺序的风险,尤其是当你的模块依赖了好多的时候:

define([ "require", "jquery", "blade/object", "blade/fn", "rdapi",
         "oauth", "blade/jig", "blade/url", "dispatch", "accounts",
         "storage", "services", "widgets/AccountPanel", "widgets/TabButton",
         "widgets/AddAccount", "less", "osTheme", "jquery-ui-1.8.7.min",
         "jquery.textOverflow"],
function (require,   $,        object,         fn,         rdapi,
          oauth,   jig,         url,         dispatch,   accounts,
          storage,   services,   AccountPanel,           TabButton,
          AddAccount,           less,   osTheme) {

});

简而为之,同时也让封装 CommonJS 模块更容易,还支持这种形式的 define() (有时这种封装还被称作“简化 CommonJS 封装”):

define(function (require) {
    var dependency1 = require('dependency1'),
        dependency2 = require('dependency2');

    return function () {};
});

AMD 加载器会使用 Function.prototype.toString() 来解析出所有的 require('') 调用, 然后在内部将上述 define 调用转换成这种形式:

define(['require', 'dependency1', 'dependency2'], function (require) {
    var dependency1 = require('dependency1'),
        dependency2 = require('dependency2');

    return function () {};
});

如此,加载器会异步加载依赖一(dependency1)与依赖二(dependency2),执行这些依赖,然后执行这个工厂函数。

并非所有的浏览器都给出可用的 Function.prototype.toString() 结果。自2011年10月, PS3 和老旧的 Opera Mobile 浏览器的返回结果就不准确。这些浏览器也更可能因网络与设备的限制, 需要编译优化模块代码,所以直接用个懂得如何解析、转换正确的依赖数组的优化工具来打包优化一下就好, 例如 RequireJS 优化工具

因为不支持 toString() 解析的浏览器相当少,用这种语法糖形式来编写你的模块是安全可靠的。 特别是当你喜欢把依赖按照它们的名字排列整齐的时候。

CommonJS 兼容性

虽然这个语法糖形式被称作“简化 CommonJS 封装”,但它并非与 CommonJS 模块 100% 兼容。不过,CommonJS 模块一般认为依赖是同步加载的,完全兼容它的话,在浏览器里就会挂掉啦。

绝大部分 CJS(CommonJS)模块,根据个人经验(不太科学地毛估估)大约有 95%,是与简化 CommonJS 封装完美兼容的。

不兼容的模块,都是那些依赖是动态计算的,调用 require() 的时候不用字符串字面量的,以及其他不像个声明方式调用 require() 的。 所以,下边这种会挂:

// 不好
var mod = require(someCondition ? 'a' : 'b');

// 不好
// 译注:这种应该不会挂,但是不能达到选择加载的效果,会在请求、加载完 a 和 a1 之后,
// 才执行模块代码
if (someCondition) {
    var a = require('a');
} else {
    var a = require('a1');
}

这些使用方式,在 require 回调 中支持, 即 AMD 加载器中提供的全局的

require([moduleName], function() {});

AMD 的执行模式比较向 ECMAScript 和谐版(Harmoney)的模块规范所约定的看齐。 CommonJS 模块规范在 AMD 中的不兼容部分在和谐版(Harmoney)模块也一样不兼容。 AMD 的代码执行逻辑的未来兼容性更好。

冗长与可用性

对 AMD 的批评其一,至少与 CJS 模块规范相比,是它要求一层缩进,以及一个函数封装。

但事实很简单:要用 AMD 就觉得需要多打点字并多缩进一层,其实这一点都没关系。你编程的时间是这么花的:

  • 思考问题
  • 阅读代码

绝大部分编程的时间是花在思考而非敲代码上的。虽然代码一般越短越好,但所能付出的代价终归是有限的, 何况用 AMD 要再打些的字其实也没那么多。

而且大部分 Web 开发者本来就在用匿名函数封装了,目的是避免污染全局变量。看到逻辑代码外边包了匿名函数,其实是很普遍的, 并不会给阅读模块代码带来困扰。

同时,CommonJS 格式还有些潜在代价:

  • 依赖工具的代价
  • 某些边界用例在浏览器中会挂,比如跨域访问
  • 调试更差,此代价随着时间增加,会越来越大

AMD 模块规范需要的工具更少,边界用例问题更少,调试支持也更好。

重要的是:能够真正与其他人分享代码。AMD 规范是达此目标更轻松的方式。

拥有一个可用的,易于调试,并且还在现如今的浏览器中能跑的模块系统,意味着在创造未来的 JavaScript 中最好的模块系统的同时,得到现实世界的体验。

AMD 和它相关的 API 们,已经帮助展示了任何未来 JS 的模块系统都具备的以下特性:

  • 返回一个函数作为模块值,尤其是构造函数,促使更好的 API 设计。Node 有 module.exports 来支持此特性, 但能够用 return function() {} 的话会更简洁。这意味着不需要持有 modulemodule.exports, 并且代码表达也更清晰。
  • 动态代码加载(AMD 模块系统中通过 require[],function(){}) 来实现) 是基本要求。CJS 小组谈到了这个问题,提了一些议案,但它并没有广为接受。Node 并不支持这个需求,转而依赖 require() 的同步行为, 而这在 Web 端是无从实现的。
  • 加载器插件 相当有用。它帮助避免基于回调的编程中常遇到的嵌套缩进的问题。
  • 选择映射某个模块 从另一个地址加载,使得提供模块模拟对象以供测试变得容易。
  • 每个模块最多一个 IO 行为,并且这个 IO 行为应该简单直接。Web 浏览器受不了找个模块还有多次 IO 查找。 这和 Node 里现在的多次寻址是相悖的,并且避免使用一个有 main 属性的 package.json。 用能够很容易地根据项目目录结构映射到某个地址的模块名,一个合理的不需要冗长的配置的约定就够了,不过要允许必要的简单配置。
  • 最好有个“选择加入”调用,用来使旧 JS 代码快速参与新的模块系统之中。

如果一个 JS 模块系统不能搞定以上特性,那么与 AMD 和它相关的 API 们,例如 callback-require加载器插件 和基于路径的模块 ID,相较之下,高下立见。

AMD 使用情况

截至 2011年10月,AMD 已经在 Web 上广为使用:

你可以做什么

如果你是写应用的:

如果你是脚本库开发者:

  • 如果可用,条件调用 define()。妙处在于不依靠 AMD 你仍然可以编写你的库,只要可用的时候参与一下就可以了。 这使得你的模块用户可以:
    • 避免往页面全局变量里头塞东西
    • 代码加载、延迟加载等更加有选择
    • 用现有的 AMD 工具来优化它们的项目
    • 参与到当今浏览器中可用的 JS 模块系统中去

如果你为 JavaScript 加载器、引擎、环境编写代码:

  • 实现 AMD API。有个 讨论列表兼容性测试。通过实现 AMD 模块规范,你可以减少多模块系统的无谓竞争, 帮助为一个 Web 端可用的 JavaScript 模块系统证明。这也能反馈到 ECMAScript 工作组,以求更好的本地模块系统支持。
  • 也要支持 callback-require加载器插件。 加载器插件是个很好的减轻回调、异步风格的代码中常见的嵌套回调症的方式。

RequireJS 其二

为何用 AMD 规范?

本文讨论异步模块规范(Asynchronous Module Definition,AMD)API,即 RequireJS 所支持的模块 API, 的设计驱使与使用方式。另有页面讨论 Web 模块化的基本方式

模块目的

什么是 JavaScript 模块?它们的目的是什么?

  • 定义:如何把一段代码封装成一个有用的单元,以及如何注册此模块的能力、输出的值
  • 依赖引用:如何引用其它代码单元

现今 Web

(function () {
    var $ = this.jQuery;

    this.myExample = function () {};
}());

现如今 JavaScript 代码段是如何定义的呢?

  • 通过立即执行的工厂函数定义。
  • 使用 HTML script 标签加载模块,通过全局变量来引用依赖。
  • 模块间依赖的声明很弱:开发者需要知道正确的依赖顺序。例如,包含 Backbone 的文件,不能放在 jQuery 标签之前。
  • 优化部署的时候,需要用额外的工具来把一系列 script 标签替换成一个。

这些都会使大型项目变得难以管理,尤其是当脚本们的诸多依赖还可能重叠、嵌套的时候。 手工写 script 标签可不怎么灵活,而且这么做就没法搞按需加载了。

CommonJS

var $ = require('jquery');
exports.myExample = function () {};

最初的 CommonJS 小组 的参与者们决定弄一份于时下的 JavaScript 编程语言有效,但不必束缚于浏览器 JS 环境的限制,的模块规范。开始的愿景是在浏览器里使用一些权宜之计, 并希望能借此影响浏览器厂商,促使它们为这种模块规范的原生支持提供解决方案。权宜之计有:

  • 要么使用一个服务来转译 CJS 模块成浏览器中可用的代码
  • 要么使用 XMLHttpRequest(XHR)以文本形式加载模块,再在浏览器中做文本变换、解析的工作

CJS 模块规范仅允许每文件一个模块,所以为优化、打包,可使用某种“转换格式”将多个模块合并到单个文件。

通过这种方式,CommonJS 小组搞定了依赖引用、如何处理循环依赖,以及如何获得当前模块的某些属性等问题。 但是,他们并没能接纳浏览器环境里不可改变、并且仍将影响模块设计的某些特性:

  • 网络加载
  • 异步继承

这也同时意味着他们为了实现这个规范,将负担更多地放到了 Web 开发者身上,而这些权宜之计也使调试变得更麻烦。 调试 eval 的代码,或者调试多个文件合并之后的单个文件,都有实际使用时的坏处。 这些缺点或许在未来某天会被浏览器调试工具解决掉,但结论仍然是:在最普遍的 JS 环境,浏览器中,使用 CommonJS 模块并不是最好的办法

AMD

define(['jquery'] , function ($) {
    return function () {};
});

AMD 规范的缘起是,我们需要一个比时下那种“写一堆 script 标签、手工按序指明依赖”要好,并且容易在浏览器中直接使用的模块格式。 某种不需要服务端工具配合,又能够易于调试的模块格式。它超脱于 Dojo 使用 XHR+eval 的现实经验, 并且要规避 Dojo 的方式的缺点。

它比时下 Web 开发中“全局变量+script 标签”的方式要好,因为:

  • 应用 CommonJS 实践,使用字符串 ID 来声明依赖。使得依赖声明清晰,并且避免了使用全局变量。
  • 模块 ID 可以被映射到不同的路径。从而允许切换模块实现。这对创建单元测试模型很有帮助。 在上例中,代码实际期待的不过是某个实现了 jQuery API 与行为的模块而已。并不是非要 jQuery 本身不可。
  • 封装了模块定义。使你能够避免污染全局命名空间。
  • 清楚地定义模块输出。可以用 return value;,也可以用 CommonJS 的 exports 范式。 后者在循环依赖的时候很有用。

它是 CommonJS 模块规范的改善,因为:

  • 它在浏览器里跑得更好,它的坑是最少的。其他的解决方案,都有调试、跨域、CDN 使用,file:// 协议以及依赖服务端工具等问题。
  • 定义了将多个模块包含进单个文件的方式。在 CommonJS 的条款里,有个“传输规范”来做这个事情, 但 CommonJS 小组还没就此达成一致
  • 允许将函数作为返回值。这在模块返回值是构造函数的时候尤其有用。在 CommonJS 中就有点尴尬了, 永远要通过给 exports 对象设置属性来输出。Node 支持了 module.exports = function() {}, 不过这还没在 CommonJS 规范里面。

模块定义

使用 JavaScript 的函数进行封装,已经有 文档 约定了:

(function () {
   this.myGlobal = function () {};
}());

这种模块依赖于给全局对象附加属性来输出模块值,并且用这种模型很难声明模块依赖。 预设是,模块的依赖在执行此函数之前就已经是立即可用的。这限制了加载依赖的策略。

AMD 通过如下手段解决了这些问题:

  • 调用 define() 来注册工厂函数,而不是立即执行该函数。
  • 以字符串数组的形式将依赖传递进来,而不是直接从全局对象上取。
  • 在所有依赖都被加载、执行完毕之后,才执行工厂函数。
  • 将依赖的模块作为执行工厂函数的参数。
//Calling define with a dependency array and a factory function
define(['dep1', 'dep2'], function (dep1, dep2) {

    //Define the module value by returning a value.
    return function () {};
});

命名的模块

请注意,上例中的模块并没有给自己取名。这使得这个模块可以随便移动。它允许开发者将模块放到不同的目录, 从而管它们叫不同的名字。AMD 加载器会根据它被其他脚本引用的方式来给这个模块标记 ID。

但是,为了提高性能,打包多个模块的工具需要有个给单个文件中的每个模块命名的方式。对此需求,AMD 允许以字符串作为 define() 的第一个参数:

//Calling define with module ID, dependency array, and factory function
define('myModule', ['dep1', 'dep2'], function (dep1, dep2) {

    //Define the module value by returning a value.
    return function () {};
});

你应该避免自行命名模块,并且在开发时保持每文件一个模块。只不过,在注重性能的阶段, 得有个模块规范以在编译后的资源中命名模块。

语法糖

上面的 AMD 例子在所有浏览器中都能跑。但是,有搞错模块数组与参数顺序的风险,尤其是当你的模块依赖了好多的时候:

define([ "require", "jquery", "blade/object", "blade/fn", "rdapi",
         "oauth", "blade/jig", "blade/url", "dispatch", "accounts",
         "storage", "services", "widgets/AccountPanel", "widgets/TabButton",
         "widgets/AddAccount", "less", "osTheme", "jquery-ui-1.8.7.min",
         "jquery.textOverflow"],
function (require,   $,        object,         fn,         rdapi,
          oauth,   jig,         url,         dispatch,   accounts,
          storage,   services,   AccountPanel,           TabButton,
          AddAccount,           less,   osTheme) {

});

简而为之,同时也让封装 CommonJS 模块更容易,还支持这种形式的 define() (有时这种封装还被称作“简化 CommonJS 封装”):

define(function (require) {
    var dependency1 = require('dependency1'),
        dependency2 = require('dependency2');

    return function () {};
});

AMD 加载器会使用 Function.prototype.toString() 来解析出所有的 require('') 调用, 然后在内部将上述 define 调用转换成这种形式:

define(['require', 'dependency1', 'dependency2'], function (require) {
    var dependency1 = require('dependency1'),
        dependency2 = require('dependency2');

    return function () {};
});

如此,加载器会异步加载依赖一(dependency1)与依赖二(dependency2),执行这些依赖,然后执行这个工厂函数。

并非所有的浏览器都给出可用的 Function.prototype.toString() 结果。自2011年10月, PS3 和老旧的 Opera Mobile 浏览器的返回结果就不准确。这些浏览器也更可能因网络与设备的限制, 需要编译优化模块代码,所以直接用个懂得如何解析、转换正确的依赖数组的优化工具来打包优化一下就好, 例如 RequireJS 优化工具

因为不支持 toString() 解析的浏览器相当少,用这种语法糖形式来编写你的模块是安全可靠的。 特别是当你喜欢把依赖按照它们的名字排列整齐的时候。

CommonJS 兼容性

虽然这个语法糖形式被称作“简化 CommonJS 封装”,但它并非与 CommonJS 模块 100% 兼容。不过,CommonJS 模块一般认为依赖是同步加载的,完全兼容它的话,在浏览器里就会挂掉啦。

绝大部分 CJS(CommonJS)模块,根据个人经验(不太科学地毛估估)大约有 95%,是与简化 CommonJS 封装完美兼容的。

不兼容的模块,都是那些依赖是动态计算的,调用 require() 的时候不用字符串字面量的,以及其他不像个声明方式调用 require() 的。 所以,下边这种会挂:

// 不好
var mod = require(someCondition ? 'a' : 'b');

// 不好
// 译注:这种应该不会挂,但是不能达到选择加载的效果,会在请求、加载完 a 和 a1 之后,
// 才执行模块代码
if (someCondition) {
    var a = require('a');
} else {
    var a = require('a1');
}

这些使用方式,在 require 回调 中支持, 即 AMD 加载器中提供的全局的

require([moduleName], function() {});

AMD 的执行模式比较向 ECMAScript 和谐版(Harmoney)的模块规范所约定的看齐。 CommonJS 模块规范在 AMD 中的不兼容部分在和谐版(Harmoney)模块也一样不兼容。 AMD 的代码执行逻辑的未来兼容性更好。

冗长与可用性

对 AMD 的批评其一,至少与 CJS 模块规范相比,是它要求一层缩进,以及一个函数封装。

但事实很简单:要用 AMD 就觉得需要多打点字并多缩进一层,其实这一点都没关系。你编程的时间是这么花的:

  • 思考问题
  • 阅读代码

绝大部分编程的时间是花在思考而非敲代码上的。虽然代码一般越短越好,但所能付出的代价终归是有限的, 何况用 AMD 要再打些的字其实也没那么多。

而且大部分 Web 开发者本来就在用匿名函数封装了,目的是避免污染全局变量。看到逻辑代码外边包了匿名函数,其实是很普遍的, 并不会给阅读模块代码带来困扰。

同时,CommonJS 格式还有些潜在代价:

  • 依赖工具的代价
  • 某些边界用例在浏览器中会挂,比如跨域访问
  • 调试更差,此代价随着时间增加,会越来越大

AMD 模块规范需要的工具更少,边界用例问题更少,调试支持也更好。

重要的是:能够真正与其他人分享代码。AMD 规范是达此目标更轻松的方式。

拥有一个可用的,易于调试,并且还在现如今的浏览器中能跑的模块系统,意味着在创造未来的 JavaScript 中最好的模块系统的同时,得到现实世界的体验。

AMD 和它相关的 API 们,已经帮助展示了任何未来 JS 的模块系统都具备的以下特性:

  • 返回一个函数作为模块值,尤其是构造函数,促使更好的 API 设计。Node 有 module.exports 来支持此特性, 但能够用 return function() {} 的话会更简洁。这意味着不需要持有 modulemodule.exports, 并且代码表达也更清晰。
  • 动态代码加载(AMD 模块系统中通过 require[],function(){}) 来实现) 是基本要求。CJS 小组谈到了这个问题,提了一些议案,但它并没有广为接受。Node 并不支持这个需求,转而依赖 require() 的同步行为, 而这在 Web 端是无从实现的。
  • 加载器插件 相当有用。它帮助避免基于回调的编程中常遇到的嵌套缩进的问题。
  • 选择映射某个模块 从另一个地址加载,使得提供模块模拟对象以供测试变得容易。
  • 每个模块最多一个 IO 行为,并且这个 IO 行为应该简单直接。Web 浏览器受不了找个模块还有多次 IO 查找。 这和 Node 里现在的多次寻址是相悖的,并且避免使用一个有 main 属性的 package.json。 用能够很容易地根据项目目录结构映射到某个地址的模块名,一个合理的不需要冗长的配置的约定就够了,不过要允许必要的简单配置。
  • 最好有个“选择加入”调用,用来使旧 JS 代码快速参与新的模块系统之中。

如果一个 JS 模块系统不能搞定以上特性,那么与 AMD 和它相关的 API 们,例如 callback-require加载器插件 和基于路径的模块 ID,相较之下,高下立见。

AMD 使用情况

截至 2011年10月,AMD 已经在 Web 上广为使用:

你可以做什么

如果你是写应用的:

如果你是脚本库开发者:

  • 如果可用,条件调用 define()。妙处在于不依靠 AMD 你仍然可以编写你的库,只要可用的时候参与一下就可以了。 这使得你的模块用户可以:
    • 避免往页面全局变量里头塞东西
    • 代码加载、延迟加载等更加有选择
    • 用现有的 AMD 工具来优化它们的项目
    • 参与到当今浏览器中可用的 JS 模块系统中去

如果你为 JavaScript 加载器、引擎、环境编写代码:

  • 实现 AMD API。有个 讨论列表兼容性测试。通过实现 AMD 模块规范,你可以减少多模块系统的无谓竞争, 帮助为一个 Web 端可用的 JavaScript 模块系统证明。这也能反馈到 ECMAScript 工作组,以求更好的本地模块系统支持。
  • 也要支持 callback-require加载器插件。 加载器插件是个很好的减轻回调、异步风格的代码中常见的嵌套回调症的方式。
comments powered by Disqus
comments powered by Disqus

SeaJS 语法一二

如何将你的代码模块化

SeaJS 文摘

为何会有文摘?

RequireJS 其一

为何用 Web 模块的方式?

本文讨论异步模块规范(Asynchronous Module Definition,AMD)API,即 RequireJS 所支持的模块 API, 的设计驱使与使用方式。另有页面讨论 Web 模块化的基本方式

模块目的

什么是 JavaScript 模块?它们的目的是什么?

  • 定义:如何把一段代码封装成一个有用的单元,以及如何注册此模块的能力、输出的值
  • 依赖引用:如何引用其它代码单元

现今 Web

(function () {
    var $ = this.jQuery;

    this.myExample = function () {};
}());

现如今 JavaScript 代码段是如何定义的呢?

  • 通过立即执行的工厂函数定义。
  • 使用 HTML script 标签加载模块,通过全局变量来引用依赖。
  • 模块间依赖的声明很弱:开发者需要知道正确的依赖顺序。例如,包含 Backbone 的文件,不能放在 jQuery 标签之前。
  • 优化部署的时候,需要用额外的工具来把一系列 script 标签替换成一个。

这些都会使大型项目变得难以管理,尤其是当脚本们的诸多依赖还可能重叠、嵌套的时候。 手工写 script 标签可不怎么灵活,而且这么做就没法搞按需加载了。

CommonJS

var $ = require('jquery');
exports.myExample = function () {};

最初的 CommonJS 小组 的参与者们决定弄一份于时下的 JavaScript 编程语言有效,但不必束缚于浏览器 JS 环境的限制,的模块规范。开始的愿景是在浏览器里使用一些权宜之计, 并希望能借此影响浏览器厂商,促使它们为这种模块规范的原生支持提供解决方案。权宜之计有:

  • 要么使用一个服务来转译 CJS 模块成浏览器中可用的代码
  • 要么使用 XMLHttpRequest(XHR)以文本形式加载模块,再在浏览器中做文本变换、解析的工作

CJS 模块规范仅允许每文件一个模块,所以为优化、打包,可使用某种“转换格式”将多个模块合并到单个文件。

通过这种方式,CommonJS 小组搞定了依赖引用、如何处理循环依赖,以及如何获得当前模块的某些属性等问题。 但是,他们并没能接纳浏览器环境里不可改变、并且仍将影响模块设计的某些特性:

  • 网络加载
  • 异步继承

这也同时意味着他们为了实现这个规范,将负担更多地放到了 Web 开发者身上,而这些权宜之计也使调试变得更麻烦。 调试 eval 的代码,或者调试多个文件合并之后的单个文件,都有实际使用时的坏处。 这些缺点或许在未来某天会被浏览器调试工具解决掉,但结论仍然是:在最普遍的 JS 环境,浏览器中,使用 CommonJS 模块并不是最好的办法

AMD

define(['jquery'] , function ($) {
    return function () {};
});

AMD 规范的缘起是,我们需要一个比时下那种“写一堆 script 标签、手工按序指明依赖”要好,并且容易在浏览器中直接使用的模块格式。 某种不需要服务端工具配合,又能够易于调试的模块格式。它超脱于 Dojo 使用 XHR+eval 的现实经验, 并且要规避 Dojo 的方式的缺点。

它比时下 Web 开发中“全局变量+script 标签”的方式要好,因为:

  • 应用 CommonJS 实践,使用字符串 ID 来声明依赖。使得依赖声明清晰,并且避免了使用全局变量。
  • 模块 ID 可以被映射到不同的路径。从而允许切换模块实现。这对创建单元测试模型很有帮助。 在上例中,代码实际期待的不过是某个实现了 jQuery API 与行为的模块而已。并不是非要 jQuery 本身不可。
  • 封装了模块定义。使你能够避免污染全局命名空间。
  • 清楚地定义模块输出。可以用 return value;,也可以用 CommonJS 的 exports 范式。 后者在循环依赖的时候很有用。

它是 CommonJS 模块规范的改善,因为:

  • 它在浏览器里跑得更好,它的坑是最少的。其他的解决方案,都有调试、跨域、CDN 使用,file:// 协议以及依赖服务端工具等问题。
  • 定义了将多个模块包含进单个文件的方式。在 CommonJS 的条款里,有个“传输规范”来做这个事情, 但 CommonJS 小组还没就此达成一致
  • 允许将函数作为返回值。这在模块返回值是构造函数的时候尤其有用。在 CommonJS 中就有点尴尬了, 永远要通过给 exports 对象设置属性来输出。Node 支持了 module.exports = function() {}, 不过这还没在 CommonJS 规范里面。

模块定义

使用 JavaScript 的函数进行封装,已经有 文档 约定了:

(function () {
   this.myGlobal = function () {};
}());

这种模块依赖于给全局对象附加属性来输出模块值,并且用这种模型很难声明模块依赖。 预设是,模块的依赖在执行此函数之前就已经是立即可用的。这限制了加载依赖的策略。

AMD 通过如下手段解决了这些问题:

  • 调用 define() 来注册工厂函数,而不是立即执行该函数。
  • 以字符串数组的形式将依赖传递进来,而不是直接从全局对象上取。
  • 在所有依赖都被加载、执行完毕之后,才执行工厂函数。
  • 将依赖的模块作为执行工厂函数的参数。
//Calling define with a dependency array and a factory function
define(['dep1', 'dep2'], function (dep1, dep2) {

    //Define the module value by returning a value.
    return function () {};
});

命名的模块

请注意,上例中的模块并没有给自己取名。这使得这个模块可以随便移动。它允许开发者将模块放到不同的目录, 从而管它们叫不同的名字。AMD 加载器会根据它被其他脚本引用的方式来给这个模块标记 ID。

但是,为了提高性能,打包多个模块的工具需要有个给单个文件中的每个模块命名的方式。对此需求,AMD 允许以字符串作为 define() 的第一个参数:

//Calling define with module ID, dependency array, and factory function
define('myModule', ['dep1', 'dep2'], function (dep1, dep2) {

    //Define the module value by returning a value.
    return function () {};
});

你应该避免自行命名模块,并且在开发时保持每文件一个模块。只不过,在注重性能的阶段, 得有个模块规范以在编译后的资源中命名模块。

语法糖

上面的 AMD 例子在所有浏览器中都能跑。但是,有搞错模块数组与参数顺序的风险,尤其是当你的模块依赖了好多的时候:

define([ "require", "jquery", "blade/object", "blade/fn", "rdapi",
         "oauth", "blade/jig", "blade/url", "dispatch", "accounts",
         "storage", "services", "widgets/AccountPanel", "widgets/TabButton",
         "widgets/AddAccount", "less", "osTheme", "jquery-ui-1.8.7.min",
         "jquery.textOverflow"],
function (require,   $,        object,         fn,         rdapi,
          oauth,   jig,         url,         dispatch,   accounts,
          storage,   services,   AccountPanel,           TabButton,
          AddAccount,           less,   osTheme) {

});

简而为之,同时也让封装 CommonJS 模块更容易,还支持这种形式的 define() (有时这种封装还被称作“简化 CommonJS 封装”):

define(function (require) {
    var dependency1 = require('dependency1'),
        dependency2 = require('dependency2');

    return function () {};
});

AMD 加载器会使用 Function.prototype.toString() 来解析出所有的 require('') 调用, 然后在内部将上述 define 调用转换成这种形式:

define(['require', 'dependency1', 'dependency2'], function (require) {
    var dependency1 = require('dependency1'),
        dependency2 = require('dependency2');

    return function () {};
});

如此,加载器会异步加载依赖一(dependency1)与依赖二(dependency2),执行这些依赖,然后执行这个工厂函数。

并非所有的浏览器都给出可用的 Function.prototype.toString() 结果。自2011年10月, PS3 和老旧的 Opera Mobile 浏览器的返回结果就不准确。这些浏览器也更可能因网络与设备的限制, 需要编译优化模块代码,所以直接用个懂得如何解析、转换正确的依赖数组的优化工具来打包优化一下就好, 例如 RequireJS 优化工具

因为不支持 toString() 解析的浏览器相当少,用这种语法糖形式来编写你的模块是安全可靠的。 特别是当你喜欢把依赖按照它们的名字排列整齐的时候。

CommonJS 兼容性

虽然这个语法糖形式被称作“简化 CommonJS 封装”,但它并非与 CommonJS 模块 100% 兼容。不过,CommonJS 模块一般认为依赖是同步加载的,完全兼容它的话,在浏览器里就会挂掉啦。

绝大部分 CJS(CommonJS)模块,根据个人经验(不太科学地毛估估)大约有 95%,是与简化 CommonJS 封装完美兼容的。

不兼容的模块,都是那些依赖是动态计算的,调用 require() 的时候不用字符串字面量的,以及其他不像个声明方式调用 require() 的。 所以,下边这种会挂:

// 不好
var mod = require(someCondition ? 'a' : 'b');

// 不好
// 译注:这种应该不会挂,但是不能达到选择加载的效果,会在请求、加载完 a 和 a1 之后,
// 才执行模块代码
if (someCondition) {
    var a = require('a');
} else {
    var a = require('a1');
}

这些使用方式,在 require 回调 中支持, 即 AMD 加载器中提供的全局的

require([moduleName], function() {});

AMD 的执行模式比较向 ECMAScript 和谐版(Harmoney)的模块规范所约定的看齐。 CommonJS 模块规范在 AMD 中的不兼容部分在和谐版(Harmoney)模块也一样不兼容。 AMD 的代码执行逻辑的未来兼容性更好。

冗长与可用性

对 AMD 的批评其一,至少与 CJS 模块规范相比,是它要求一层缩进,以及一个函数封装。

但事实很简单:要用 AMD 就觉得需要多打点字并多缩进一层,其实这一点都没关系。你编程的时间是这么花的:

  • 思考问题
  • 阅读代码

绝大部分编程的时间是花在思考而非敲代码上的。虽然代码一般越短越好,但所能付出的代价终归是有限的, 何况用 AMD 要再打些的字其实也没那么多。

而且大部分 Web 开发者本来就在用匿名函数封装了,目的是避免污染全局变量。看到逻辑代码外边包了匿名函数,其实是很普遍的, 并不会给阅读模块代码带来困扰。

同时,CommonJS 格式还有些潜在代价:

  • 依赖工具的代价
  • 某些边界用例在浏览器中会挂,比如跨域访问
  • 调试更差,此代价随着时间增加,会越来越大

AMD 模块规范需要的工具更少,边界用例问题更少,调试支持也更好。

重要的是:能够真正与其他人分享代码。AMD 规范是达此目标更轻松的方式。

拥有一个可用的,易于调试,并且还在现如今的浏览器中能跑的模块系统,意味着在创造未来的 JavaScript 中最好的模块系统的同时,得到现实世界的体验。

AMD 和它相关的 API 们,已经帮助展示了任何未来 JS 的模块系统都具备的以下特性:

  • 返回一个函数作为模块值,尤其是构造函数,促使更好的 API 设计。Node 有 module.exports 来支持此特性, 但能够用 return function() {} 的话会更简洁。这意味着不需要持有 modulemodule.exports, 并且代码表达也更清晰。
  • 动态代码加载(AMD 模块系统中通过 require[],function(){}) 来实现) 是基本要求。CJS 小组谈到了这个问题,提了一些议案,但它并没有广为接受。Node 并不支持这个需求,转而依赖 require() 的同步行为, 而这在 Web 端是无从实现的。
  • 加载器插件 相当有用。它帮助避免基于回调的编程中常遇到的嵌套缩进的问题。
  • 选择映射某个模块 从另一个地址加载,使得提供模块模拟对象以供测试变得容易。
  • 每个模块最多一个 IO 行为,并且这个 IO 行为应该简单直接。Web 浏览器受不了找个模块还有多次 IO 查找。 这和 Node 里现在的多次寻址是相悖的,并且避免使用一个有 main 属性的 package.json。 用能够很容易地根据项目目录结构映射到某个地址的模块名,一个合理的不需要冗长的配置的约定就够了,不过要允许必要的简单配置。
  • 最好有个“选择加入”调用,用来使旧 JS 代码快速参与新的模块系统之中。

如果一个 JS 模块系统不能搞定以上特性,那么与 AMD 和它相关的 API 们,例如 callback-require加载器插件 和基于路径的模块 ID,相较之下,高下立见。

AMD 使用情况

截至 2011年10月,AMD 已经在 Web 上广为使用:

你可以做什么

如果你是写应用的:

如果你是脚本库开发者:

  • 如果可用,条件调用 define()。妙处在于不依靠 AMD 你仍然可以编写你的库,只要可用的时候参与一下就可以了。 这使得你的模块用户可以:
    • 避免往页面全局变量里头塞东西
    • 代码加载、延迟加载等更加有选择
    • 用现有的 AMD 工具来优化它们的项目
    • 参与到当今浏览器中可用的 JS 模块系统中去

如果你为 JavaScript 加载器、引擎、环境编写代码:

  • 实现 AMD API。有个 讨论列表兼容性测试。通过实现 AMD 模块规范,你可以减少多模块系统的无谓竞争, 帮助为一个 Web 端可用的 JavaScript 模块系统证明。这也能反馈到 ECMAScript 工作组,以求更好的本地模块系统支持。
  • 也要支持 callback-require加载器插件。 加载器插件是个很好的减轻回调、异步风格的代码中常见的嵌套回调症的方式。

RequireJS 其二

为何用 AMD 规范?

本文讨论异步模块规范(Asynchronous Module Definition,AMD)API,即 RequireJS 所支持的模块 API, 的设计驱使与使用方式。另有页面讨论 Web 模块化的基本方式

模块目的

什么是 JavaScript 模块?它们的目的是什么?

  • 定义:如何把一段代码封装成一个有用的单元,以及如何注册此模块的能力、输出的值
  • 依赖引用:如何引用其它代码单元

现今 Web

(function () {
    var $ = this.jQuery;

    this.myExample = function () {};
}());

现如今 JavaScript 代码段是如何定义的呢?

  • 通过立即执行的工厂函数定义。
  • 使用 HTML script 标签加载模块,通过全局变量来引用依赖。
  • 模块间依赖的声明很弱:开发者需要知道正确的依赖顺序。例如,包含 Backbone 的文件,不能放在 jQuery 标签之前。
  • 优化部署的时候,需要用额外的工具来把一系列 script 标签替换成一个。

这些都会使大型项目变得难以管理,尤其是当脚本们的诸多依赖还可能重叠、嵌套的时候。 手工写 script 标签可不怎么灵活,而且这么做就没法搞按需加载了。

CommonJS

var $ = require('jquery');
exports.myExample = function () {};

最初的 CommonJS 小组 的参与者们决定弄一份于时下的 JavaScript 编程语言有效,但不必束缚于浏览器 JS 环境的限制,的模块规范。开始的愿景是在浏览器里使用一些权宜之计, 并希望能借此影响浏览器厂商,促使它们为这种模块规范的原生支持提供解决方案。权宜之计有:

  • 要么使用一个服务来转译 CJS 模块成浏览器中可用的代码
  • 要么使用 XMLHttpRequest(XHR)以文本形式加载模块,再在浏览器中做文本变换、解析的工作

CJS 模块规范仅允许每文件一个模块,所以为优化、打包,可使用某种“转换格式”将多个模块合并到单个文件。

通过这种方式,CommonJS 小组搞定了依赖引用、如何处理循环依赖,以及如何获得当前模块的某些属性等问题。 但是,他们并没能接纳浏览器环境里不可改变、并且仍将影响模块设计的某些特性:

  • 网络加载
  • 异步继承

这也同时意味着他们为了实现这个规范,将负担更多地放到了 Web 开发者身上,而这些权宜之计也使调试变得更麻烦。 调试 eval 的代码,或者调试多个文件合并之后的单个文件,都有实际使用时的坏处。 这些缺点或许在未来某天会被浏览器调试工具解决掉,但结论仍然是:在最普遍的 JS 环境,浏览器中,使用 CommonJS 模块并不是最好的办法

AMD

define(['jquery'] , function ($) {
    return function () {};
});

AMD 规范的缘起是,我们需要一个比时下那种“写一堆 script 标签、手工按序指明依赖”要好,并且容易在浏览器中直接使用的模块格式。 某种不需要服务端工具配合,又能够易于调试的模块格式。它超脱于 Dojo 使用 XHR+eval 的现实经验, 并且要规避 Dojo 的方式的缺点。

它比时下 Web 开发中“全局变量+script 标签”的方式要好,因为:

  • 应用 CommonJS 实践,使用字符串 ID 来声明依赖。使得依赖声明清晰,并且避免了使用全局变量。
  • 模块 ID 可以被映射到不同的路径。从而允许切换模块实现。这对创建单元测试模型很有帮助。 在上例中,代码实际期待的不过是某个实现了 jQuery API 与行为的模块而已。并不是非要 jQuery 本身不可。
  • 封装了模块定义。使你能够避免污染全局命名空间。
  • 清楚地定义模块输出。可以用 return value;,也可以用 CommonJS 的 exports 范式。 后者在循环依赖的时候很有用。

它是 CommonJS 模块规范的改善,因为:

  • 它在浏览器里跑得更好,它的坑是最少的。其他的解决方案,都有调试、跨域、CDN 使用,file:// 协议以及依赖服务端工具等问题。
  • 定义了将多个模块包含进单个文件的方式。在 CommonJS 的条款里,有个“传输规范”来做这个事情, 但 CommonJS 小组还没就此达成一致
  • 允许将函数作为返回值。这在模块返回值是构造函数的时候尤其有用。在 CommonJS 中就有点尴尬了, 永远要通过给 exports 对象设置属性来输出。Node 支持了 module.exports = function() {}, 不过这还没在 CommonJS 规范里面。

模块定义

使用 JavaScript 的函数进行封装,已经有 文档 约定了:

(function () {
   this.myGlobal = function () {};
}());

这种模块依赖于给全局对象附加属性来输出模块值,并且用这种模型很难声明模块依赖。 预设是,模块的依赖在执行此函数之前就已经是立即可用的。这限制了加载依赖的策略。

AMD 通过如下手段解决了这些问题:

  • 调用 define() 来注册工厂函数,而不是立即执行该函数。
  • 以字符串数组的形式将依赖传递进来,而不是直接从全局对象上取。
  • 在所有依赖都被加载、执行完毕之后,才执行工厂函数。
  • 将依赖的模块作为执行工厂函数的参数。
//Calling define with a dependency array and a factory function
define(['dep1', 'dep2'], function (dep1, dep2) {

    //Define the module value by returning a value.
    return function () {};
});

命名的模块

请注意,上例中的模块并没有给自己取名。这使得这个模块可以随便移动。它允许开发者将模块放到不同的目录, 从而管它们叫不同的名字。AMD 加载器会根据它被其他脚本引用的方式来给这个模块标记 ID。

但是,为了提高性能,打包多个模块的工具需要有个给单个文件中的每个模块命名的方式。对此需求,AMD 允许以字符串作为 define() 的第一个参数:

//Calling define with module ID, dependency array, and factory function
define('myModule', ['dep1', 'dep2'], function (dep1, dep2) {

    //Define the module value by returning a value.
    return function () {};
});

你应该避免自行命名模块,并且在开发时保持每文件一个模块。只不过,在注重性能的阶段, 得有个模块规范以在编译后的资源中命名模块。

语法糖

上面的 AMD 例子在所有浏览器中都能跑。但是,有搞错模块数组与参数顺序的风险,尤其是当你的模块依赖了好多的时候:

define([ "require", "jquery", "blade/object", "blade/fn", "rdapi",
         "oauth", "blade/jig", "blade/url", "dispatch", "accounts",
         "storage", "services", "widgets/AccountPanel", "widgets/TabButton",
         "widgets/AddAccount", "less", "osTheme", "jquery-ui-1.8.7.min",
         "jquery.textOverflow"],
function (require,   $,        object,         fn,         rdapi,
          oauth,   jig,         url,         dispatch,   accounts,
          storage,   services,   AccountPanel,           TabButton,
          AddAccount,           less,   osTheme) {

});

简而为之,同时也让封装 CommonJS 模块更容易,还支持这种形式的 define() (有时这种封装还被称作“简化 CommonJS 封装”):

define(function (require) {
    var dependency1 = require('dependency1'),
        dependency2 = require('dependency2');

    return function () {};
});

AMD 加载器会使用 Function.prototype.toString() 来解析出所有的 require('') 调用, 然后在内部将上述 define 调用转换成这种形式:

define(['require', 'dependency1', 'dependency2'], function (require) {
    var dependency1 = require('dependency1'),
        dependency2 = require('dependency2');

    return function () {};
});

如此,加载器会异步加载依赖一(dependency1)与依赖二(dependency2),执行这些依赖,然后执行这个工厂函数。

并非所有的浏览器都给出可用的 Function.prototype.toString() 结果。自2011年10月, PS3 和老旧的 Opera Mobile 浏览器的返回结果就不准确。这些浏览器也更可能因网络与设备的限制, 需要编译优化模块代码,所以直接用个懂得如何解析、转换正确的依赖数组的优化工具来打包优化一下就好, 例如 RequireJS 优化工具

因为不支持 toString() 解析的浏览器相当少,用这种语法糖形式来编写你的模块是安全可靠的。 特别是当你喜欢把依赖按照它们的名字排列整齐的时候。

CommonJS 兼容性

虽然这个语法糖形式被称作“简化 CommonJS 封装”,但它并非与 CommonJS 模块 100% 兼容。不过,CommonJS 模块一般认为依赖是同步加载的,完全兼容它的话,在浏览器里就会挂掉啦。

绝大部分 CJS(CommonJS)模块,根据个人经验(不太科学地毛估估)大约有 95%,是与简化 CommonJS 封装完美兼容的。

不兼容的模块,都是那些依赖是动态计算的,调用 require() 的时候不用字符串字面量的,以及其他不像个声明方式调用 require() 的。 所以,下边这种会挂:

// 不好
var mod = require(someCondition ? 'a' : 'b');

// 不好
// 译注:这种应该不会挂,但是不能达到选择加载的效果,会在请求、加载完 a 和 a1 之后,
// 才执行模块代码
if (someCondition) {
    var a = require('a');
} else {
    var a = require('a1');
}

这些使用方式,在 require 回调 中支持, 即 AMD 加载器中提供的全局的

require([moduleName], function() {});

AMD 的执行模式比较向 ECMAScript 和谐版(Harmoney)的模块规范所约定的看齐。 CommonJS 模块规范在 AMD 中的不兼容部分在和谐版(Harmoney)模块也一样不兼容。 AMD 的代码执行逻辑的未来兼容性更好。

冗长与可用性

对 AMD 的批评其一,至少与 CJS 模块规范相比,是它要求一层缩进,以及一个函数封装。

但事实很简单:要用 AMD 就觉得需要多打点字并多缩进一层,其实这一点都没关系。你编程的时间是这么花的:

  • 思考问题
  • 阅读代码

绝大部分编程的时间是花在思考而非敲代码上的。虽然代码一般越短越好,但所能付出的代价终归是有限的, 何况用 AMD 要再打些的字其实也没那么多。

而且大部分 Web 开发者本来就在用匿名函数封装了,目的是避免污染全局变量。看到逻辑代码外边包了匿名函数,其实是很普遍的, 并不会给阅读模块代码带来困扰。

同时,CommonJS 格式还有些潜在代价:

  • 依赖工具的代价
  • 某些边界用例在浏览器中会挂,比如跨域访问
  • 调试更差,此代价随着时间增加,会越来越大

AMD 模块规范需要的工具更少,边界用例问题更少,调试支持也更好。

重要的是:能够真正与其他人分享代码。AMD 规范是达此目标更轻松的方式。

拥有一个可用的,易于调试,并且还在现如今的浏览器中能跑的模块系统,意味着在创造未来的 JavaScript 中最好的模块系统的同时,得到现实世界的体验。

AMD 和它相关的 API 们,已经帮助展示了任何未来 JS 的模块系统都具备的以下特性:

  • 返回一个函数作为模块值,尤其是构造函数,促使更好的 API 设计。Node 有 module.exports 来支持此特性, 但能够用 return function() {} 的话会更简洁。这意味着不需要持有 modulemodule.exports, 并且代码表达也更清晰。
  • 动态代码加载(AMD 模块系统中通过 require[],function(){}) 来实现) 是基本要求。CJS 小组谈到了这个问题,提了一些议案,但它并没有广为接受。Node 并不支持这个需求,转而依赖 require() 的同步行为, 而这在 Web 端是无从实现的。
  • 加载器插件 相当有用。它帮助避免基于回调的编程中常遇到的嵌套缩进的问题。
  • 选择映射某个模块 从另一个地址加载,使得提供模块模拟对象以供测试变得容易。
  • 每个模块最多一个 IO 行为,并且这个 IO 行为应该简单直接。Web 浏览器受不了找个模块还有多次 IO 查找。 这和 Node 里现在的多次寻址是相悖的,并且避免使用一个有 main 属性的 package.json。 用能够很容易地根据项目目录结构映射到某个地址的模块名,一个合理的不需要冗长的配置的约定就够了,不过要允许必要的简单配置。
  • 最好有个“选择加入”调用,用来使旧 JS 代码快速参与新的模块系统之中。

如果一个 JS 模块系统不能搞定以上特性,那么与 AMD 和它相关的 API 们,例如 callback-require加载器插件 和基于路径的模块 ID,相较之下,高下立见。

AMD 使用情况

截至 2011年10月,AMD 已经在 Web 上广为使用:

你可以做什么

如果你是写应用的:

如果你是脚本库开发者:

  • 如果可用,条件调用 define()。妙处在于不依靠 AMD 你仍然可以编写你的库,只要可用的时候参与一下就可以了。 这使得你的模块用户可以:
    • 避免往页面全局变量里头塞东西
    • 代码加载、延迟加载等更加有选择
    • 用现有的 AMD 工具来优化它们的项目
    • 参与到当今浏览器中可用的 JS 模块系统中去

如果你为 JavaScript 加载器、引擎、环境编写代码:

  • 实现 AMD API。有个 讨论列表兼容性测试。通过实现 AMD 模块规范,你可以减少多模块系统的无谓竞争, 帮助为一个 Web 端可用的 JavaScript 模块系统证明。这也能反馈到 ECMAScript 工作组,以求更好的本地模块系统支持。
  • 也要支持 callback-require加载器插件。 加载器插件是个很好的减轻回调、异步风格的代码中常见的嵌套回调症的方式。
comments powered by Disqus

SeaJS 模块预处理

如何正确压缩、打包你的模块们

RequireJS 其一

为何用 Web 模块的方式?

本文讨论异步模块规范(Asynchronous Module Definition,AMD)API,即 RequireJS 所支持的模块 API, 的设计驱使与使用方式。另有页面讨论 Web 模块化的基本方式

模块目的

什么是 JavaScript 模块?它们的目的是什么?

  • 定义:如何把一段代码封装成一个有用的单元,以及如何注册此模块的能力、输出的值
  • 依赖引用:如何引用其它代码单元

现今 Web

(function () {
    var $ = this.jQuery;

    this.myExample = function () {};
}());

现如今 JavaScript 代码段是如何定义的呢?

  • 通过立即执行的工厂函数定义。
  • 使用 HTML script 标签加载模块,通过全局变量来引用依赖。
  • 模块间依赖的声明很弱:开发者需要知道正确的依赖顺序。例如,包含 Backbone 的文件,不能放在 jQuery 标签之前。
  • 优化部署的时候,需要用额外的工具来把一系列 script 标签替换成一个。

这些都会使大型项目变得难以管理,尤其是当脚本们的诸多依赖还可能重叠、嵌套的时候。 手工写 script 标签可不怎么灵活,而且这么做就没法搞按需加载了。

CommonJS

var $ = require('jquery');
exports.myExample = function () {};

最初的 CommonJS 小组 的参与者们决定弄一份于时下的 JavaScript 编程语言有效,但不必束缚于浏览器 JS 环境的限制,的模块规范。开始的愿景是在浏览器里使用一些权宜之计, 并希望能借此影响浏览器厂商,促使它们为这种模块规范的原生支持提供解决方案。权宜之计有:

  • 要么使用一个服务来转译 CJS 模块成浏览器中可用的代码
  • 要么使用 XMLHttpRequest(XHR)以文本形式加载模块,再在浏览器中做文本变换、解析的工作

CJS 模块规范仅允许每文件一个模块,所以为优化、打包,可使用某种“转换格式”将多个模块合并到单个文件。

通过这种方式,CommonJS 小组搞定了依赖引用、如何处理循环依赖,以及如何获得当前模块的某些属性等问题。 但是,他们并没能接纳浏览器环境里不可改变、并且仍将影响模块设计的某些特性:

  • 网络加载
  • 异步继承

这也同时意味着他们为了实现这个规范,将负担更多地放到了 Web 开发者身上,而这些权宜之计也使调试变得更麻烦。 调试 eval 的代码,或者调试多个文件合并之后的单个文件,都有实际使用时的坏处。 这些缺点或许在未来某天会被浏览器调试工具解决掉,但结论仍然是:在最普遍的 JS 环境,浏览器中,使用 CommonJS 模块并不是最好的办法

AMD

define(['jquery'] , function ($) {
    return function () {};
});

AMD 规范的缘起是,我们需要一个比时下那种“写一堆 script 标签、手工按序指明依赖”要好,并且容易在浏览器中直接使用的模块格式。 某种不需要服务端工具配合,又能够易于调试的模块格式。它超脱于 Dojo 使用 XHR+eval 的现实经验, 并且要规避 Dojo 的方式的缺点。

它比时下 Web 开发中“全局变量+script 标签”的方式要好,因为:

  • 应用 CommonJS 实践,使用字符串 ID 来声明依赖。使得依赖声明清晰,并且避免了使用全局变量。
  • 模块 ID 可以被映射到不同的路径。从而允许切换模块实现。这对创建单元测试模型很有帮助。 在上例中,代码实际期待的不过是某个实现了 jQuery API 与行为的模块而已。并不是非要 jQuery 本身不可。
  • 封装了模块定义。使你能够避免污染全局命名空间。
  • 清楚地定义模块输出。可以用 return value;,也可以用 CommonJS 的 exports 范式。 后者在循环依赖的时候很有用。

它是 CommonJS 模块规范的改善,因为:

  • 它在浏览器里跑得更好,它的坑是最少的。其他的解决方案,都有调试、跨域、CDN 使用,file:// 协议以及依赖服务端工具等问题。
  • 定义了将多个模块包含进单个文件的方式。在 CommonJS 的条款里,有个“传输规范”来做这个事情, 但 CommonJS 小组还没就此达成一致
  • 允许将函数作为返回值。这在模块返回值是构造函数的时候尤其有用。在 CommonJS 中就有点尴尬了, 永远要通过给 exports 对象设置属性来输出。Node 支持了 module.exports = function() {}, 不过这还没在 CommonJS 规范里面。

模块定义

使用 JavaScript 的函数进行封装,已经有 文档 约定了:

(function () {
   this.myGlobal = function () {};
}());

这种模块依赖于给全局对象附加属性来输出模块值,并且用这种模型很难声明模块依赖。 预设是,模块的依赖在执行此函数之前就已经是立即可用的。这限制了加载依赖的策略。

AMD 通过如下手段解决了这些问题:

  • 调用 define() 来注册工厂函数,而不是立即执行该函数。
  • 以字符串数组的形式将依赖传递进来,而不是直接从全局对象上取。
  • 在所有依赖都被加载、执行完毕之后,才执行工厂函数。
  • 将依赖的模块作为执行工厂函数的参数。
//Calling define with a dependency array and a factory function
define(['dep1', 'dep2'], function (dep1, dep2) {

    //Define the module value by returning a value.
    return function () {};
});

命名的模块

请注意,上例中的模块并没有给自己取名。这使得这个模块可以随便移动。它允许开发者将模块放到不同的目录, 从而管它们叫不同的名字。AMD 加载器会根据它被其他脚本引用的方式来给这个模块标记 ID。

但是,为了提高性能,打包多个模块的工具需要有个给单个文件中的每个模块命名的方式。对此需求,AMD 允许以字符串作为 define() 的第一个参数:

//Calling define with module ID, dependency array, and factory function
define('myModule', ['dep1', 'dep2'], function (dep1, dep2) {

    //Define the module value by returning a value.
    return function () {};
});

你应该避免自行命名模块,并且在开发时保持每文件一个模块。只不过,在注重性能的阶段, 得有个模块规范以在编译后的资源中命名模块。

语法糖

上面的 AMD 例子在所有浏览器中都能跑。但是,有搞错模块数组与参数顺序的风险,尤其是当你的模块依赖了好多的时候:

define([ "require", "jquery", "blade/object", "blade/fn", "rdapi",
         "oauth", "blade/jig", "blade/url", "dispatch", "accounts",
         "storage", "services", "widgets/AccountPanel", "widgets/TabButton",
         "widgets/AddAccount", "less", "osTheme", "jquery-ui-1.8.7.min",
         "jquery.textOverflow"],
function (require,   $,        object,         fn,         rdapi,
          oauth,   jig,         url,         dispatch,   accounts,
          storage,   services,   AccountPanel,           TabButton,
          AddAccount,           less,   osTheme) {

});

简而为之,同时也让封装 CommonJS 模块更容易,还支持这种形式的 define() (有时这种封装还被称作“简化 CommonJS 封装”):

define(function (require) {
    var dependency1 = require('dependency1'),
        dependency2 = require('dependency2');

    return function () {};
});

AMD 加载器会使用 Function.prototype.toString() 来解析出所有的 require('') 调用, 然后在内部将上述 define 调用转换成这种形式:

define(['require', 'dependency1', 'dependency2'], function (require) {
    var dependency1 = require('dependency1'),
        dependency2 = require('dependency2');

    return function () {};
});

如此,加载器会异步加载依赖一(dependency1)与依赖二(dependency2),执行这些依赖,然后执行这个工厂函数。

并非所有的浏览器都给出可用的 Function.prototype.toString() 结果。自2011年10月, PS3 和老旧的 Opera Mobile 浏览器的返回结果就不准确。这些浏览器也更可能因网络与设备的限制, 需要编译优化模块代码,所以直接用个懂得如何解析、转换正确的依赖数组的优化工具来打包优化一下就好, 例如 RequireJS 优化工具

因为不支持 toString() 解析的浏览器相当少,用这种语法糖形式来编写你的模块是安全可靠的。 特别是当你喜欢把依赖按照它们的名字排列整齐的时候。

CommonJS 兼容性

虽然这个语法糖形式被称作“简化 CommonJS 封装”,但它并非与 CommonJS 模块 100% 兼容。不过,CommonJS 模块一般认为依赖是同步加载的,完全兼容它的话,在浏览器里就会挂掉啦。

绝大部分 CJS(CommonJS)模块,根据个人经验(不太科学地毛估估)大约有 95%,是与简化 CommonJS 封装完美兼容的。

不兼容的模块,都是那些依赖是动态计算的,调用 require() 的时候不用字符串字面量的,以及其他不像个声明方式调用 require() 的。 所以,下边这种会挂:

// 不好
var mod = require(someCondition ? 'a' : 'b');

// 不好
// 译注:这种应该不会挂,但是不能达到选择加载的效果,会在请求、加载完 a 和 a1 之后,
// 才执行模块代码
if (someCondition) {
    var a = require('a');
} else {
    var a = require('a1');
}

这些使用方式,在 require 回调 中支持, 即 AMD 加载器中提供的全局的

require([moduleName], function() {});

AMD 的执行模式比较向 ECMAScript 和谐版(Harmoney)的模块规范所约定的看齐。 CommonJS 模块规范在 AMD 中的不兼容部分在和谐版(Harmoney)模块也一样不兼容。 AMD 的代码执行逻辑的未来兼容性更好。

冗长与可用性

对 AMD 的批评其一,至少与 CJS 模块规范相比,是它要求一层缩进,以及一个函数封装。

但事实很简单:要用 AMD 就觉得需要多打点字并多缩进一层,其实这一点都没关系。你编程的时间是这么花的:

  • 思考问题
  • 阅读代码

绝大部分编程的时间是花在思考而非敲代码上的。虽然代码一般越短越好,但所能付出的代价终归是有限的, 何况用 AMD 要再打些的字其实也没那么多。

而且大部分 Web 开发者本来就在用匿名函数封装了,目的是避免污染全局变量。看到逻辑代码外边包了匿名函数,其实是很普遍的, 并不会给阅读模块代码带来困扰。

同时,CommonJS 格式还有些潜在代价:

  • 依赖工具的代价
  • 某些边界用例在浏览器中会挂,比如跨域访问
  • 调试更差,此代价随着时间增加,会越来越大

AMD 模块规范需要的工具更少,边界用例问题更少,调试支持也更好。

重要的是:能够真正与其他人分享代码。AMD 规范是达此目标更轻松的方式。

拥有一个可用的,易于调试,并且还在现如今的浏览器中能跑的模块系统,意味着在创造未来的 JavaScript 中最好的模块系统的同时,得到现实世界的体验。

AMD 和它相关的 API 们,已经帮助展示了任何未来 JS 的模块系统都具备的以下特性:

  • 返回一个函数作为模块值,尤其是构造函数,促使更好的 API 设计。Node 有 module.exports 来支持此特性, 但能够用 return function() {} 的话会更简洁。这意味着不需要持有 modulemodule.exports, 并且代码表达也更清晰。
  • 动态代码加载(AMD 模块系统中通过 require[],function(){}) 来实现) 是基本要求。CJS 小组谈到了这个问题,提了一些议案,但它并没有广为接受。Node 并不支持这个需求,转而依赖 require() 的同步行为, 而这在 Web 端是无从实现的。
  • 加载器插件 相当有用。它帮助避免基于回调的编程中常遇到的嵌套缩进的问题。
  • 选择映射某个模块 从另一个地址加载,使得提供模块模拟对象以供测试变得容易。
  • 每个模块最多一个 IO 行为,并且这个 IO 行为应该简单直接。Web 浏览器受不了找个模块还有多次 IO 查找。 这和 Node 里现在的多次寻址是相悖的,并且避免使用一个有 main 属性的 package.json。 用能够很容易地根据项目目录结构映射到某个地址的模块名,一个合理的不需要冗长的配置的约定就够了,不过要允许必要的简单配置。
  • 最好有个“选择加入”调用,用来使旧 JS 代码快速参与新的模块系统之中。

如果一个 JS 模块系统不能搞定以上特性,那么与 AMD 和它相关的 API 们,例如 callback-require加载器插件 和基于路径的模块 ID,相较之下,高下立见。

AMD 使用情况

截至 2011年10月,AMD 已经在 Web 上广为使用:

你可以做什么

如果你是写应用的:

如果你是脚本库开发者:

  • 如果可用,条件调用 define()。妙处在于不依靠 AMD 你仍然可以编写你的库,只要可用的时候参与一下就可以了。 这使得你的模块用户可以:
    • 避免往页面全局变量里头塞东西
    • 代码加载、延迟加载等更加有选择
    • 用现有的 AMD 工具来优化它们的项目
    • 参与到当今浏览器中可用的 JS 模块系统中去

如果你为 JavaScript 加载器、引擎、环境编写代码:

  • 实现 AMD API。有个 讨论列表兼容性测试。通过实现 AMD 模块规范,你可以减少多模块系统的无谓竞争, 帮助为一个 Web 端可用的 JavaScript 模块系统证明。这也能反馈到 ECMAScript 工作组,以求更好的本地模块系统支持。
  • 也要支持 callback-require加载器插件。 加载器插件是个很好的减轻回调、异步风格的代码中常见的嵌套回调症的方式。

RequireJS 其二

为何用 AMD 规范?

本文讨论异步模块规范(Asynchronous Module Definition,AMD)API,即 RequireJS 所支持的模块 API, 的设计驱使与使用方式。另有页面讨论 Web 模块化的基本方式

模块目的

什么是 JavaScript 模块?它们的目的是什么?

  • 定义:如何把一段代码封装成一个有用的单元,以及如何注册此模块的能力、输出的值
  • 依赖引用:如何引用其它代码单元

现今 Web

(function () {
    var $ = this.jQuery;

    this.myExample = function () {};
}());

现如今 JavaScript 代码段是如何定义的呢?

  • 通过立即执行的工厂函数定义。
  • 使用 HTML script 标签加载模块,通过全局变量来引用依赖。
  • 模块间依赖的声明很弱:开发者需要知道正确的依赖顺序。例如,包含 Backbone 的文件,不能放在 jQuery 标签之前。
  • 优化部署的时候,需要用额外的工具来把一系列 script 标签替换成一个。

这些都会使大型项目变得难以管理,尤其是当脚本们的诸多依赖还可能重叠、嵌套的时候。 手工写 script 标签可不怎么灵活,而且这么做就没法搞按需加载了。

CommonJS

var $ = require('jquery');
exports.myExample = function () {};

最初的 CommonJS 小组 的参与者们决定弄一份于时下的 JavaScript 编程语言有效,但不必束缚于浏览器 JS 环境的限制,的模块规范。开始的愿景是在浏览器里使用一些权宜之计, 并希望能借此影响浏览器厂商,促使它们为这种模块规范的原生支持提供解决方案。权宜之计有:

  • 要么使用一个服务来转译 CJS 模块成浏览器中可用的代码
  • 要么使用 XMLHttpRequest(XHR)以文本形式加载模块,再在浏览器中做文本变换、解析的工作

CJS 模块规范仅允许每文件一个模块,所以为优化、打包,可使用某种“转换格式”将多个模块合并到单个文件。

通过这种方式,CommonJS 小组搞定了依赖引用、如何处理循环依赖,以及如何获得当前模块的某些属性等问题。 但是,他们并没能接纳浏览器环境里不可改变、并且仍将影响模块设计的某些特性:

  • 网络加载
  • 异步继承

这也同时意味着他们为了实现这个规范,将负担更多地放到了 Web 开发者身上,而这些权宜之计也使调试变得更麻烦。 调试 eval 的代码,或者调试多个文件合并之后的单个文件,都有实际使用时的坏处。 这些缺点或许在未来某天会被浏览器调试工具解决掉,但结论仍然是:在最普遍的 JS 环境,浏览器中,使用 CommonJS 模块并不是最好的办法

AMD

define(['jquery'] , function ($) {
    return function () {};
});

AMD 规范的缘起是,我们需要一个比时下那种“写一堆 script 标签、手工按序指明依赖”要好,并且容易在浏览器中直接使用的模块格式。 某种不需要服务端工具配合,又能够易于调试的模块格式。它超脱于 Dojo 使用 XHR+eval 的现实经验, 并且要规避 Dojo 的方式的缺点。

它比时下 Web 开发中“全局变量+script 标签”的方式要好,因为:

  • 应用 CommonJS 实践,使用字符串 ID 来声明依赖。使得依赖声明清晰,并且避免了使用全局变量。
  • 模块 ID 可以被映射到不同的路径。从而允许切换模块实现。这对创建单元测试模型很有帮助。 在上例中,代码实际期待的不过是某个实现了 jQuery API 与行为的模块而已。并不是非要 jQuery 本身不可。
  • 封装了模块定义。使你能够避免污染全局命名空间。
  • 清楚地定义模块输出。可以用 return value;,也可以用 CommonJS 的 exports 范式。 后者在循环依赖的时候很有用。

它是 CommonJS 模块规范的改善,因为:

  • 它在浏览器里跑得更好,它的坑是最少的。其他的解决方案,都有调试、跨域、CDN 使用,file:// 协议以及依赖服务端工具等问题。
  • 定义了将多个模块包含进单个文件的方式。在 CommonJS 的条款里,有个“传输规范”来做这个事情, 但 CommonJS 小组还没就此达成一致
  • 允许将函数作为返回值。这在模块返回值是构造函数的时候尤其有用。在 CommonJS 中就有点尴尬了, 永远要通过给 exports 对象设置属性来输出。Node 支持了 module.exports = function() {}, 不过这还没在 CommonJS 规范里面。

模块定义

使用 JavaScript 的函数进行封装,已经有 文档 约定了:

(function () {
   this.myGlobal = function () {};
}());

这种模块依赖于给全局对象附加属性来输出模块值,并且用这种模型很难声明模块依赖。 预设是,模块的依赖在执行此函数之前就已经是立即可用的。这限制了加载依赖的策略。

AMD 通过如下手段解决了这些问题:

  • 调用 define() 来注册工厂函数,而不是立即执行该函数。
  • 以字符串数组的形式将依赖传递进来,而不是直接从全局对象上取。
  • 在所有依赖都被加载、执行完毕之后,才执行工厂函数。
  • 将依赖的模块作为执行工厂函数的参数。
//Calling define with a dependency array and a factory function
define(['dep1', 'dep2'], function (dep1, dep2) {

    //Define the module value by returning a value.
    return function () {};
});

命名的模块

请注意,上例中的模块并没有给自己取名。这使得这个模块可以随便移动。它允许开发者将模块放到不同的目录, 从而管它们叫不同的名字。AMD 加载器会根据它被其他脚本引用的方式来给这个模块标记 ID。

但是,为了提高性能,打包多个模块的工具需要有个给单个文件中的每个模块命名的方式。对此需求,AMD 允许以字符串作为 define() 的第一个参数:

//Calling define with module ID, dependency array, and factory function
define('myModule', ['dep1', 'dep2'], function (dep1, dep2) {

    //Define the module value by returning a value.
    return function () {};
});

你应该避免自行命名模块,并且在开发时保持每文件一个模块。只不过,在注重性能的阶段, 得有个模块规范以在编译后的资源中命名模块。

语法糖

上面的 AMD 例子在所有浏览器中都能跑。但是,有搞错模块数组与参数顺序的风险,尤其是当你的模块依赖了好多的时候:

define([ "require", "jquery", "blade/object", "blade/fn", "rdapi",
         "oauth", "blade/jig", "blade/url", "dispatch", "accounts",
         "storage", "services", "widgets/AccountPanel", "widgets/TabButton",
         "widgets/AddAccount", "less", "osTheme", "jquery-ui-1.8.7.min",
         "jquery.textOverflow"],
function (require,   $,        object,         fn,         rdapi,
          oauth,   jig,         url,         dispatch,   accounts,
          storage,   services,   AccountPanel,           TabButton,
          AddAccount,           less,   osTheme) {

});

简而为之,同时也让封装 CommonJS 模块更容易,还支持这种形式的 define() (有时这种封装还被称作“简化 CommonJS 封装”):

define(function (require) {
    var dependency1 = require('dependency1'),
        dependency2 = require('dependency2');

    return function () {};
});

AMD 加载器会使用 Function.prototype.toString() 来解析出所有的 require('') 调用, 然后在内部将上述 define 调用转换成这种形式:

define(['require', 'dependency1', 'dependency2'], function (require) {
    var dependency1 = require('dependency1'),
        dependency2 = require('dependency2');

    return function () {};
});

如此,加载器会异步加载依赖一(dependency1)与依赖二(dependency2),执行这些依赖,然后执行这个工厂函数。

并非所有的浏览器都给出可用的 Function.prototype.toString() 结果。自2011年10月, PS3 和老旧的 Opera Mobile 浏览器的返回结果就不准确。这些浏览器也更可能因网络与设备的限制, 需要编译优化模块代码,所以直接用个懂得如何解析、转换正确的依赖数组的优化工具来打包优化一下就好, 例如 RequireJS 优化工具

因为不支持 toString() 解析的浏览器相当少,用这种语法糖形式来编写你的模块是安全可靠的。 特别是当你喜欢把依赖按照它们的名字排列整齐的时候。

CommonJS 兼容性

虽然这个语法糖形式被称作“简化 CommonJS 封装”,但它并非与 CommonJS 模块 100% 兼容。不过,CommonJS 模块一般认为依赖是同步加载的,完全兼容它的话,在浏览器里就会挂掉啦。

绝大部分 CJS(CommonJS)模块,根据个人经验(不太科学地毛估估)大约有 95%,是与简化 CommonJS 封装完美兼容的。

不兼容的模块,都是那些依赖是动态计算的,调用 require() 的时候不用字符串字面量的,以及其他不像个声明方式调用 require() 的。 所以,下边这种会挂:

// 不好
var mod = require(someCondition ? 'a' : 'b');

// 不好
// 译注:这种应该不会挂,但是不能达到选择加载的效果,会在请求、加载完 a 和 a1 之后,
// 才执行模块代码
if (someCondition) {
    var a = require('a');
} else {
    var a = require('a1');
}

这些使用方式,在 require 回调 中支持, 即 AMD 加载器中提供的全局的

require([moduleName], function() {});

AMD 的执行模式比较向 ECMAScript 和谐版(Harmoney)的模块规范所约定的看齐。 CommonJS 模块规范在 AMD 中的不兼容部分在和谐版(Harmoney)模块也一样不兼容。 AMD 的代码执行逻辑的未来兼容性更好。

冗长与可用性

对 AMD 的批评其一,至少与 CJS 模块规范相比,是它要求一层缩进,以及一个函数封装。

但事实很简单:要用 AMD 就觉得需要多打点字并多缩进一层,其实这一点都没关系。你编程的时间是这么花的:

  • 思考问题
  • 阅读代码

绝大部分编程的时间是花在思考而非敲代码上的。虽然代码一般越短越好,但所能付出的代价终归是有限的, 何况用 AMD 要再打些的字其实也没那么多。

而且大部分 Web 开发者本来就在用匿名函数封装了,目的是避免污染全局变量。看到逻辑代码外边包了匿名函数,其实是很普遍的, 并不会给阅读模块代码带来困扰。

同时,CommonJS 格式还有些潜在代价:

  • 依赖工具的代价
  • 某些边界用例在浏览器中会挂,比如跨域访问
  • 调试更差,此代价随着时间增加,会越来越大

AMD 模块规范需要的工具更少,边界用例问题更少,调试支持也更好。

重要的是:能够真正与其他人分享代码。AMD 规范是达此目标更轻松的方式。

拥有一个可用的,易于调试,并且还在现如今的浏览器中能跑的模块系统,意味着在创造未来的 JavaScript 中最好的模块系统的同时,得到现实世界的体验。

AMD 和它相关的 API 们,已经帮助展示了任何未来 JS 的模块系统都具备的以下特性:

  • 返回一个函数作为模块值,尤其是构造函数,促使更好的 API 设计。Node 有 module.exports 来支持此特性, 但能够用 return function() {} 的话会更简洁。这意味着不需要持有 modulemodule.exports, 并且代码表达也更清晰。
  • 动态代码加载(AMD 模块系统中通过 require[],function(){}) 来实现) 是基本要求。CJS 小组谈到了这个问题,提了一些议案,但它并没有广为接受。Node 并不支持这个需求,转而依赖 require() 的同步行为, 而这在 Web 端是无从实现的。
  • 加载器插件 相当有用。它帮助避免基于回调的编程中常遇到的嵌套缩进的问题。
  • 选择映射某个模块 从另一个地址加载,使得提供模块模拟对象以供测试变得容易。
  • 每个模块最多一个 IO 行为,并且这个 IO 行为应该简单直接。Web 浏览器受不了找个模块还有多次 IO 查找。 这和 Node 里现在的多次寻址是相悖的,并且避免使用一个有 main 属性的 package.json。 用能够很容易地根据项目目录结构映射到某个地址的模块名,一个合理的不需要冗长的配置的约定就够了,不过要允许必要的简单配置。
  • 最好有个“选择加入”调用,用来使旧 JS 代码快速参与新的模块系统之中。

如果一个 JS 模块系统不能搞定以上特性,那么与 AMD 和它相关的 API 们,例如 callback-require加载器插件 和基于路径的模块 ID,相较之下,高下立见。

AMD 使用情况

截至 2011年10月,AMD 已经在 Web 上广为使用:

你可以做什么

如果你是写应用的:

如果你是脚本库开发者:

  • 如果可用,条件调用 define()。妙处在于不依靠 AMD 你仍然可以编写你的库,只要可用的时候参与一下就可以了。 这使得你的模块用户可以:
    • 避免往页面全局变量里头塞东西
    • 代码加载、延迟加载等更加有选择
    • 用现有的 AMD 工具来优化它们的项目
    • 参与到当今浏览器中可用的 JS 模块系统中去

如果你为 JavaScript 加载器、引擎、环境编写代码:

  • 实现 AMD API。有个 讨论列表兼容性测试。通过实现 AMD 模块规范,你可以减少多模块系统的无谓竞争, 帮助为一个 Web 端可用的 JavaScript 模块系统证明。这也能反馈到 ECMAScript 工作组,以求更好的本地模块系统支持。
  • 也要支持 callback-require加载器插件。 加载器插件是个很好的减轻回调、异步风格的代码中常见的嵌套回调症的方式。
comments powered by Disqus
comments powered by Disqus

SeaJS 之异步

为什么它是这么实现的

SeaJS 文摘

为何会有文摘?

RequireJS 其一

为何用 Web 模块的方式?

本文讨论异步模块规范(Asynchronous Module Definition,AMD)API,即 RequireJS 所支持的模块 API, 的设计驱使与使用方式。另有页面讨论 Web 模块化的基本方式

模块目的

什么是 JavaScript 模块?它们的目的是什么?

  • 定义:如何把一段代码封装成一个有用的单元,以及如何注册此模块的能力、输出的值
  • 依赖引用:如何引用其它代码单元

现今 Web

(function () {
    var $ = this.jQuery;

    this.myExample = function () {};
}());

现如今 JavaScript 代码段是如何定义的呢?

  • 通过立即执行的工厂函数定义。
  • 使用 HTML script 标签加载模块,通过全局变量来引用依赖。
  • 模块间依赖的声明很弱:开发者需要知道正确的依赖顺序。例如,包含 Backbone 的文件,不能放在 jQuery 标签之前。
  • 优化部署的时候,需要用额外的工具来把一系列 script 标签替换成一个。

这些都会使大型项目变得难以管理,尤其是当脚本们的诸多依赖还可能重叠、嵌套的时候。 手工写 script 标签可不怎么灵活,而且这么做就没法搞按需加载了。

CommonJS

var $ = require('jquery');
exports.myExample = function () {};

最初的 CommonJS 小组 的参与者们决定弄一份于时下的 JavaScript 编程语言有效,但不必束缚于浏览器 JS 环境的限制,的模块规范。开始的愿景是在浏览器里使用一些权宜之计, 并希望能借此影响浏览器厂商,促使它们为这种模块规范的原生支持提供解决方案。权宜之计有:

  • 要么使用一个服务来转译 CJS 模块成浏览器中可用的代码
  • 要么使用 XMLHttpRequest(XHR)以文本形式加载模块,再在浏览器中做文本变换、解析的工作

CJS 模块规范仅允许每文件一个模块,所以为优化、打包,可使用某种“转换格式”将多个模块合并到单个文件。

通过这种方式,CommonJS 小组搞定了依赖引用、如何处理循环依赖,以及如何获得当前模块的某些属性等问题。 但是,他们并没能接纳浏览器环境里不可改变、并且仍将影响模块设计的某些特性:

  • 网络加载
  • 异步继承

这也同时意味着他们为了实现这个规范,将负担更多地放到了 Web 开发者身上,而这些权宜之计也使调试变得更麻烦。 调试 eval 的代码,或者调试多个文件合并之后的单个文件,都有实际使用时的坏处。 这些缺点或许在未来某天会被浏览器调试工具解决掉,但结论仍然是:在最普遍的 JS 环境,浏览器中,使用 CommonJS 模块并不是最好的办法

AMD

define(['jquery'] , function ($) {
    return function () {};
});

AMD 规范的缘起是,我们需要一个比时下那种“写一堆 script 标签、手工按序指明依赖”要好,并且容易在浏览器中直接使用的模块格式。 某种不需要服务端工具配合,又能够易于调试的模块格式。它超脱于 Dojo 使用 XHR+eval 的现实经验, 并且要规避 Dojo 的方式的缺点。

它比时下 Web 开发中“全局变量+script 标签”的方式要好,因为:

  • 应用 CommonJS 实践,使用字符串 ID 来声明依赖。使得依赖声明清晰,并且避免了使用全局变量。
  • 模块 ID 可以被映射到不同的路径。从而允许切换模块实现。这对创建单元测试模型很有帮助。 在上例中,代码实际期待的不过是某个实现了 jQuery API 与行为的模块而已。并不是非要 jQuery 本身不可。
  • 封装了模块定义。使你能够避免污染全局命名空间。
  • 清楚地定义模块输出。可以用 return value;,也可以用 CommonJS 的 exports 范式。 后者在循环依赖的时候很有用。

它是 CommonJS 模块规范的改善,因为:

  • 它在浏览器里跑得更好,它的坑是最少的。其他的解决方案,都有调试、跨域、CDN 使用,file:// 协议以及依赖服务端工具等问题。
  • 定义了将多个模块包含进单个文件的方式。在 CommonJS 的条款里,有个“传输规范”来做这个事情, 但 CommonJS 小组还没就此达成一致
  • 允许将函数作为返回值。这在模块返回值是构造函数的时候尤其有用。在 CommonJS 中就有点尴尬了, 永远要通过给 exports 对象设置属性来输出。Node 支持了 module.exports = function() {}, 不过这还没在 CommonJS 规范里面。

模块定义

使用 JavaScript 的函数进行封装,已经有 文档 约定了:

(function () {
   this.myGlobal = function () {};
}());

这种模块依赖于给全局对象附加属性来输出模块值,并且用这种模型很难声明模块依赖。 预设是,模块的依赖在执行此函数之前就已经是立即可用的。这限制了加载依赖的策略。

AMD 通过如下手段解决了这些问题:

  • 调用 define() 来注册工厂函数,而不是立即执行该函数。
  • 以字符串数组的形式将依赖传递进来,而不是直接从全局对象上取。
  • 在所有依赖都被加载、执行完毕之后,才执行工厂函数。
  • 将依赖的模块作为执行工厂函数的参数。
//Calling define with a dependency array and a factory function
define(['dep1', 'dep2'], function (dep1, dep2) {

    //Define the module value by returning a value.
    return function () {};
});

命名的模块

请注意,上例中的模块并没有给自己取名。这使得这个模块可以随便移动。它允许开发者将模块放到不同的目录, 从而管它们叫不同的名字。AMD 加载器会根据它被其他脚本引用的方式来给这个模块标记 ID。

但是,为了提高性能,打包多个模块的工具需要有个给单个文件中的每个模块命名的方式。对此需求,AMD 允许以字符串作为 define() 的第一个参数:

//Calling define with module ID, dependency array, and factory function
define('myModule', ['dep1', 'dep2'], function (dep1, dep2) {

    //Define the module value by returning a value.
    return function () {};
});

你应该避免自行命名模块,并且在开发时保持每文件一个模块。只不过,在注重性能的阶段, 得有个模块规范以在编译后的资源中命名模块。

语法糖

上面的 AMD 例子在所有浏览器中都能跑。但是,有搞错模块数组与参数顺序的风险,尤其是当你的模块依赖了好多的时候:

define([ "require", "jquery", "blade/object", "blade/fn", "rdapi",
         "oauth", "blade/jig", "blade/url", "dispatch", "accounts",
         "storage", "services", "widgets/AccountPanel", "widgets/TabButton",
         "widgets/AddAccount", "less", "osTheme", "jquery-ui-1.8.7.min",
         "jquery.textOverflow"],
function (require,   $,        object,         fn,         rdapi,
          oauth,   jig,         url,         dispatch,   accounts,
          storage,   services,   AccountPanel,           TabButton,
          AddAccount,           less,   osTheme) {

});

简而为之,同时也让封装 CommonJS 模块更容易,还支持这种形式的 define() (有时这种封装还被称作“简化 CommonJS 封装”):

define(function (require) {
    var dependency1 = require('dependency1'),
        dependency2 = require('dependency2');

    return function () {};
});

AMD 加载器会使用 Function.prototype.toString() 来解析出所有的 require('') 调用, 然后在内部将上述 define 调用转换成这种形式:

define(['require', 'dependency1', 'dependency2'], function (require) {
    var dependency1 = require('dependency1'),
        dependency2 = require('dependency2');

    return function () {};
});

如此,加载器会异步加载依赖一(dependency1)与依赖二(dependency2),执行这些依赖,然后执行这个工厂函数。

并非所有的浏览器都给出可用的 Function.prototype.toString() 结果。自2011年10月, PS3 和老旧的 Opera Mobile 浏览器的返回结果就不准确。这些浏览器也更可能因网络与设备的限制, 需要编译优化模块代码,所以直接用个懂得如何解析、转换正确的依赖数组的优化工具来打包优化一下就好, 例如 RequireJS 优化工具

因为不支持 toString() 解析的浏览器相当少,用这种语法糖形式来编写你的模块是安全可靠的。 特别是当你喜欢把依赖按照它们的名字排列整齐的时候。

CommonJS 兼容性

虽然这个语法糖形式被称作“简化 CommonJS 封装”,但它并非与 CommonJS 模块 100% 兼容。不过,CommonJS 模块一般认为依赖是同步加载的,完全兼容它的话,在浏览器里就会挂掉啦。

绝大部分 CJS(CommonJS)模块,根据个人经验(不太科学地毛估估)大约有 95%,是与简化 CommonJS 封装完美兼容的。

不兼容的模块,都是那些依赖是动态计算的,调用 require() 的时候不用字符串字面量的,以及其他不像个声明方式调用 require() 的。 所以,下边这种会挂:

// 不好
var mod = require(someCondition ? 'a' : 'b');

// 不好
// 译注:这种应该不会挂,但是不能达到选择加载的效果,会在请求、加载完 a 和 a1 之后,
// 才执行模块代码
if (someCondition) {
    var a = require('a');
} else {
    var a = require('a1');
}

这些使用方式,在 require 回调 中支持, 即 AMD 加载器中提供的全局的

require([moduleName], function() {});

AMD 的执行模式比较向 ECMAScript 和谐版(Harmoney)的模块规范所约定的看齐。 CommonJS 模块规范在 AMD 中的不兼容部分在和谐版(Harmoney)模块也一样不兼容。 AMD 的代码执行逻辑的未来兼容性更好。

冗长与可用性

对 AMD 的批评其一,至少与 CJS 模块规范相比,是它要求一层缩进,以及一个函数封装。

但事实很简单:要用 AMD 就觉得需要多打点字并多缩进一层,其实这一点都没关系。你编程的时间是这么花的:

  • 思考问题
  • 阅读代码

绝大部分编程的时间是花在思考而非敲代码上的。虽然代码一般越短越好,但所能付出的代价终归是有限的, 何况用 AMD 要再打些的字其实也没那么多。

而且大部分 Web 开发者本来就在用匿名函数封装了,目的是避免污染全局变量。看到逻辑代码外边包了匿名函数,其实是很普遍的, 并不会给阅读模块代码带来困扰。

同时,CommonJS 格式还有些潜在代价:

  • 依赖工具的代价
  • 某些边界用例在浏览器中会挂,比如跨域访问
  • 调试更差,此代价随着时间增加,会越来越大

AMD 模块规范需要的工具更少,边界用例问题更少,调试支持也更好。

重要的是:能够真正与其他人分享代码。AMD 规范是达此目标更轻松的方式。

拥有一个可用的,易于调试,并且还在现如今的浏览器中能跑的模块系统,意味着在创造未来的 JavaScript 中最好的模块系统的同时,得到现实世界的体验。

AMD 和它相关的 API 们,已经帮助展示了任何未来 JS 的模块系统都具备的以下特性:

  • 返回一个函数作为模块值,尤其是构造函数,促使更好的 API 设计。Node 有 module.exports 来支持此特性, 但能够用 return function() {} 的话会更简洁。这意味着不需要持有 modulemodule.exports, 并且代码表达也更清晰。
  • 动态代码加载(AMD 模块系统中通过 require[],function(){}) 来实现) 是基本要求。CJS 小组谈到了这个问题,提了一些议案,但它并没有广为接受。Node 并不支持这个需求,转而依赖 require() 的同步行为, 而这在 Web 端是无从实现的。
  • 加载器插件 相当有用。它帮助避免基于回调的编程中常遇到的嵌套缩进的问题。
  • 选择映射某个模块 从另一个地址加载,使得提供模块模拟对象以供测试变得容易。
  • 每个模块最多一个 IO 行为,并且这个 IO 行为应该简单直接。Web 浏览器受不了找个模块还有多次 IO 查找。 这和 Node 里现在的多次寻址是相悖的,并且避免使用一个有 main 属性的 package.json。 用能够很容易地根据项目目录结构映射到某个地址的模块名,一个合理的不需要冗长的配置的约定就够了,不过要允许必要的简单配置。
  • 最好有个“选择加入”调用,用来使旧 JS 代码快速参与新的模块系统之中。

如果一个 JS 模块系统不能搞定以上特性,那么与 AMD 和它相关的 API 们,例如 callback-require加载器插件 和基于路径的模块 ID,相较之下,高下立见。

AMD 使用情况

截至 2011年10月,AMD 已经在 Web 上广为使用:

你可以做什么

如果你是写应用的:

如果你是脚本库开发者:

  • 如果可用,条件调用 define()。妙处在于不依靠 AMD 你仍然可以编写你的库,只要可用的时候参与一下就可以了。 这使得你的模块用户可以:
    • 避免往页面全局变量里头塞东西
    • 代码加载、延迟加载等更加有选择
    • 用现有的 AMD 工具来优化它们的项目
    • 参与到当今浏览器中可用的 JS 模块系统中去

如果你为 JavaScript 加载器、引擎、环境编写代码:

  • 实现 AMD API。有个 讨论列表兼容性测试。通过实现 AMD 模块规范,你可以减少多模块系统的无谓竞争, 帮助为一个 Web 端可用的 JavaScript 模块系统证明。这也能反馈到 ECMAScript 工作组,以求更好的本地模块系统支持。
  • 也要支持 callback-require加载器插件。 加载器插件是个很好的减轻回调、异步风格的代码中常见的嵌套回调症的方式。

RequireJS 其二

为何用 AMD 规范?

本文讨论异步模块规范(Asynchronous Module Definition,AMD)API,即 RequireJS 所支持的模块 API, 的设计驱使与使用方式。另有页面讨论 Web 模块化的基本方式

模块目的

什么是 JavaScript 模块?它们的目的是什么?

  • 定义:如何把一段代码封装成一个有用的单元,以及如何注册此模块的能力、输出的值
  • 依赖引用:如何引用其它代码单元

现今 Web

(function () {
    var $ = this.jQuery;

    this.myExample = function () {};
}());

现如今 JavaScript 代码段是如何定义的呢?

  • 通过立即执行的工厂函数定义。
  • 使用 HTML script 标签加载模块,通过全局变量来引用依赖。
  • 模块间依赖的声明很弱:开发者需要知道正确的依赖顺序。例如,包含 Backbone 的文件,不能放在 jQuery 标签之前。
  • 优化部署的时候,需要用额外的工具来把一系列 script 标签替换成一个。

这些都会使大型项目变得难以管理,尤其是当脚本们的诸多依赖还可能重叠、嵌套的时候。 手工写 script 标签可不怎么灵活,而且这么做就没法搞按需加载了。

CommonJS

var $ = require('jquery');
exports.myExample = function () {};

最初的 CommonJS 小组 的参与者们决定弄一份于时下的 JavaScript 编程语言有效,但不必束缚于浏览器 JS 环境的限制,的模块规范。开始的愿景是在浏览器里使用一些权宜之计, 并希望能借此影响浏览器厂商,促使它们为这种模块规范的原生支持提供解决方案。权宜之计有:

  • 要么使用一个服务来转译 CJS 模块成浏览器中可用的代码
  • 要么使用 XMLHttpRequest(XHR)以文本形式加载模块,再在浏览器中做文本变换、解析的工作

CJS 模块规范仅允许每文件一个模块,所以为优化、打包,可使用某种“转换格式”将多个模块合并到单个文件。

通过这种方式,CommonJS 小组搞定了依赖引用、如何处理循环依赖,以及如何获得当前模块的某些属性等问题。 但是,他们并没能接纳浏览器环境里不可改变、并且仍将影响模块设计的某些特性:

  • 网络加载
  • 异步继承

这也同时意味着他们为了实现这个规范,将负担更多地放到了 Web 开发者身上,而这些权宜之计也使调试变得更麻烦。 调试 eval 的代码,或者调试多个文件合并之后的单个文件,都有实际使用时的坏处。 这些缺点或许在未来某天会被浏览器调试工具解决掉,但结论仍然是:在最普遍的 JS 环境,浏览器中,使用 CommonJS 模块并不是最好的办法

AMD

define(['jquery'] , function ($) {
    return function () {};
});

AMD 规范的缘起是,我们需要一个比时下那种“写一堆 script 标签、手工按序指明依赖”要好,并且容易在浏览器中直接使用的模块格式。 某种不需要服务端工具配合,又能够易于调试的模块格式。它超脱于 Dojo 使用 XHR+eval 的现实经验, 并且要规避 Dojo 的方式的缺点。

它比时下 Web 开发中“全局变量+script 标签”的方式要好,因为:

  • 应用 CommonJS 实践,使用字符串 ID 来声明依赖。使得依赖声明清晰,并且避免了使用全局变量。
  • 模块 ID 可以被映射到不同的路径。从而允许切换模块实现。这对创建单元测试模型很有帮助。 在上例中,代码实际期待的不过是某个实现了 jQuery API 与行为的模块而已。并不是非要 jQuery 本身不可。
  • 封装了模块定义。使你能够避免污染全局命名空间。
  • 清楚地定义模块输出。可以用 return value;,也可以用 CommonJS 的 exports 范式。 后者在循环依赖的时候很有用。

它是 CommonJS 模块规范的改善,因为:

  • 它在浏览器里跑得更好,它的坑是最少的。其他的解决方案,都有调试、跨域、CDN 使用,file:// 协议以及依赖服务端工具等问题。
  • 定义了将多个模块包含进单个文件的方式。在 CommonJS 的条款里,有个“传输规范”来做这个事情, 但 CommonJS 小组还没就此达成一致
  • 允许将函数作为返回值。这在模块返回值是构造函数的时候尤其有用。在 CommonJS 中就有点尴尬了, 永远要通过给 exports 对象设置属性来输出。Node 支持了 module.exports = function() {}, 不过这还没在 CommonJS 规范里面。

模块定义

使用 JavaScript 的函数进行封装,已经有 文档 约定了:

(function () {
   this.myGlobal = function () {};
}());

这种模块依赖于给全局对象附加属性来输出模块值,并且用这种模型很难声明模块依赖。 预设是,模块的依赖在执行此函数之前就已经是立即可用的。这限制了加载依赖的策略。

AMD 通过如下手段解决了这些问题:

  • 调用 define() 来注册工厂函数,而不是立即执行该函数。
  • 以字符串数组的形式将依赖传递进来,而不是直接从全局对象上取。
  • 在所有依赖都被加载、执行完毕之后,才执行工厂函数。
  • 将依赖的模块作为执行工厂函数的参数。
//Calling define with a dependency array and a factory function
define(['dep1', 'dep2'], function (dep1, dep2) {

    //Define the module value by returning a value.
    return function () {};
});

命名的模块

请注意,上例中的模块并没有给自己取名。这使得这个模块可以随便移动。它允许开发者将模块放到不同的目录, 从而管它们叫不同的名字。AMD 加载器会根据它被其他脚本引用的方式来给这个模块标记 ID。

但是,为了提高性能,打包多个模块的工具需要有个给单个文件中的每个模块命名的方式。对此需求,AMD 允许以字符串作为 define() 的第一个参数:

//Calling define with module ID, dependency array, and factory function
define('myModule', ['dep1', 'dep2'], function (dep1, dep2) {

    //Define the module value by returning a value.
    return function () {};
});

你应该避免自行命名模块,并且在开发时保持每文件一个模块。只不过,在注重性能的阶段, 得有个模块规范以在编译后的资源中命名模块。

语法糖

上面的 AMD 例子在所有浏览器中都能跑。但是,有搞错模块数组与参数顺序的风险,尤其是当你的模块依赖了好多的时候:

define([ "require", "jquery", "blade/object", "blade/fn", "rdapi",
         "oauth", "blade/jig", "blade/url", "dispatch", "accounts",
         "storage", "services", "widgets/AccountPanel", "widgets/TabButton",
         "widgets/AddAccount", "less", "osTheme", "jquery-ui-1.8.7.min",
         "jquery.textOverflow"],
function (require,   $,        object,         fn,         rdapi,
          oauth,   jig,         url,         dispatch,   accounts,
          storage,   services,   AccountPanel,           TabButton,
          AddAccount,           less,   osTheme) {

});

简而为之,同时也让封装 CommonJS 模块更容易,还支持这种形式的 define() (有时这种封装还被称作“简化 CommonJS 封装”):

define(function (require) {
    var dependency1 = require('dependency1'),
        dependency2 = require('dependency2');

    return function () {};
});

AMD 加载器会使用 Function.prototype.toString() 来解析出所有的 require('') 调用, 然后在内部将上述 define 调用转换成这种形式:

define(['require', 'dependency1', 'dependency2'], function (require) {
    var dependency1 = require('dependency1'),
        dependency2 = require('dependency2');

    return function () {};
});

如此,加载器会异步加载依赖一(dependency1)与依赖二(dependency2),执行这些依赖,然后执行这个工厂函数。

并非所有的浏览器都给出可用的 Function.prototype.toString() 结果。自2011年10月, PS3 和老旧的 Opera Mobile 浏览器的返回结果就不准确。这些浏览器也更可能因网络与设备的限制, 需要编译优化模块代码,所以直接用个懂得如何解析、转换正确的依赖数组的优化工具来打包优化一下就好, 例如 RequireJS 优化工具

因为不支持 toString() 解析的浏览器相当少,用这种语法糖形式来编写你的模块是安全可靠的。 特别是当你喜欢把依赖按照它们的名字排列整齐的时候。

CommonJS 兼容性

虽然这个语法糖形式被称作“简化 CommonJS 封装”,但它并非与 CommonJS 模块 100% 兼容。不过,CommonJS 模块一般认为依赖是同步加载的,完全兼容它的话,在浏览器里就会挂掉啦。

绝大部分 CJS(CommonJS)模块,根据个人经验(不太科学地毛估估)大约有 95%,是与简化 CommonJS 封装完美兼容的。

不兼容的模块,都是那些依赖是动态计算的,调用 require() 的时候不用字符串字面量的,以及其他不像个声明方式调用 require() 的。 所以,下边这种会挂:

// 不好
var mod = require(someCondition ? 'a' : 'b');

// 不好
// 译注:这种应该不会挂,但是不能达到选择加载的效果,会在请求、加载完 a 和 a1 之后,
// 才执行模块代码
if (someCondition) {
    var a = require('a');
} else {
    var a = require('a1');
}

这些使用方式,在 require 回调 中支持, 即 AMD 加载器中提供的全局的

require([moduleName], function() {});

AMD 的执行模式比较向 ECMAScript 和谐版(Harmoney)的模块规范所约定的看齐。 CommonJS 模块规范在 AMD 中的不兼容部分在和谐版(Harmoney)模块也一样不兼容。 AMD 的代码执行逻辑的未来兼容性更好。

冗长与可用性

对 AMD 的批评其一,至少与 CJS 模块规范相比,是它要求一层缩进,以及一个函数封装。

但事实很简单:要用 AMD 就觉得需要多打点字并多缩进一层,其实这一点都没关系。你编程的时间是这么花的:

  • 思考问题
  • 阅读代码

绝大部分编程的时间是花在思考而非敲代码上的。虽然代码一般越短越好,但所能付出的代价终归是有限的, 何况用 AMD 要再打些的字其实也没那么多。

而且大部分 Web 开发者本来就在用匿名函数封装了,目的是避免污染全局变量。看到逻辑代码外边包了匿名函数,其实是很普遍的, 并不会给阅读模块代码带来困扰。

同时,CommonJS 格式还有些潜在代价:

  • 依赖工具的代价
  • 某些边界用例在浏览器中会挂,比如跨域访问
  • 调试更差,此代价随着时间增加,会越来越大

AMD 模块规范需要的工具更少,边界用例问题更少,调试支持也更好。

重要的是:能够真正与其他人分享代码。AMD 规范是达此目标更轻松的方式。

拥有一个可用的,易于调试,并且还在现如今的浏览器中能跑的模块系统,意味着在创造未来的 JavaScript 中最好的模块系统的同时,得到现实世界的体验。

AMD 和它相关的 API 们,已经帮助展示了任何未来 JS 的模块系统都具备的以下特性:

  • 返回一个函数作为模块值,尤其是构造函数,促使更好的 API 设计。Node 有 module.exports 来支持此特性, 但能够用 return function() {} 的话会更简洁。这意味着不需要持有 modulemodule.exports, 并且代码表达也更清晰。
  • 动态代码加载(AMD 模块系统中通过 require[],function(){}) 来实现) 是基本要求。CJS 小组谈到了这个问题,提了一些议案,但它并没有广为接受。Node 并不支持这个需求,转而依赖 require() 的同步行为, 而这在 Web 端是无从实现的。
  • 加载器插件 相当有用。它帮助避免基于回调的编程中常遇到的嵌套缩进的问题。
  • 选择映射某个模块 从另一个地址加载,使得提供模块模拟对象以供测试变得容易。
  • 每个模块最多一个 IO 行为,并且这个 IO 行为应该简单直接。Web 浏览器受不了找个模块还有多次 IO 查找。 这和 Node 里现在的多次寻址是相悖的,并且避免使用一个有 main 属性的 package.json。 用能够很容易地根据项目目录结构映射到某个地址的模块名,一个合理的不需要冗长的配置的约定就够了,不过要允许必要的简单配置。
  • 最好有个“选择加入”调用,用来使旧 JS 代码快速参与新的模块系统之中。

如果一个 JS 模块系统不能搞定以上特性,那么与 AMD 和它相关的 API 们,例如 callback-require加载器插件 和基于路径的模块 ID,相较之下,高下立见。

AMD 使用情况

截至 2011年10月,AMD 已经在 Web 上广为使用:

你可以做什么

如果你是写应用的:

如果你是脚本库开发者:

  • 如果可用,条件调用 define()。妙处在于不依靠 AMD 你仍然可以编写你的库,只要可用的时候参与一下就可以了。 这使得你的模块用户可以:
    • 避免往页面全局变量里头塞东西
    • 代码加载、延迟加载等更加有选择
    • 用现有的 AMD 工具来优化它们的项目
    • 参与到当今浏览器中可用的 JS 模块系统中去

如果你为 JavaScript 加载器、引擎、环境编写代码:

  • 实现 AMD API。有个 讨论列表兼容性测试。通过实现 AMD 模块规范,你可以减少多模块系统的无谓竞争, 帮助为一个 Web 端可用的 JavaScript 模块系统证明。这也能反馈到 ECMAScript 工作组,以求更好的本地模块系统支持。
  • 也要支持 callback-require加载器插件。 加载器插件是个很好的减轻回调、异步风格的代码中常见的嵌套回调症的方式。
comments powered by Disqus

SeaJS 模块预处理

如何正确压缩、打包你的模块们

RequireJS 其一

为何用 Web 模块的方式?

本文讨论异步模块规范(Asynchronous Module Definition,AMD)API,即 RequireJS 所支持的模块 API, 的设计驱使与使用方式。另有页面讨论 Web 模块化的基本方式

模块目的

什么是 JavaScript 模块?它们的目的是什么?

  • 定义:如何把一段代码封装成一个有用的单元,以及如何注册此模块的能力、输出的值
  • 依赖引用:如何引用其它代码单元

现今 Web

(function () {
    var $ = this.jQuery;

    this.myExample = function () {};
}());

现如今 JavaScript 代码段是如何定义的呢?

  • 通过立即执行的工厂函数定义。
  • 使用 HTML script 标签加载模块,通过全局变量来引用依赖。
  • 模块间依赖的声明很弱:开发者需要知道正确的依赖顺序。例如,包含 Backbone 的文件,不能放在 jQuery 标签之前。
  • 优化部署的时候,需要用额外的工具来把一系列 script 标签替换成一个。

这些都会使大型项目变得难以管理,尤其是当脚本们的诸多依赖还可能重叠、嵌套的时候。 手工写 script 标签可不怎么灵活,而且这么做就没法搞按需加载了。

CommonJS

var $ = require('jquery');
exports.myExample = function () {};

最初的 CommonJS 小组 的参与者们决定弄一份于时下的 JavaScript 编程语言有效,但不必束缚于浏览器 JS 环境的限制,的模块规范。开始的愿景是在浏览器里使用一些权宜之计, 并希望能借此影响浏览器厂商,促使它们为这种模块规范的原生支持提供解决方案。权宜之计有:

  • 要么使用一个服务来转译 CJS 模块成浏览器中可用的代码
  • 要么使用 XMLHttpRequest(XHR)以文本形式加载模块,再在浏览器中做文本变换、解析的工作

CJS 模块规范仅允许每文件一个模块,所以为优化、打包,可使用某种“转换格式”将多个模块合并到单个文件。

通过这种方式,CommonJS 小组搞定了依赖引用、如何处理循环依赖,以及如何获得当前模块的某些属性等问题。 但是,他们并没能接纳浏览器环境里不可改变、并且仍将影响模块设计的某些特性:

  • 网络加载
  • 异步继承

这也同时意味着他们为了实现这个规范,将负担更多地放到了 Web 开发者身上,而这些权宜之计也使调试变得更麻烦。 调试 eval 的代码,或者调试多个文件合并之后的单个文件,都有实际使用时的坏处。 这些缺点或许在未来某天会被浏览器调试工具解决掉,但结论仍然是:在最普遍的 JS 环境,浏览器中,使用 CommonJS 模块并不是最好的办法

AMD

define(['jquery'] , function ($) {
    return function () {};
});

AMD 规范的缘起是,我们需要一个比时下那种“写一堆 script 标签、手工按序指明依赖”要好,并且容易在浏览器中直接使用的模块格式。 某种不需要服务端工具配合,又能够易于调试的模块格式。它超脱于 Dojo 使用 XHR+eval 的现实经验, 并且要规避 Dojo 的方式的缺点。

它比时下 Web 开发中“全局变量+script 标签”的方式要好,因为:

  • 应用 CommonJS 实践,使用字符串 ID 来声明依赖。使得依赖声明清晰,并且避免了使用全局变量。
  • 模块 ID 可以被映射到不同的路径。从而允许切换模块实现。这对创建单元测试模型很有帮助。 在上例中,代码实际期待的不过是某个实现了 jQuery API 与行为的模块而已。并不是非要 jQuery 本身不可。
  • 封装了模块定义。使你能够避免污染全局命名空间。
  • 清楚地定义模块输出。可以用 return value;,也可以用 CommonJS 的 exports 范式。 后者在循环依赖的时候很有用。

它是 CommonJS 模块规范的改善,因为:

  • 它在浏览器里跑得更好,它的坑是最少的。其他的解决方案,都有调试、跨域、CDN 使用,file:// 协议以及依赖服务端工具等问题。
  • 定义了将多个模块包含进单个文件的方式。在 CommonJS 的条款里,有个“传输规范”来做这个事情, 但 CommonJS 小组还没就此达成一致
  • 允许将函数作为返回值。这在模块返回值是构造函数的时候尤其有用。在 CommonJS 中就有点尴尬了, 永远要通过给 exports 对象设置属性来输出。Node 支持了 module.exports = function() {}, 不过这还没在 CommonJS 规范里面。

模块定义

使用 JavaScript 的函数进行封装,已经有 文档 约定了:

(function () {
   this.myGlobal = function () {};
}());

这种模块依赖于给全局对象附加属性来输出模块值,并且用这种模型很难声明模块依赖。 预设是,模块的依赖在执行此函数之前就已经是立即可用的。这限制了加载依赖的策略。

AMD 通过如下手段解决了这些问题:

  • 调用 define() 来注册工厂函数,而不是立即执行该函数。
  • 以字符串数组的形式将依赖传递进来,而不是直接从全局对象上取。
  • 在所有依赖都被加载、执行完毕之后,才执行工厂函数。
  • 将依赖的模块作为执行工厂函数的参数。
//Calling define with a dependency array and a factory function
define(['dep1', 'dep2'], function (dep1, dep2) {

    //Define the module value by returning a value.
    return function () {};
});

命名的模块

请注意,上例中的模块并没有给自己取名。这使得这个模块可以随便移动。它允许开发者将模块放到不同的目录, 从而管它们叫不同的名字。AMD 加载器会根据它被其他脚本引用的方式来给这个模块标记 ID。

但是,为了提高性能,打包多个模块的工具需要有个给单个文件中的每个模块命名的方式。对此需求,AMD 允许以字符串作为 define() 的第一个参数:

//Calling define with module ID, dependency array, and factory function
define('myModule', ['dep1', 'dep2'], function (dep1, dep2) {

    //Define the module value by returning a value.
    return function () {};
});

你应该避免自行命名模块,并且在开发时保持每文件一个模块。只不过,在注重性能的阶段, 得有个模块规范以在编译后的资源中命名模块。

语法糖

上面的 AMD 例子在所有浏览器中都能跑。但是,有搞错模块数组与参数顺序的风险,尤其是当你的模块依赖了好多的时候:

define([ "require", "jquery", "blade/object", "blade/fn", "rdapi",
         "oauth", "blade/jig", "blade/url", "dispatch", "accounts",
         "storage", "services", "widgets/AccountPanel", "widgets/TabButton",
         "widgets/AddAccount", "less", "osTheme", "jquery-ui-1.8.7.min",
         "jquery.textOverflow"],
function (require,   $,        object,         fn,         rdapi,
          oauth,   jig,         url,         dispatch,   accounts,
          storage,   services,   AccountPanel,           TabButton,
          AddAccount,           less,   osTheme) {

});

简而为之,同时也让封装 CommonJS 模块更容易,还支持这种形式的 define() (有时这种封装还被称作“简化 CommonJS 封装”):

define(function (require) {
    var dependency1 = require('dependency1'),
        dependency2 = require('dependency2');

    return function () {};
});

AMD 加载器会使用 Function.prototype.toString() 来解析出所有的 require('') 调用, 然后在内部将上述 define 调用转换成这种形式:

define(['require', 'dependency1', 'dependency2'], function (require) {
    var dependency1 = require('dependency1'),
        dependency2 = require('dependency2');

    return function () {};
});

如此,加载器会异步加载依赖一(dependency1)与依赖二(dependency2),执行这些依赖,然后执行这个工厂函数。

并非所有的浏览器都给出可用的 Function.prototype.toString() 结果。自2011年10月, PS3 和老旧的 Opera Mobile 浏览器的返回结果就不准确。这些浏览器也更可能因网络与设备的限制, 需要编译优化模块代码,所以直接用个懂得如何解析、转换正确的依赖数组的优化工具来打包优化一下就好, 例如 RequireJS 优化工具

因为不支持 toString() 解析的浏览器相当少,用这种语法糖形式来编写你的模块是安全可靠的。 特别是当你喜欢把依赖按照它们的名字排列整齐的时候。

CommonJS 兼容性

虽然这个语法糖形式被称作“简化 CommonJS 封装”,但它并非与 CommonJS 模块 100% 兼容。不过,CommonJS 模块一般认为依赖是同步加载的,完全兼容它的话,在浏览器里就会挂掉啦。

绝大部分 CJS(CommonJS)模块,根据个人经验(不太科学地毛估估)大约有 95%,是与简化 CommonJS 封装完美兼容的。

不兼容的模块,都是那些依赖是动态计算的,调用 require() 的时候不用字符串字面量的,以及其他不像个声明方式调用 require() 的。 所以,下边这种会挂:

// 不好
var mod = require(someCondition ? 'a' : 'b');

// 不好
// 译注:这种应该不会挂,但是不能达到选择加载的效果,会在请求、加载完 a 和 a1 之后,
// 才执行模块代码
if (someCondition) {
    var a = require('a');
} else {
    var a = require('a1');
}

这些使用方式,在 require 回调 中支持, 即 AMD 加载器中提供的全局的

require([moduleName], function() {});

AMD 的执行模式比较向 ECMAScript 和谐版(Harmoney)的模块规范所约定的看齐。 CommonJS 模块规范在 AMD 中的不兼容部分在和谐版(Harmoney)模块也一样不兼容。 AMD 的代码执行逻辑的未来兼容性更好。

冗长与可用性

对 AMD 的批评其一,至少与 CJS 模块规范相比,是它要求一层缩进,以及一个函数封装。

但事实很简单:要用 AMD 就觉得需要多打点字并多缩进一层,其实这一点都没关系。你编程的时间是这么花的:

  • 思考问题
  • 阅读代码

绝大部分编程的时间是花在思考而非敲代码上的。虽然代码一般越短越好,但所能付出的代价终归是有限的, 何况用 AMD 要再打些的字其实也没那么多。

而且大部分 Web 开发者本来就在用匿名函数封装了,目的是避免污染全局变量。看到逻辑代码外边包了匿名函数,其实是很普遍的, 并不会给阅读模块代码带来困扰。

同时,CommonJS 格式还有些潜在代价:

  • 依赖工具的代价
  • 某些边界用例在浏览器中会挂,比如跨域访问
  • 调试更差,此代价随着时间增加,会越来越大

AMD 模块规范需要的工具更少,边界用例问题更少,调试支持也更好。

重要的是:能够真正与其他人分享代码。AMD 规范是达此目标更轻松的方式。

拥有一个可用的,易于调试,并且还在现如今的浏览器中能跑的模块系统,意味着在创造未来的 JavaScript 中最好的模块系统的同时,得到现实世界的体验。

AMD 和它相关的 API 们,已经帮助展示了任何未来 JS 的模块系统都具备的以下特性:

  • 返回一个函数作为模块值,尤其是构造函数,促使更好的 API 设计。Node 有 module.exports 来支持此特性, 但能够用 return function() {} 的话会更简洁。这意味着不需要持有 modulemodule.exports, 并且代码表达也更清晰。
  • 动态代码加载(AMD 模块系统中通过 require[],function(){}) 来实现) 是基本要求。CJS 小组谈到了这个问题,提了一些议案,但它并没有广为接受。Node 并不支持这个需求,转而依赖 require() 的同步行为, 而这在 Web 端是无从实现的。
  • 加载器插件 相当有用。它帮助避免基于回调的编程中常遇到的嵌套缩进的问题。
  • 选择映射某个模块 从另一个地址加载,使得提供模块模拟对象以供测试变得容易。
  • 每个模块最多一个 IO 行为,并且这个 IO 行为应该简单直接。Web 浏览器受不了找个模块还有多次 IO 查找。 这和 Node 里现在的多次寻址是相悖的,并且避免使用一个有 main 属性的 package.json。 用能够很容易地根据项目目录结构映射到某个地址的模块名,一个合理的不需要冗长的配置的约定就够了,不过要允许必要的简单配置。
  • 最好有个“选择加入”调用,用来使旧 JS 代码快速参与新的模块系统之中。

如果一个 JS 模块系统不能搞定以上特性,那么与 AMD 和它相关的 API 们,例如 callback-require加载器插件 和基于路径的模块 ID,相较之下,高下立见。

AMD 使用情况

截至 2011年10月,AMD 已经在 Web 上广为使用:

你可以做什么

如果你是写应用的:

如果你是脚本库开发者:

  • 如果可用,条件调用 define()。妙处在于不依靠 AMD 你仍然可以编写你的库,只要可用的时候参与一下就可以了。 这使得你的模块用户可以:
    • 避免往页面全局变量里头塞东西
    • 代码加载、延迟加载等更加有选择
    • 用现有的 AMD 工具来优化它们的项目
    • 参与到当今浏览器中可用的 JS 模块系统中去

如果你为 JavaScript 加载器、引擎、环境编写代码:

  • 实现 AMD API。有个 讨论列表兼容性测试。通过实现 AMD 模块规范,你可以减少多模块系统的无谓竞争, 帮助为一个 Web 端可用的 JavaScript 模块系统证明。这也能反馈到 ECMAScript 工作组,以求更好的本地模块系统支持。
  • 也要支持 callback-require加载器插件。 加载器插件是个很好的减轻回调、异步风格的代码中常见的嵌套回调症的方式。

RequireJS 其二

为何用 AMD 规范?

本文讨论异步模块规范(Asynchronous Module Definition,AMD)API,即 RequireJS 所支持的模块 API, 的设计驱使与使用方式。另有页面讨论 Web 模块化的基本方式

模块目的

什么是 JavaScript 模块?它们的目的是什么?

  • 定义:如何把一段代码封装成一个有用的单元,以及如何注册此模块的能力、输出的值
  • 依赖引用:如何引用其它代码单元

现今 Web

(function () {
    var $ = this.jQuery;

    this.myExample = function () {};
}());

现如今 JavaScript 代码段是如何定义的呢?

  • 通过立即执行的工厂函数定义。
  • 使用 HTML script 标签加载模块,通过全局变量来引用依赖。
  • 模块间依赖的声明很弱:开发者需要知道正确的依赖顺序。例如,包含 Backbone 的文件,不能放在 jQuery 标签之前。
  • 优化部署的时候,需要用额外的工具来把一系列 script 标签替换成一个。

这些都会使大型项目变得难以管理,尤其是当脚本们的诸多依赖还可能重叠、嵌套的时候。 手工写 script 标签可不怎么灵活,而且这么做就没法搞按需加载了。

CommonJS

var $ = require('jquery');
exports.myExample = function () {};

最初的 CommonJS 小组 的参与者们决定弄一份于时下的 JavaScript 编程语言有效,但不必束缚于浏览器 JS 环境的限制,的模块规范。开始的愿景是在浏览器里使用一些权宜之计, 并希望能借此影响浏览器厂商,促使它们为这种模块规范的原生支持提供解决方案。权宜之计有:

  • 要么使用一个服务来转译 CJS 模块成浏览器中可用的代码
  • 要么使用 XMLHttpRequest(XHR)以文本形式加载模块,再在浏览器中做文本变换、解析的工作

CJS 模块规范仅允许每文件一个模块,所以为优化、打包,可使用某种“转换格式”将多个模块合并到单个文件。

通过这种方式,CommonJS 小组搞定了依赖引用、如何处理循环依赖,以及如何获得当前模块的某些属性等问题。 但是,他们并没能接纳浏览器环境里不可改变、并且仍将影响模块设计的某些特性:

  • 网络加载
  • 异步继承

这也同时意味着他们为了实现这个规范,将负担更多地放到了 Web 开发者身上,而这些权宜之计也使调试变得更麻烦。 调试 eval 的代码,或者调试多个文件合并之后的单个文件,都有实际使用时的坏处。 这些缺点或许在未来某天会被浏览器调试工具解决掉,但结论仍然是:在最普遍的 JS 环境,浏览器中,使用 CommonJS 模块并不是最好的办法

AMD

define(['jquery'] , function ($) {
    return function () {};
});

AMD 规范的缘起是,我们需要一个比时下那种“写一堆 script 标签、手工按序指明依赖”要好,并且容易在浏览器中直接使用的模块格式。 某种不需要服务端工具配合,又能够易于调试的模块格式。它超脱于 Dojo 使用 XHR+eval 的现实经验, 并且要规避 Dojo 的方式的缺点。

它比时下 Web 开发中“全局变量+script 标签”的方式要好,因为:

  • 应用 CommonJS 实践,使用字符串 ID 来声明依赖。使得依赖声明清晰,并且避免了使用全局变量。
  • 模块 ID 可以被映射到不同的路径。从而允许切换模块实现。这对创建单元测试模型很有帮助。 在上例中,代码实际期待的不过是某个实现了 jQuery API 与行为的模块而已。并不是非要 jQuery 本身不可。
  • 封装了模块定义。使你能够避免污染全局命名空间。
  • 清楚地定义模块输出。可以用 return value;,也可以用 CommonJS 的 exports 范式。 后者在循环依赖的时候很有用。

它是 CommonJS 模块规范的改善,因为:

  • 它在浏览器里跑得更好,它的坑是最少的。其他的解决方案,都有调试、跨域、CDN 使用,file:// 协议以及依赖服务端工具等问题。
  • 定义了将多个模块包含进单个文件的方式。在 CommonJS 的条款里,有个“传输规范”来做这个事情, 但 CommonJS 小组还没就此达成一致
  • 允许将函数作为返回值。这在模块返回值是构造函数的时候尤其有用。在 CommonJS 中就有点尴尬了, 永远要通过给 exports 对象设置属性来输出。Node 支持了 module.exports = function() {}, 不过这还没在 CommonJS 规范里面。

模块定义

使用 JavaScript 的函数进行封装,已经有 文档 约定了:

(function () {
   this.myGlobal = function () {};
}());

这种模块依赖于给全局对象附加属性来输出模块值,并且用这种模型很难声明模块依赖。 预设是,模块的依赖在执行此函数之前就已经是立即可用的。这限制了加载依赖的策略。

AMD 通过如下手段解决了这些问题:

  • 调用 define() 来注册工厂函数,而不是立即执行该函数。
  • 以字符串数组的形式将依赖传递进来,而不是直接从全局对象上取。
  • 在所有依赖都被加载、执行完毕之后,才执行工厂函数。
  • 将依赖的模块作为执行工厂函数的参数。
//Calling define with a dependency array and a factory function
define(['dep1', 'dep2'], function (dep1, dep2) {

    //Define the module value by returning a value.
    return function () {};
});

命名的模块

请注意,上例中的模块并没有给自己取名。这使得这个模块可以随便移动。它允许开发者将模块放到不同的目录, 从而管它们叫不同的名字。AMD 加载器会根据它被其他脚本引用的方式来给这个模块标记 ID。

但是,为了提高性能,打包多个模块的工具需要有个给单个文件中的每个模块命名的方式。对此需求,AMD 允许以字符串作为 define() 的第一个参数:

//Calling define with module ID, dependency array, and factory function
define('myModule', ['dep1', 'dep2'], function (dep1, dep2) {

    //Define the module value by returning a value.
    return function () {};
});

你应该避免自行命名模块,并且在开发时保持每文件一个模块。只不过,在注重性能的阶段, 得有个模块规范以在编译后的资源中命名模块。

语法糖

上面的 AMD 例子在所有浏览器中都能跑。但是,有搞错模块数组与参数顺序的风险,尤其是当你的模块依赖了好多的时候:

define([ "require", "jquery", "blade/object", "blade/fn", "rdapi",
         "oauth", "blade/jig", "blade/url", "dispatch", "accounts",
         "storage", "services", "widgets/AccountPanel", "widgets/TabButton",
         "widgets/AddAccount", "less", "osTheme", "jquery-ui-1.8.7.min",
         "jquery.textOverflow"],
function (require,   $,        object,         fn,         rdapi,
          oauth,   jig,         url,         dispatch,   accounts,
          storage,   services,   AccountPanel,           TabButton,
          AddAccount,           less,   osTheme) {

});

简而为之,同时也让封装 CommonJS 模块更容易,还支持这种形式的 define() (有时这种封装还被称作“简化 CommonJS 封装”):

define(function (require) {
    var dependency1 = require('dependency1'),
        dependency2 = require('dependency2');

    return function () {};
});

AMD 加载器会使用 Function.prototype.toString() 来解析出所有的 require('') 调用, 然后在内部将上述 define 调用转换成这种形式:

define(['require', 'dependency1', 'dependency2'], function (require) {
    var dependency1 = require('dependency1'),
        dependency2 = require('dependency2');

    return function () {};
});

如此,加载器会异步加载依赖一(dependency1)与依赖二(dependency2),执行这些依赖,然后执行这个工厂函数。

并非所有的浏览器都给出可用的 Function.prototype.toString() 结果。自2011年10月, PS3 和老旧的 Opera Mobile 浏览器的返回结果就不准确。这些浏览器也更可能因网络与设备的限制, 需要编译优化模块代码,所以直接用个懂得如何解析、转换正确的依赖数组的优化工具来打包优化一下就好, 例如 RequireJS 优化工具

因为不支持 toString() 解析的浏览器相当少,用这种语法糖形式来编写你的模块是安全可靠的。 特别是当你喜欢把依赖按照它们的名字排列整齐的时候。

CommonJS 兼容性

虽然这个语法糖形式被称作“简化 CommonJS 封装”,但它并非与 CommonJS 模块 100% 兼容。不过,CommonJS 模块一般认为依赖是同步加载的,完全兼容它的话,在浏览器里就会挂掉啦。

绝大部分 CJS(CommonJS)模块,根据个人经验(不太科学地毛估估)大约有 95%,是与简化 CommonJS 封装完美兼容的。

不兼容的模块,都是那些依赖是动态计算的,调用 require() 的时候不用字符串字面量的,以及其他不像个声明方式调用 require() 的。 所以,下边这种会挂:

// 不好
var mod = require(someCondition ? 'a' : 'b');

// 不好
// 译注:这种应该不会挂,但是不能达到选择加载的效果,会在请求、加载完 a 和 a1 之后,
// 才执行模块代码
if (someCondition) {
    var a = require('a');
} else {
    var a = require('a1');
}

这些使用方式,在 require 回调 中支持, 即 AMD 加载器中提供的全局的

require([moduleName], function() {});

AMD 的执行模式比较向 ECMAScript 和谐版(Harmoney)的模块规范所约定的看齐。 CommonJS 模块规范在 AMD 中的不兼容部分在和谐版(Harmoney)模块也一样不兼容。 AMD 的代码执行逻辑的未来兼容性更好。

冗长与可用性

对 AMD 的批评其一,至少与 CJS 模块规范相比,是它要求一层缩进,以及一个函数封装。

但事实很简单:要用 AMD 就觉得需要多打点字并多缩进一层,其实这一点都没关系。你编程的时间是这么花的:

  • 思考问题
  • 阅读代码

绝大部分编程的时间是花在思考而非敲代码上的。虽然代码一般越短越好,但所能付出的代价终归是有限的, 何况用 AMD 要再打些的字其实也没那么多。

而且大部分 Web 开发者本来就在用匿名函数封装了,目的是避免污染全局变量。看到逻辑代码外边包了匿名函数,其实是很普遍的, 并不会给阅读模块代码带来困扰。

同时,CommonJS 格式还有些潜在代价:

  • 依赖工具的代价
  • 某些边界用例在浏览器中会挂,比如跨域访问
  • 调试更差,此代价随着时间增加,会越来越大

AMD 模块规范需要的工具更少,边界用例问题更少,调试支持也更好。

重要的是:能够真正与其他人分享代码。AMD 规范是达此目标更轻松的方式。

拥有一个可用的,易于调试,并且还在现如今的浏览器中能跑的模块系统,意味着在创造未来的 JavaScript 中最好的模块系统的同时,得到现实世界的体验。

AMD 和它相关的 API 们,已经帮助展示了任何未来 JS 的模块系统都具备的以下特性:

  • 返回一个函数作为模块值,尤其是构造函数,促使更好的 API 设计。Node 有 module.exports 来支持此特性, 但能够用 return function() {} 的话会更简洁。这意味着不需要持有 modulemodule.exports, 并且代码表达也更清晰。
  • 动态代码加载(AMD 模块系统中通过 require[],function(){}) 来实现) 是基本要求。CJS 小组谈到了这个问题,提了一些议案,但它并没有广为接受。Node 并不支持这个需求,转而依赖 require() 的同步行为, 而这在 Web 端是无从实现的。
  • 加载器插件 相当有用。它帮助避免基于回调的编程中常遇到的嵌套缩进的问题。
  • 选择映射某个模块 从另一个地址加载,使得提供模块模拟对象以供测试变得容易。
  • 每个模块最多一个 IO 行为,并且这个 IO 行为应该简单直接。Web 浏览器受不了找个模块还有多次 IO 查找。 这和 Node 里现在的多次寻址是相悖的,并且避免使用一个有 main 属性的 package.json。 用能够很容易地根据项目目录结构映射到某个地址的模块名,一个合理的不需要冗长的配置的约定就够了,不过要允许必要的简单配置。
  • 最好有个“选择加入”调用,用来使旧 JS 代码快速参与新的模块系统之中。

如果一个 JS 模块系统不能搞定以上特性,那么与 AMD 和它相关的 API 们,例如 callback-require加载器插件 和基于路径的模块 ID,相较之下,高下立见。

AMD 使用情况

截至 2011年10月,AMD 已经在 Web 上广为使用:

你可以做什么

如果你是写应用的:

如果你是脚本库开发者:

  • 如果可用,条件调用 define()。妙处在于不依靠 AMD 你仍然可以编写你的库,只要可用的时候参与一下就可以了。 这使得你的模块用户可以:
    • 避免往页面全局变量里头塞东西
    • 代码加载、延迟加载等更加有选择
    • 用现有的 AMD 工具来优化它们的项目
    • 参与到当今浏览器中可用的 JS 模块系统中去

如果你为 JavaScript 加载器、引擎、环境编写代码:

  • 实现 AMD API。有个 讨论列表兼容性测试。通过实现 AMD 模块规范,你可以减少多模块系统的无谓竞争, 帮助为一个 Web 端可用的 JavaScript 模块系统证明。这也能反馈到 ECMAScript 工作组,以求更好的本地模块系统支持。
  • 也要支持 callback-require加载器插件。 加载器插件是个很好的减轻回调、异步风格的代码中常见的嵌套回调症的方式。
comments powered by Disqus
comments powered by Disqus

SeaJS 进阶

如何处理复杂代码结构

SeaJS 文摘

为何会有文摘?

RequireJS 其一

为何用 Web 模块的方式?

本文讨论异步模块规范(Asynchronous Module Definition,AMD)API,即 RequireJS 所支持的模块 API, 的设计驱使与使用方式。另有页面讨论 Web 模块化的基本方式

模块目的

什么是 JavaScript 模块?它们的目的是什么?

  • 定义:如何把一段代码封装成一个有用的单元,以及如何注册此模块的能力、输出的值
  • 依赖引用:如何引用其它代码单元

现今 Web

(function () {
    var $ = this.jQuery;

    this.myExample = function () {};
}());

现如今 JavaScript 代码段是如何定义的呢?

  • 通过立即执行的工厂函数定义。
  • 使用 HTML script 标签加载模块,通过全局变量来引用依赖。
  • 模块间依赖的声明很弱:开发者需要知道正确的依赖顺序。例如,包含 Backbone 的文件,不能放在 jQuery 标签之前。
  • 优化部署的时候,需要用额外的工具来把一系列 script 标签替换成一个。

这些都会使大型项目变得难以管理,尤其是当脚本们的诸多依赖还可能重叠、嵌套的时候。 手工写 script 标签可不怎么灵活,而且这么做就没法搞按需加载了。

CommonJS

var $ = require('jquery');
exports.myExample = function () {};

最初的 CommonJS 小组 的参与者们决定弄一份于时下的 JavaScript 编程语言有效,但不必束缚于浏览器 JS 环境的限制,的模块规范。开始的愿景是在浏览器里使用一些权宜之计, 并希望能借此影响浏览器厂商,促使它们为这种模块规范的原生支持提供解决方案。权宜之计有:

  • 要么使用一个服务来转译 CJS 模块成浏览器中可用的代码
  • 要么使用 XMLHttpRequest(XHR)以文本形式加载模块,再在浏览器中做文本变换、解析的工作

CJS 模块规范仅允许每文件一个模块,所以为优化、打包,可使用某种“转换格式”将多个模块合并到单个文件。

通过这种方式,CommonJS 小组搞定了依赖引用、如何处理循环依赖,以及如何获得当前模块的某些属性等问题。 但是,他们并没能接纳浏览器环境里不可改变、并且仍将影响模块设计的某些特性:

  • 网络加载
  • 异步继承

这也同时意味着他们为了实现这个规范,将负担更多地放到了 Web 开发者身上,而这些权宜之计也使调试变得更麻烦。 调试 eval 的代码,或者调试多个文件合并之后的单个文件,都有实际使用时的坏处。 这些缺点或许在未来某天会被浏览器调试工具解决掉,但结论仍然是:在最普遍的 JS 环境,浏览器中,使用 CommonJS 模块并不是最好的办法

AMD

define(['jquery'] , function ($) {
    return function () {};
});

AMD 规范的缘起是,我们需要一个比时下那种“写一堆 script 标签、手工按序指明依赖”要好,并且容易在浏览器中直接使用的模块格式。 某种不需要服务端工具配合,又能够易于调试的模块格式。它超脱于 Dojo 使用 XHR+eval 的现实经验, 并且要规避 Dojo 的方式的缺点。

它比时下 Web 开发中“全局变量+script 标签”的方式要好,因为:

  • 应用 CommonJS 实践,使用字符串 ID 来声明依赖。使得依赖声明清晰,并且避免了使用全局变量。
  • 模块 ID 可以被映射到不同的路径。从而允许切换模块实现。这对创建单元测试模型很有帮助。 在上例中,代码实际期待的不过是某个实现了 jQuery API 与行为的模块而已。并不是非要 jQuery 本身不可。
  • 封装了模块定义。使你能够避免污染全局命名空间。
  • 清楚地定义模块输出。可以用 return value;,也可以用 CommonJS 的 exports 范式。 后者在循环依赖的时候很有用。

它是 CommonJS 模块规范的改善,因为:

  • 它在浏览器里跑得更好,它的坑是最少的。其他的解决方案,都有调试、跨域、CDN 使用,file:// 协议以及依赖服务端工具等问题。
  • 定义了将多个模块包含进单个文件的方式。在 CommonJS 的条款里,有个“传输规范”来做这个事情, 但 CommonJS 小组还没就此达成一致
  • 允许将函数作为返回值。这在模块返回值是构造函数的时候尤其有用。在 CommonJS 中就有点尴尬了, 永远要通过给 exports 对象设置属性来输出。Node 支持了 module.exports = function() {}, 不过这还没在 CommonJS 规范里面。

模块定义

使用 JavaScript 的函数进行封装,已经有 文档 约定了:

(function () {
   this.myGlobal = function () {};
}());

这种模块依赖于给全局对象附加属性来输出模块值,并且用这种模型很难声明模块依赖。 预设是,模块的依赖在执行此函数之前就已经是立即可用的。这限制了加载依赖的策略。

AMD 通过如下手段解决了这些问题:

  • 调用 define() 来注册工厂函数,而不是立即执行该函数。
  • 以字符串数组的形式将依赖传递进来,而不是直接从全局对象上取。
  • 在所有依赖都被加载、执行完毕之后,才执行工厂函数。
  • 将依赖的模块作为执行工厂函数的参数。
//Calling define with a dependency array and a factory function
define(['dep1', 'dep2'], function (dep1, dep2) {

    //Define the module value by returning a value.
    return function () {};
});

命名的模块

请注意,上例中的模块并没有给自己取名。这使得这个模块可以随便移动。它允许开发者将模块放到不同的目录, 从而管它们叫不同的名字。AMD 加载器会根据它被其他脚本引用的方式来给这个模块标记 ID。

但是,为了提高性能,打包多个模块的工具需要有个给单个文件中的每个模块命名的方式。对此需求,AMD 允许以字符串作为 define() 的第一个参数:

//Calling define with module ID, dependency array, and factory function
define('myModule', ['dep1', 'dep2'], function (dep1, dep2) {

    //Define the module value by returning a value.
    return function () {};
});

你应该避免自行命名模块,并且在开发时保持每文件一个模块。只不过,在注重性能的阶段, 得有个模块规范以在编译后的资源中命名模块。

语法糖

上面的 AMD 例子在所有浏览器中都能跑。但是,有搞错模块数组与参数顺序的风险,尤其是当你的模块依赖了好多的时候:

define([ "require", "jquery", "blade/object", "blade/fn", "rdapi",
         "oauth", "blade/jig", "blade/url", "dispatch", "accounts",
         "storage", "services", "widgets/AccountPanel", "widgets/TabButton",
         "widgets/AddAccount", "less", "osTheme", "jquery-ui-1.8.7.min",
         "jquery.textOverflow"],
function (require,   $,        object,         fn,         rdapi,
          oauth,   jig,         url,         dispatch,   accounts,
          storage,   services,   AccountPanel,           TabButton,
          AddAccount,           less,   osTheme) {

});

简而为之,同时也让封装 CommonJS 模块更容易,还支持这种形式的 define() (有时这种封装还被称作“简化 CommonJS 封装”):

define(function (require) {
    var dependency1 = require('dependency1'),
        dependency2 = require('dependency2');

    return function () {};
});

AMD 加载器会使用 Function.prototype.toString() 来解析出所有的 require('') 调用, 然后在内部将上述 define 调用转换成这种形式:

define(['require', 'dependency1', 'dependency2'], function (require) {
    var dependency1 = require('dependency1'),
        dependency2 = require('dependency2');

    return function () {};
});

如此,加载器会异步加载依赖一(dependency1)与依赖二(dependency2),执行这些依赖,然后执行这个工厂函数。

并非所有的浏览器都给出可用的 Function.prototype.toString() 结果。自2011年10月, PS3 和老旧的 Opera Mobile 浏览器的返回结果就不准确。这些浏览器也更可能因网络与设备的限制, 需要编译优化模块代码,所以直接用个懂得如何解析、转换正确的依赖数组的优化工具来打包优化一下就好, 例如 RequireJS 优化工具

因为不支持 toString() 解析的浏览器相当少,用这种语法糖形式来编写你的模块是安全可靠的。 特别是当你喜欢把依赖按照它们的名字排列整齐的时候。

CommonJS 兼容性

虽然这个语法糖形式被称作“简化 CommonJS 封装”,但它并非与 CommonJS 模块 100% 兼容。不过,CommonJS 模块一般认为依赖是同步加载的,完全兼容它的话,在浏览器里就会挂掉啦。

绝大部分 CJS(CommonJS)模块,根据个人经验(不太科学地毛估估)大约有 95%,是与简化 CommonJS 封装完美兼容的。

不兼容的模块,都是那些依赖是动态计算的,调用 require() 的时候不用字符串字面量的,以及其他不像个声明方式调用 require() 的。 所以,下边这种会挂:

// 不好
var mod = require(someCondition ? 'a' : 'b');

// 不好
// 译注:这种应该不会挂,但是不能达到选择加载的效果,会在请求、加载完 a 和 a1 之后,
// 才执行模块代码
if (someCondition) {
    var a = require('a');
} else {
    var a = require('a1');
}

这些使用方式,在 require 回调 中支持, 即 AMD 加载器中提供的全局的

require([moduleName], function() {});

AMD 的执行模式比较向 ECMAScript 和谐版(Harmoney)的模块规范所约定的看齐。 CommonJS 模块规范在 AMD 中的不兼容部分在和谐版(Harmoney)模块也一样不兼容。 AMD 的代码执行逻辑的未来兼容性更好。

冗长与可用性

对 AMD 的批评其一,至少与 CJS 模块规范相比,是它要求一层缩进,以及一个函数封装。

但事实很简单:要用 AMD 就觉得需要多打点字并多缩进一层,其实这一点都没关系。你编程的时间是这么花的:

  • 思考问题
  • 阅读代码

绝大部分编程的时间是花在思考而非敲代码上的。虽然代码一般越短越好,但所能付出的代价终归是有限的, 何况用 AMD 要再打些的字其实也没那么多。

而且大部分 Web 开发者本来就在用匿名函数封装了,目的是避免污染全局变量。看到逻辑代码外边包了匿名函数,其实是很普遍的, 并不会给阅读模块代码带来困扰。

同时,CommonJS 格式还有些潜在代价:

  • 依赖工具的代价
  • 某些边界用例在浏览器中会挂,比如跨域访问
  • 调试更差,此代价随着时间增加,会越来越大

AMD 模块规范需要的工具更少,边界用例问题更少,调试支持也更好。

重要的是:能够真正与其他人分享代码。AMD 规范是达此目标更轻松的方式。

拥有一个可用的,易于调试,并且还在现如今的浏览器中能跑的模块系统,意味着在创造未来的 JavaScript 中最好的模块系统的同时,得到现实世界的体验。

AMD 和它相关的 API 们,已经帮助展示了任何未来 JS 的模块系统都具备的以下特性:

  • 返回一个函数作为模块值,尤其是构造函数,促使更好的 API 设计。Node 有 module.exports 来支持此特性, 但能够用 return function() {} 的话会更简洁。这意味着不需要持有 modulemodule.exports, 并且代码表达也更清晰。
  • 动态代码加载(AMD 模块系统中通过 require[],function(){}) 来实现) 是基本要求。CJS 小组谈到了这个问题,提了一些议案,但它并没有广为接受。Node 并不支持这个需求,转而依赖 require() 的同步行为, 而这在 Web 端是无从实现的。
  • 加载器插件 相当有用。它帮助避免基于回调的编程中常遇到的嵌套缩进的问题。
  • 选择映射某个模块 从另一个地址加载,使得提供模块模拟对象以供测试变得容易。
  • 每个模块最多一个 IO 行为,并且这个 IO 行为应该简单直接。Web 浏览器受不了找个模块还有多次 IO 查找。 这和 Node 里现在的多次寻址是相悖的,并且避免使用一个有 main 属性的 package.json。 用能够很容易地根据项目目录结构映射到某个地址的模块名,一个合理的不需要冗长的配置的约定就够了,不过要允许必要的简单配置。
  • 最好有个“选择加入”调用,用来使旧 JS 代码快速参与新的模块系统之中。

如果一个 JS 模块系统不能搞定以上特性,那么与 AMD 和它相关的 API 们,例如 callback-require加载器插件 和基于路径的模块 ID,相较之下,高下立见。

AMD 使用情况

截至 2011年10月,AMD 已经在 Web 上广为使用:

你可以做什么

如果你是写应用的:

如果你是脚本库开发者:

  • 如果可用,条件调用 define()。妙处在于不依靠 AMD 你仍然可以编写你的库,只要可用的时候参与一下就可以了。 这使得你的模块用户可以:
    • 避免往页面全局变量里头塞东西
    • 代码加载、延迟加载等更加有选择
    • 用现有的 AMD 工具来优化它们的项目
    • 参与到当今浏览器中可用的 JS 模块系统中去

如果你为 JavaScript 加载器、引擎、环境编写代码:

  • 实现 AMD API。有个 讨论列表兼容性测试。通过实现 AMD 模块规范,你可以减少多模块系统的无谓竞争, 帮助为一个 Web 端可用的 JavaScript 模块系统证明。这也能反馈到 ECMAScript 工作组,以求更好的本地模块系统支持。
  • 也要支持 callback-require加载器插件。 加载器插件是个很好的减轻回调、异步风格的代码中常见的嵌套回调症的方式。

RequireJS 其二

为何用 AMD 规范?

本文讨论异步模块规范(Asynchronous Module Definition,AMD)API,即 RequireJS 所支持的模块 API, 的设计驱使与使用方式。另有页面讨论 Web 模块化的基本方式

模块目的

什么是 JavaScript 模块?它们的目的是什么?

  • 定义:如何把一段代码封装成一个有用的单元,以及如何注册此模块的能力、输出的值
  • 依赖引用:如何引用其它代码单元

现今 Web

(function () {
    var $ = this.jQuery;

    this.myExample = function () {};
}());

现如今 JavaScript 代码段是如何定义的呢?

  • 通过立即执行的工厂函数定义。
  • 使用 HTML script 标签加载模块,通过全局变量来引用依赖。
  • 模块间依赖的声明很弱:开发者需要知道正确的依赖顺序。例如,包含 Backbone 的文件,不能放在 jQuery 标签之前。
  • 优化部署的时候,需要用额外的工具来把一系列 script 标签替换成一个。

这些都会使大型项目变得难以管理,尤其是当脚本们的诸多依赖还可能重叠、嵌套的时候。 手工写 script 标签可不怎么灵活,而且这么做就没法搞按需加载了。

CommonJS

var $ = require('jquery');
exports.myExample = function () {};

最初的 CommonJS 小组 的参与者们决定弄一份于时下的 JavaScript 编程语言有效,但不必束缚于浏览器 JS 环境的限制,的模块规范。开始的愿景是在浏览器里使用一些权宜之计, 并希望能借此影响浏览器厂商,促使它们为这种模块规范的原生支持提供解决方案。权宜之计有:

  • 要么使用一个服务来转译 CJS 模块成浏览器中可用的代码
  • 要么使用 XMLHttpRequest(XHR)以文本形式加载模块,再在浏览器中做文本变换、解析的工作

CJS 模块规范仅允许每文件一个模块,所以为优化、打包,可使用某种“转换格式”将多个模块合并到单个文件。

通过这种方式,CommonJS 小组搞定了依赖引用、如何处理循环依赖,以及如何获得当前模块的某些属性等问题。 但是,他们并没能接纳浏览器环境里不可改变、并且仍将影响模块设计的某些特性:

  • 网络加载
  • 异步继承

这也同时意味着他们为了实现这个规范,将负担更多地放到了 Web 开发者身上,而这些权宜之计也使调试变得更麻烦。 调试 eval 的代码,或者调试多个文件合并之后的单个文件,都有实际使用时的坏处。 这些缺点或许在未来某天会被浏览器调试工具解决掉,但结论仍然是:在最普遍的 JS 环境,浏览器中,使用 CommonJS 模块并不是最好的办法

AMD

define(['jquery'] , function ($) {
    return function () {};
});

AMD 规范的缘起是,我们需要一个比时下那种“写一堆 script 标签、手工按序指明依赖”要好,并且容易在浏览器中直接使用的模块格式。 某种不需要服务端工具配合,又能够易于调试的模块格式。它超脱于 Dojo 使用 XHR+eval 的现实经验, 并且要规避 Dojo 的方式的缺点。

它比时下 Web 开发中“全局变量+script 标签”的方式要好,因为:

  • 应用 CommonJS 实践,使用字符串 ID 来声明依赖。使得依赖声明清晰,并且避免了使用全局变量。
  • 模块 ID 可以被映射到不同的路径。从而允许切换模块实现。这对创建单元测试模型很有帮助。 在上例中,代码实际期待的不过是某个实现了 jQuery API 与行为的模块而已。并不是非要 jQuery 本身不可。
  • 封装了模块定义。使你能够避免污染全局命名空间。
  • 清楚地定义模块输出。可以用 return value;,也可以用 CommonJS 的 exports 范式。 后者在循环依赖的时候很有用。

它是 CommonJS 模块规范的改善,因为:

  • 它在浏览器里跑得更好,它的坑是最少的。其他的解决方案,都有调试、跨域、CDN 使用,file:// 协议以及依赖服务端工具等问题。
  • 定义了将多个模块包含进单个文件的方式。在 CommonJS 的条款里,有个“传输规范”来做这个事情, 但 CommonJS 小组还没就此达成一致
  • 允许将函数作为返回值。这在模块返回值是构造函数的时候尤其有用。在 CommonJS 中就有点尴尬了, 永远要通过给 exports 对象设置属性来输出。Node 支持了 module.exports = function() {}, 不过这还没在 CommonJS 规范里面。

模块定义

使用 JavaScript 的函数进行封装,已经有 文档 约定了:

(function () {
   this.myGlobal = function () {};
}());

这种模块依赖于给全局对象附加属性来输出模块值,并且用这种模型很难声明模块依赖。 预设是,模块的依赖在执行此函数之前就已经是立即可用的。这限制了加载依赖的策略。

AMD 通过如下手段解决了这些问题:

  • 调用 define() 来注册工厂函数,而不是立即执行该函数。
  • 以字符串数组的形式将依赖传递进来,而不是直接从全局对象上取。
  • 在所有依赖都被加载、执行完毕之后,才执行工厂函数。
  • 将依赖的模块作为执行工厂函数的参数。
//Calling define with a dependency array and a factory function
define(['dep1', 'dep2'], function (dep1, dep2) {

    //Define the module value by returning a value.
    return function () {};
});

命名的模块

请注意,上例中的模块并没有给自己取名。这使得这个模块可以随便移动。它允许开发者将模块放到不同的目录, 从而管它们叫不同的名字。AMD 加载器会根据它被其他脚本引用的方式来给这个模块标记 ID。

但是,为了提高性能,打包多个模块的工具需要有个给单个文件中的每个模块命名的方式。对此需求,AMD 允许以字符串作为 define() 的第一个参数:

//Calling define with module ID, dependency array, and factory function
define('myModule', ['dep1', 'dep2'], function (dep1, dep2) {

    //Define the module value by returning a value.
    return function () {};
});

你应该避免自行命名模块,并且在开发时保持每文件一个模块。只不过,在注重性能的阶段, 得有个模块规范以在编译后的资源中命名模块。

语法糖

上面的 AMD 例子在所有浏览器中都能跑。但是,有搞错模块数组与参数顺序的风险,尤其是当你的模块依赖了好多的时候:

define([ "require", "jquery", "blade/object", "blade/fn", "rdapi",
         "oauth", "blade/jig", "blade/url", "dispatch", "accounts",
         "storage", "services", "widgets/AccountPanel", "widgets/TabButton",
         "widgets/AddAccount", "less", "osTheme", "jquery-ui-1.8.7.min",
         "jquery.textOverflow"],
function (require,   $,        object,         fn,         rdapi,
          oauth,   jig,         url,         dispatch,   accounts,
          storage,   services,   AccountPanel,           TabButton,
          AddAccount,           less,   osTheme) {

});

简而为之,同时也让封装 CommonJS 模块更容易,还支持这种形式的 define() (有时这种封装还被称作“简化 CommonJS 封装”):

define(function (require) {
    var dependency1 = require('dependency1'),
        dependency2 = require('dependency2');

    return function () {};
});

AMD 加载器会使用 Function.prototype.toString() 来解析出所有的 require('') 调用, 然后在内部将上述 define 调用转换成这种形式:

define(['require', 'dependency1', 'dependency2'], function (require) {
    var dependency1 = require('dependency1'),
        dependency2 = require('dependency2');

    return function () {};
});

如此,加载器会异步加载依赖一(dependency1)与依赖二(dependency2),执行这些依赖,然后执行这个工厂函数。

并非所有的浏览器都给出可用的 Function.prototype.toString() 结果。自2011年10月, PS3 和老旧的 Opera Mobile 浏览器的返回结果就不准确。这些浏览器也更可能因网络与设备的限制, 需要编译优化模块代码,所以直接用个懂得如何解析、转换正确的依赖数组的优化工具来打包优化一下就好, 例如 RequireJS 优化工具

因为不支持 toString() 解析的浏览器相当少,用这种语法糖形式来编写你的模块是安全可靠的。 特别是当你喜欢把依赖按照它们的名字排列整齐的时候。

CommonJS 兼容性

虽然这个语法糖形式被称作“简化 CommonJS 封装”,但它并非与 CommonJS 模块 100% 兼容。不过,CommonJS 模块一般认为依赖是同步加载的,完全兼容它的话,在浏览器里就会挂掉啦。

绝大部分 CJS(CommonJS)模块,根据个人经验(不太科学地毛估估)大约有 95%,是与简化 CommonJS 封装完美兼容的。

不兼容的模块,都是那些依赖是动态计算的,调用 require() 的时候不用字符串字面量的,以及其他不像个声明方式调用 require() 的。 所以,下边这种会挂:

// 不好
var mod = require(someCondition ? 'a' : 'b');

// 不好
// 译注:这种应该不会挂,但是不能达到选择加载的效果,会在请求、加载完 a 和 a1 之后,
// 才执行模块代码
if (someCondition) {
    var a = require('a');
} else {
    var a = require('a1');
}

这些使用方式,在 require 回调 中支持, 即 AMD 加载器中提供的全局的

require([moduleName], function() {});

AMD 的执行模式比较向 ECMAScript 和谐版(Harmoney)的模块规范所约定的看齐。 CommonJS 模块规范在 AMD 中的不兼容部分在和谐版(Harmoney)模块也一样不兼容。 AMD 的代码执行逻辑的未来兼容性更好。

冗长与可用性

对 AMD 的批评其一,至少与 CJS 模块规范相比,是它要求一层缩进,以及一个函数封装。

但事实很简单:要用 AMD 就觉得需要多打点字并多缩进一层,其实这一点都没关系。你编程的时间是这么花的:

  • 思考问题
  • 阅读代码

绝大部分编程的时间是花在思考而非敲代码上的。虽然代码一般越短越好,但所能付出的代价终归是有限的, 何况用 AMD 要再打些的字其实也没那么多。

而且大部分 Web 开发者本来就在用匿名函数封装了,目的是避免污染全局变量。看到逻辑代码外边包了匿名函数,其实是很普遍的, 并不会给阅读模块代码带来困扰。

同时,CommonJS 格式还有些潜在代价:

  • 依赖工具的代价
  • 某些边界用例在浏览器中会挂,比如跨域访问
  • 调试更差,此代价随着时间增加,会越来越大

AMD 模块规范需要的工具更少,边界用例问题更少,调试支持也更好。

重要的是:能够真正与其他人分享代码。AMD 规范是达此目标更轻松的方式。

拥有一个可用的,易于调试,并且还在现如今的浏览器中能跑的模块系统,意味着在创造未来的 JavaScript 中最好的模块系统的同时,得到现实世界的体验。

AMD 和它相关的 API 们,已经帮助展示了任何未来 JS 的模块系统都具备的以下特性:

  • 返回一个函数作为模块值,尤其是构造函数,促使更好的 API 设计。Node 有 module.exports 来支持此特性, 但能够用 return function() {} 的话会更简洁。这意味着不需要持有 modulemodule.exports, 并且代码表达也更清晰。
  • 动态代码加载(AMD 模块系统中通过 require[],function(){}) 来实现) 是基本要求。CJS 小组谈到了这个问题,提了一些议案,但它并没有广为接受。Node 并不支持这个需求,转而依赖 require() 的同步行为, 而这在 Web 端是无从实现的。
  • 加载器插件 相当有用。它帮助避免基于回调的编程中常遇到的嵌套缩进的问题。
  • 选择映射某个模块 从另一个地址加载,使得提供模块模拟对象以供测试变得容易。
  • 每个模块最多一个 IO 行为,并且这个 IO 行为应该简单直接。Web 浏览器受不了找个模块还有多次 IO 查找。 这和 Node 里现在的多次寻址是相悖的,并且避免使用一个有 main 属性的 package.json。 用能够很容易地根据项目目录结构映射到某个地址的模块名,一个合理的不需要冗长的配置的约定就够了,不过要允许必要的简单配置。
  • 最好有个“选择加入”调用,用来使旧 JS 代码快速参与新的模块系统之中。

如果一个 JS 模块系统不能搞定以上特性,那么与 AMD 和它相关的 API 们,例如 callback-require加载器插件 和基于路径的模块 ID,相较之下,高下立见。

AMD 使用情况

截至 2011年10月,AMD 已经在 Web 上广为使用:

你可以做什么

如果你是写应用的:

如果你是脚本库开发者:

  • 如果可用,条件调用 define()。妙处在于不依靠 AMD 你仍然可以编写你的库,只要可用的时候参与一下就可以了。 这使得你的模块用户可以:
    • 避免往页面全局变量里头塞东西
    • 代码加载、延迟加载等更加有选择
    • 用现有的 AMD 工具来优化它们的项目
    • 参与到当今浏览器中可用的 JS 模块系统中去

如果你为 JavaScript 加载器、引擎、环境编写代码:

  • 实现 AMD API。有个 讨论列表兼容性测试。通过实现 AMD 模块规范,你可以减少多模块系统的无谓竞争, 帮助为一个 Web 端可用的 JavaScript 模块系统证明。这也能反馈到 ECMAScript 工作组,以求更好的本地模块系统支持。
  • 也要支持 callback-require加载器插件。 加载器插件是个很好的减轻回调、异步风格的代码中常见的嵌套回调症的方式。
comments powered by Disqus

SeaJS 模块预处理

如何正确压缩、打包你的模块们

RequireJS 其一

为何用 Web 模块的方式?

本文讨论异步模块规范(Asynchronous Module Definition,AMD)API,即 RequireJS 所支持的模块 API, 的设计驱使与使用方式。另有页面讨论 Web 模块化的基本方式

模块目的

什么是 JavaScript 模块?它们的目的是什么?

  • 定义:如何把一段代码封装成一个有用的单元,以及如何注册此模块的能力、输出的值
  • 依赖引用:如何引用其它代码单元

现今 Web

(function () {
    var $ = this.jQuery;

    this.myExample = function () {};
}());

现如今 JavaScript 代码段是如何定义的呢?

  • 通过立即执行的工厂函数定义。
  • 使用 HTML script 标签加载模块,通过全局变量来引用依赖。
  • 模块间依赖的声明很弱:开发者需要知道正确的依赖顺序。例如,包含 Backbone 的文件,不能放在 jQuery 标签之前。
  • 优化部署的时候,需要用额外的工具来把一系列 script 标签替换成一个。

这些都会使大型项目变得难以管理,尤其是当脚本们的诸多依赖还可能重叠、嵌套的时候。 手工写 script 标签可不怎么灵活,而且这么做就没法搞按需加载了。

CommonJS

var $ = require('jquery');
exports.myExample = function () {};

最初的 CommonJS 小组 的参与者们决定弄一份于时下的 JavaScript 编程语言有效,但不必束缚于浏览器 JS 环境的限制,的模块规范。开始的愿景是在浏览器里使用一些权宜之计, 并希望能借此影响浏览器厂商,促使它们为这种模块规范的原生支持提供解决方案。权宜之计有:

  • 要么使用一个服务来转译 CJS 模块成浏览器中可用的代码
  • 要么使用 XMLHttpRequest(XHR)以文本形式加载模块,再在浏览器中做文本变换、解析的工作

CJS 模块规范仅允许每文件一个模块,所以为优化、打包,可使用某种“转换格式”将多个模块合并到单个文件。

通过这种方式,CommonJS 小组搞定了依赖引用、如何处理循环依赖,以及如何获得当前模块的某些属性等问题。 但是,他们并没能接纳浏览器环境里不可改变、并且仍将影响模块设计的某些特性:

  • 网络加载
  • 异步继承

这也同时意味着他们为了实现这个规范,将负担更多地放到了 Web 开发者身上,而这些权宜之计也使调试变得更麻烦。 调试 eval 的代码,或者调试多个文件合并之后的单个文件,都有实际使用时的坏处。 这些缺点或许在未来某天会被浏览器调试工具解决掉,但结论仍然是:在最普遍的 JS 环境,浏览器中,使用 CommonJS 模块并不是最好的办法

AMD

define(['jquery'] , function ($) {
    return function () {};
});

AMD 规范的缘起是,我们需要一个比时下那种“写一堆 script 标签、手工按序指明依赖”要好,并且容易在浏览器中直接使用的模块格式。 某种不需要服务端工具配合,又能够易于调试的模块格式。它超脱于 Dojo 使用 XHR+eval 的现实经验, 并且要规避 Dojo 的方式的缺点。

它比时下 Web 开发中“全局变量+script 标签”的方式要好,因为:

  • 应用 CommonJS 实践,使用字符串 ID 来声明依赖。使得依赖声明清晰,并且避免了使用全局变量。
  • 模块 ID 可以被映射到不同的路径。从而允许切换模块实现。这对创建单元测试模型很有帮助。 在上例中,代码实际期待的不过是某个实现了 jQuery API 与行为的模块而已。并不是非要 jQuery 本身不可。
  • 封装了模块定义。使你能够避免污染全局命名空间。
  • 清楚地定义模块输出。可以用 return value;,也可以用 CommonJS 的 exports 范式。 后者在循环依赖的时候很有用。

它是 CommonJS 模块规范的改善,因为:

  • 它在浏览器里跑得更好,它的坑是最少的。其他的解决方案,都有调试、跨域、CDN 使用,file:// 协议以及依赖服务端工具等问题。
  • 定义了将多个模块包含进单个文件的方式。在 CommonJS 的条款里,有个“传输规范”来做这个事情, 但 CommonJS 小组还没就此达成一致
  • 允许将函数作为返回值。这在模块返回值是构造函数的时候尤其有用。在 CommonJS 中就有点尴尬了, 永远要通过给 exports 对象设置属性来输出。Node 支持了 module.exports = function() {}, 不过这还没在 CommonJS 规范里面。

模块定义

使用 JavaScript 的函数进行封装,已经有 文档 约定了:

(function () {
   this.myGlobal = function () {};
}());

这种模块依赖于给全局对象附加属性来输出模块值,并且用这种模型很难声明模块依赖。 预设是,模块的依赖在执行此函数之前就已经是立即可用的。这限制了加载依赖的策略。

AMD 通过如下手段解决了这些问题:

  • 调用 define() 来注册工厂函数,而不是立即执行该函数。
  • 以字符串数组的形式将依赖传递进来,而不是直接从全局对象上取。
  • 在所有依赖都被加载、执行完毕之后,才执行工厂函数。
  • 将依赖的模块作为执行工厂函数的参数。
//Calling define with a dependency array and a factory function
define(['dep1', 'dep2'], function (dep1, dep2) {

    //Define the module value by returning a value.
    return function () {};
});

命名的模块

请注意,上例中的模块并没有给自己取名。这使得这个模块可以随便移动。它允许开发者将模块放到不同的目录, 从而管它们叫不同的名字。AMD 加载器会根据它被其他脚本引用的方式来给这个模块标记 ID。

但是,为了提高性能,打包多个模块的工具需要有个给单个文件中的每个模块命名的方式。对此需求,AMD 允许以字符串作为 define() 的第一个参数:

//Calling define with module ID, dependency array, and factory function
define('myModule', ['dep1', 'dep2'], function (dep1, dep2) {

    //Define the module value by returning a value.
    return function () {};
});

你应该避免自行命名模块,并且在开发时保持每文件一个模块。只不过,在注重性能的阶段, 得有个模块规范以在编译后的资源中命名模块。

语法糖

上面的 AMD 例子在所有浏览器中都能跑。但是,有搞错模块数组与参数顺序的风险,尤其是当你的模块依赖了好多的时候:

define([ "require", "jquery", "blade/object", "blade/fn", "rdapi",
         "oauth", "blade/jig", "blade/url", "dispatch", "accounts",
         "storage", "services", "widgets/AccountPanel", "widgets/TabButton",
         "widgets/AddAccount", "less", "osTheme", "jquery-ui-1.8.7.min",
         "jquery.textOverflow"],
function (require,   $,        object,         fn,         rdapi,
          oauth,   jig,         url,         dispatch,   accounts,
          storage,   services,   AccountPanel,           TabButton,
          AddAccount,           less,   osTheme) {

});

简而为之,同时也让封装 CommonJS 模块更容易,还支持这种形式的 define() (有时这种封装还被称作“简化 CommonJS 封装”):

define(function (require) {
    var dependency1 = require('dependency1'),
        dependency2 = require('dependency2');

    return function () {};
});

AMD 加载器会使用 Function.prototype.toString() 来解析出所有的 require('') 调用, 然后在内部将上述 define 调用转换成这种形式:

define(['require', 'dependency1', 'dependency2'], function (require) {
    var dependency1 = require('dependency1'),
        dependency2 = require('dependency2');

    return function () {};
});

如此,加载器会异步加载依赖一(dependency1)与依赖二(dependency2),执行这些依赖,然后执行这个工厂函数。

并非所有的浏览器都给出可用的 Function.prototype.toString() 结果。自2011年10月, PS3 和老旧的 Opera Mobile 浏览器的返回结果就不准确。这些浏览器也更可能因网络与设备的限制, 需要编译优化模块代码,所以直接用个懂得如何解析、转换正确的依赖数组的优化工具来打包优化一下就好, 例如 RequireJS 优化工具

因为不支持 toString() 解析的浏览器相当少,用这种语法糖形式来编写你的模块是安全可靠的。 特别是当你喜欢把依赖按照它们的名字排列整齐的时候。

CommonJS 兼容性

虽然这个语法糖形式被称作“简化 CommonJS 封装”,但它并非与 CommonJS 模块 100% 兼容。不过,CommonJS 模块一般认为依赖是同步加载的,完全兼容它的话,在浏览器里就会挂掉啦。

绝大部分 CJS(CommonJS)模块,根据个人经验(不太科学地毛估估)大约有 95%,是与简化 CommonJS 封装完美兼容的。

不兼容的模块,都是那些依赖是动态计算的,调用 require() 的时候不用字符串字面量的,以及其他不像个声明方式调用 require() 的。 所以,下边这种会挂:

// 不好
var mod = require(someCondition ? 'a' : 'b');

// 不好
// 译注:这种应该不会挂,但是不能达到选择加载的效果,会在请求、加载完 a 和 a1 之后,
// 才执行模块代码
if (someCondition) {
    var a = require('a');
} else {
    var a = require('a1');
}

这些使用方式,在 require 回调 中支持, 即 AMD 加载器中提供的全局的

require([moduleName], function() {});

AMD 的执行模式比较向 ECMAScript 和谐版(Harmoney)的模块规范所约定的看齐。 CommonJS 模块规范在 AMD 中的不兼容部分在和谐版(Harmoney)模块也一样不兼容。 AMD 的代码执行逻辑的未来兼容性更好。

冗长与可用性

对 AMD 的批评其一,至少与 CJS 模块规范相比,是它要求一层缩进,以及一个函数封装。

但事实很简单:要用 AMD 就觉得需要多打点字并多缩进一层,其实这一点都没关系。你编程的时间是这么花的:

  • 思考问题
  • 阅读代码

绝大部分编程的时间是花在思考而非敲代码上的。虽然代码一般越短越好,但所能付出的代价终归是有限的, 何况用 AMD 要再打些的字其实也没那么多。

而且大部分 Web 开发者本来就在用匿名函数封装了,目的是避免污染全局变量。看到逻辑代码外边包了匿名函数,其实是很普遍的, 并不会给阅读模块代码带来困扰。

同时,CommonJS 格式还有些潜在代价:

  • 依赖工具的代价
  • 某些边界用例在浏览器中会挂,比如跨域访问
  • 调试更差,此代价随着时间增加,会越来越大

AMD 模块规范需要的工具更少,边界用例问题更少,调试支持也更好。

重要的是:能够真正与其他人分享代码。AMD 规范是达此目标更轻松的方式。

拥有一个可用的,易于调试,并且还在现如今的浏览器中能跑的模块系统,意味着在创造未来的 JavaScript 中最好的模块系统的同时,得到现实世界的体验。

AMD 和它相关的 API 们,已经帮助展示了任何未来 JS 的模块系统都具备的以下特性:

  • 返回一个函数作为模块值,尤其是构造函数,促使更好的 API 设计。Node 有 module.exports 来支持此特性, 但能够用 return function() {} 的话会更简洁。这意味着不需要持有 modulemodule.exports, 并且代码表达也更清晰。
  • 动态代码加载(AMD 模块系统中通过 require[],function(){}) 来实现) 是基本要求。CJS 小组谈到了这个问题,提了一些议案,但它并没有广为接受。Node 并不支持这个需求,转而依赖 require() 的同步行为, 而这在 Web 端是无从实现的。
  • 加载器插件 相当有用。它帮助避免基于回调的编程中常遇到的嵌套缩进的问题。
  • 选择映射某个模块 从另一个地址加载,使得提供模块模拟对象以供测试变得容易。
  • 每个模块最多一个 IO 行为,并且这个 IO 行为应该简单直接。Web 浏览器受不了找个模块还有多次 IO 查找。 这和 Node 里现在的多次寻址是相悖的,并且避免使用一个有 main 属性的 package.json。 用能够很容易地根据项目目录结构映射到某个地址的模块名,一个合理的不需要冗长的配置的约定就够了,不过要允许必要的简单配置。
  • 最好有个“选择加入”调用,用来使旧 JS 代码快速参与新的模块系统之中。

如果一个 JS 模块系统不能搞定以上特性,那么与 AMD 和它相关的 API 们,例如 callback-require加载器插件 和基于路径的模块 ID,相较之下,高下立见。

AMD 使用情况

截至 2011年10月,AMD 已经在 Web 上广为使用:

你可以做什么

如果你是写应用的:

如果你是脚本库开发者:

  • 如果可用,条件调用 define()。妙处在于不依靠 AMD 你仍然可以编写你的库,只要可用的时候参与一下就可以了。 这使得你的模块用户可以:
    • 避免往页面全局变量里头塞东西
    • 代码加载、延迟加载等更加有选择
    • 用现有的 AMD 工具来优化它们的项目
    • 参与到当今浏览器中可用的 JS 模块系统中去

如果你为 JavaScript 加载器、引擎、环境编写代码:

  • 实现 AMD API。有个 讨论列表兼容性测试。通过实现 AMD 模块规范,你可以减少多模块系统的无谓竞争, 帮助为一个 Web 端可用的 JavaScript 模块系统证明。这也能反馈到 ECMAScript 工作组,以求更好的本地模块系统支持。
  • 也要支持 callback-require加载器插件。 加载器插件是个很好的减轻回调、异步风格的代码中常见的嵌套回调症的方式。

RequireJS 其二

为何用 AMD 规范?

本文讨论异步模块规范(Asynchronous Module Definition,AMD)API,即 RequireJS 所支持的模块 API, 的设计驱使与使用方式。另有页面讨论 Web 模块化的基本方式

模块目的

什么是 JavaScript 模块?它们的目的是什么?

  • 定义:如何把一段代码封装成一个有用的单元,以及如何注册此模块的能力、输出的值
  • 依赖引用:如何引用其它代码单元

现今 Web

(function () {
    var $ = this.jQuery;

    this.myExample = function () {};
}());

现如今 JavaScript 代码段是如何定义的呢?

  • 通过立即执行的工厂函数定义。
  • 使用 HTML script 标签加载模块,通过全局变量来引用依赖。
  • 模块间依赖的声明很弱:开发者需要知道正确的依赖顺序。例如,包含 Backbone 的文件,不能放在 jQuery 标签之前。
  • 优化部署的时候,需要用额外的工具来把一系列 script 标签替换成一个。

这些都会使大型项目变得难以管理,尤其是当脚本们的诸多依赖还可能重叠、嵌套的时候。 手工写 script 标签可不怎么灵活,而且这么做就没法搞按需加载了。

CommonJS

var $ = require('jquery');
exports.myExample = function () {};

最初的 CommonJS 小组 的参与者们决定弄一份于时下的 JavaScript 编程语言有效,但不必束缚于浏览器 JS 环境的限制,的模块规范。开始的愿景是在浏览器里使用一些权宜之计, 并希望能借此影响浏览器厂商,促使它们为这种模块规范的原生支持提供解决方案。权宜之计有:

  • 要么使用一个服务来转译 CJS 模块成浏览器中可用的代码
  • 要么使用 XMLHttpRequest(XHR)以文本形式加载模块,再在浏览器中做文本变换、解析的工作

CJS 模块规范仅允许每文件一个模块,所以为优化、打包,可使用某种“转换格式”将多个模块合并到单个文件。

通过这种方式,CommonJS 小组搞定了依赖引用、如何处理循环依赖,以及如何获得当前模块的某些属性等问题。 但是,他们并没能接纳浏览器环境里不可改变、并且仍将影响模块设计的某些特性:

  • 网络加载
  • 异步继承

这也同时意味着他们为了实现这个规范,将负担更多地放到了 Web 开发者身上,而这些权宜之计也使调试变得更麻烦。 调试 eval 的代码,或者调试多个文件合并之后的单个文件,都有实际使用时的坏处。 这些缺点或许在未来某天会被浏览器调试工具解决掉,但结论仍然是:在最普遍的 JS 环境,浏览器中,使用 CommonJS 模块并不是最好的办法

AMD

define(['jquery'] , function ($) {
    return function () {};
});

AMD 规范的缘起是,我们需要一个比时下那种“写一堆 script 标签、手工按序指明依赖”要好,并且容易在浏览器中直接使用的模块格式。 某种不需要服务端工具配合,又能够易于调试的模块格式。它超脱于 Dojo 使用 XHR+eval 的现实经验, 并且要规避 Dojo 的方式的缺点。

它比时下 Web 开发中“全局变量+script 标签”的方式要好,因为:

  • 应用 CommonJS 实践,使用字符串 ID 来声明依赖。使得依赖声明清晰,并且避免了使用全局变量。
  • 模块 ID 可以被映射到不同的路径。从而允许切换模块实现。这对创建单元测试模型很有帮助。 在上例中,代码实际期待的不过是某个实现了 jQuery API 与行为的模块而已。并不是非要 jQuery 本身不可。
  • 封装了模块定义。使你能够避免污染全局命名空间。
  • 清楚地定义模块输出。可以用 return value;,也可以用 CommonJS 的 exports 范式。 后者在循环依赖的时候很有用。

它是 CommonJS 模块规范的改善,因为:

  • 它在浏览器里跑得更好,它的坑是最少的。其他的解决方案,都有调试、跨域、CDN 使用,file:// 协议以及依赖服务端工具等问题。
  • 定义了将多个模块包含进单个文件的方式。在 CommonJS 的条款里,有个“传输规范”来做这个事情, 但 CommonJS 小组还没就此达成一致
  • 允许将函数作为返回值。这在模块返回值是构造函数的时候尤其有用。在 CommonJS 中就有点尴尬了, 永远要通过给 exports 对象设置属性来输出。Node 支持了 module.exports = function() {}, 不过这还没在 CommonJS 规范里面。

模块定义

使用 JavaScript 的函数进行封装,已经有 文档 约定了:

(function () {
   this.myGlobal = function () {};
}());

这种模块依赖于给全局对象附加属性来输出模块值,并且用这种模型很难声明模块依赖。 预设是,模块的依赖在执行此函数之前就已经是立即可用的。这限制了加载依赖的策略。

AMD 通过如下手段解决了这些问题:

  • 调用 define() 来注册工厂函数,而不是立即执行该函数。
  • 以字符串数组的形式将依赖传递进来,而不是直接从全局对象上取。
  • 在所有依赖都被加载、执行完毕之后,才执行工厂函数。
  • 将依赖的模块作为执行工厂函数的参数。
//Calling define with a dependency array and a factory function
define(['dep1', 'dep2'], function (dep1, dep2) {

    //Define the module value by returning a value.
    return function () {};
});

命名的模块

请注意,上例中的模块并没有给自己取名。这使得这个模块可以随便移动。它允许开发者将模块放到不同的目录, 从而管它们叫不同的名字。AMD 加载器会根据它被其他脚本引用的方式来给这个模块标记 ID。

但是,为了提高性能,打包多个模块的工具需要有个给单个文件中的每个模块命名的方式。对此需求,AMD 允许以字符串作为 define() 的第一个参数:

//Calling define with module ID, dependency array, and factory function
define('myModule', ['dep1', 'dep2'], function (dep1, dep2) {

    //Define the module value by returning a value.
    return function () {};
});

你应该避免自行命名模块,并且在开发时保持每文件一个模块。只不过,在注重性能的阶段, 得有个模块规范以在编译后的资源中命名模块。

语法糖

上面的 AMD 例子在所有浏览器中都能跑。但是,有搞错模块数组与参数顺序的风险,尤其是当你的模块依赖了好多的时候:

define([ "require", "jquery", "blade/object", "blade/fn", "rdapi",
         "oauth", "blade/jig", "blade/url", "dispatch", "accounts",
         "storage", "services", "widgets/AccountPanel", "widgets/TabButton",
         "widgets/AddAccount", "less", "osTheme", "jquery-ui-1.8.7.min",
         "jquery.textOverflow"],
function (require,   $,        object,         fn,         rdapi,
          oauth,   jig,         url,         dispatch,   accounts,
          storage,   services,   AccountPanel,           TabButton,
          AddAccount,           less,   osTheme) {

});

简而为之,同时也让封装 CommonJS 模块更容易,还支持这种形式的 define() (有时这种封装还被称作“简化 CommonJS 封装”):

define(function (require) {
    var dependency1 = require('dependency1'),
        dependency2 = require('dependency2');

    return function () {};
});

AMD 加载器会使用 Function.prototype.toString() 来解析出所有的 require('') 调用, 然后在内部将上述 define 调用转换成这种形式:

define(['require', 'dependency1', 'dependency2'], function (require) {
    var dependency1 = require('dependency1'),
        dependency2 = require('dependency2');

    return function () {};
});

如此,加载器会异步加载依赖一(dependency1)与依赖二(dependency2),执行这些依赖,然后执行这个工厂函数。

并非所有的浏览器都给出可用的 Function.prototype.toString() 结果。自2011年10月, PS3 和老旧的 Opera Mobile 浏览器的返回结果就不准确。这些浏览器也更可能因网络与设备的限制, 需要编译优化模块代码,所以直接用个懂得如何解析、转换正确的依赖数组的优化工具来打包优化一下就好, 例如 RequireJS 优化工具

因为不支持 toString() 解析的浏览器相当少,用这种语法糖形式来编写你的模块是安全可靠的。 特别是当你喜欢把依赖按照它们的名字排列整齐的时候。

CommonJS 兼容性

虽然这个语法糖形式被称作“简化 CommonJS 封装”,但它并非与 CommonJS 模块 100% 兼容。不过,CommonJS 模块一般认为依赖是同步加载的,完全兼容它的话,在浏览器里就会挂掉啦。

绝大部分 CJS(CommonJS)模块,根据个人经验(不太科学地毛估估)大约有 95%,是与简化 CommonJS 封装完美兼容的。

不兼容的模块,都是那些依赖是动态计算的,调用 require() 的时候不用字符串字面量的,以及其他不像个声明方式调用 require() 的。 所以,下边这种会挂:

// 不好
var mod = require(someCondition ? 'a' : 'b');

// 不好
// 译注:这种应该不会挂,但是不能达到选择加载的效果,会在请求、加载完 a 和 a1 之后,
// 才执行模块代码
if (someCondition) {
    var a = require('a');
} else {
    var a = require('a1');
}

这些使用方式,在 require 回调 中支持, 即 AMD 加载器中提供的全局的

require([moduleName], function() {});

AMD 的执行模式比较向 ECMAScript 和谐版(Harmoney)的模块规范所约定的看齐。 CommonJS 模块规范在 AMD 中的不兼容部分在和谐版(Harmoney)模块也一样不兼容。 AMD 的代码执行逻辑的未来兼容性更好。

冗长与可用性

对 AMD 的批评其一,至少与 CJS 模块规范相比,是它要求一层缩进,以及一个函数封装。

但事实很简单:要用 AMD 就觉得需要多打点字并多缩进一层,其实这一点都没关系。你编程的时间是这么花的:

  • 思考问题
  • 阅读代码

绝大部分编程的时间是花在思考而非敲代码上的。虽然代码一般越短越好,但所能付出的代价终归是有限的, 何况用 AMD 要再打些的字其实也没那么多。

而且大部分 Web 开发者本来就在用匿名函数封装了,目的是避免污染全局变量。看到逻辑代码外边包了匿名函数,其实是很普遍的, 并不会给阅读模块代码带来困扰。

同时,CommonJS 格式还有些潜在代价:

  • 依赖工具的代价
  • 某些边界用例在浏览器中会挂,比如跨域访问
  • 调试更差,此代价随着时间增加,会越来越大

AMD 模块规范需要的工具更少,边界用例问题更少,调试支持也更好。

重要的是:能够真正与其他人分享代码。AMD 规范是达此目标更轻松的方式。

拥有一个可用的,易于调试,并且还在现如今的浏览器中能跑的模块系统,意味着在创造未来的 JavaScript 中最好的模块系统的同时,得到现实世界的体验。

AMD 和它相关的 API 们,已经帮助展示了任何未来 JS 的模块系统都具备的以下特性:

  • 返回一个函数作为模块值,尤其是构造函数,促使更好的 API 设计。Node 有 module.exports 来支持此特性, 但能够用 return function() {} 的话会更简洁。这意味着不需要持有 modulemodule.exports, 并且代码表达也更清晰。
  • 动态代码加载(AMD 模块系统中通过 require[],function(){}) 来实现) 是基本要求。CJS 小组谈到了这个问题,提了一些议案,但它并没有广为接受。Node 并不支持这个需求,转而依赖 require() 的同步行为, 而这在 Web 端是无从实现的。
  • 加载器插件 相当有用。它帮助避免基于回调的编程中常遇到的嵌套缩进的问题。
  • 选择映射某个模块 从另一个地址加载,使得提供模块模拟对象以供测试变得容易。
  • 每个模块最多一个 IO 行为,并且这个 IO 行为应该简单直接。Web 浏览器受不了找个模块还有多次 IO 查找。 这和 Node 里现在的多次寻址是相悖的,并且避免使用一个有 main 属性的 package.json。 用能够很容易地根据项目目录结构映射到某个地址的模块名,一个合理的不需要冗长的配置的约定就够了,不过要允许必要的简单配置。
  • 最好有个“选择加入”调用,用来使旧 JS 代码快速参与新的模块系统之中。

如果一个 JS 模块系统不能搞定以上特性,那么与 AMD 和它相关的 API 们,例如 callback-require加载器插件 和基于路径的模块 ID,相较之下,高下立见。

AMD 使用情况

截至 2011年10月,AMD 已经在 Web 上广为使用:

你可以做什么

如果你是写应用的:

如果你是脚本库开发者:

  • 如果可用,条件调用 define()。妙处在于不依靠 AMD 你仍然可以编写你的库,只要可用的时候参与一下就可以了。 这使得你的模块用户可以:
    • 避免往页面全局变量里头塞东西
    • 代码加载、延迟加载等更加有选择
    • 用现有的 AMD 工具来优化它们的项目
    • 参与到当今浏览器中可用的 JS 模块系统中去

如果你为 JavaScript 加载器、引擎、环境编写代码:

  • 实现 AMD API。有个 讨论列表兼容性测试。通过实现 AMD 模块规范,你可以减少多模块系统的无谓竞争, 帮助为一个 Web 端可用的 JavaScript 模块系统证明。这也能反馈到 ECMAScript 工作组,以求更好的本地模块系统支持。
  • 也要支持 callback-require加载器插件。 加载器插件是个很好的减轻回调、异步风格的代码中常见的嵌套回调症的方式。
comments powered by Disqus
comments powered by Disqus
comments powered by Disqus