为何选择 AMD?

本页讨论 JavaScript 模块的 异步模块定义 (AMD) API 的设计驱动力和使用,RequireJS 支持该模块 API。 另一页讨论了 Web 上模块的一般方法

模块用途 § 1

什么是 JavaScript 模块?它们的用途是什么?

  • 定义:如何将一段代码封装成一个有用的单元,以及如何注册其功能/导出模块的值。
  • 依赖关系引用:如何引用其他代码单元。

现今的 Web § 2

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

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

现今如何定义 JavaScript 代码段?

  • 通过立即执行的工厂函数定义。
  • 对依赖项的引用是通过全局变量名完成的,这些全局变量名是通过 HTML 脚本标签加载的。
  • 依赖关系的声明非常薄弱:开发人员需要知道正确的依赖顺序。例如,包含 Backbone 的文件不能出现在 jQuery 标签之前。
  • 它需要额外的工具来将一组脚本标签替换为一个标签,以便进行优化部署。

这在大型项目中可能难以管理,特别是当脚本开始具有许多可能重叠和嵌套的依赖项时。手动编写脚本标签的可扩展性不高,并且无法按需加载脚本。

CommonJS § 3

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

最初的 CommonJS (CJS) 列表 参与者决定制定一种模块格式,该格式适用于当今的 JavaScript 语言,但不一定受限于浏览器 JS 环境的限制。他们希望在浏览器中使用一些权宜之计,并希望影响浏览器制造商构建解决方案,使他们的模块格式能够在本地更好地工作。权宜之计

  • 要么使用服务器将 CJS 模块转换为浏览器可用的内容。
  • 要么使用 XMLHttpRequest (XHR) 加载模块的文本并在浏览器中进行文本转换/解析。

CJS 模块格式只允许每个文件一个模块,因此“传输格式”将用于将多个模块捆绑在一个文件中,以实现优化/捆绑目的。

通过这种方法,CommonJS 小组能够解决依赖关系引用以及如何处理循环依赖关系,以及如何获取有关当前模块的一些属性。但是,他们并没有完全接受浏览器环境中的一些无法改变但仍然会影响模块设计的东西

  • 网络加载
  • 固有的异步性

这也意味着他们给 Web 开发人员带来了更多实现格式的负担,而且权宜之计意味着调试更糟。基于 eval 的调试或调试连接到一个文件中的多个文件存在实际的弱点。这些弱点将来可能会在浏览器工具中得到解决,但最终结果是:在最常见的 JS 环境(浏览器)中使用 CommonJS 模块,在今天并不是最佳选择。

AMD § 4

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

AMD 格式的出现是因为想要一种比现今的“编写一堆带有必须手动排序的隐式依赖项的脚本标签”更好的模块格式,并且可以直接在浏览器中轻松使用。具有良好调试特性的东西,不需要特定于服务器的工具即可开始使用。它源于 Dojo 在使用 XHR+eval 方面的实际经验,并希望避免其在未来的弱点。

它是对 Web 当前“全局变量和脚本标签”的改进,因为

  • 使用 CommonJS 的字符串 ID 来表示依赖项。清晰地声明依赖关系并避免使用全局变量。
  • ID 可以映射到不同的路径。这允许交换实现。这对于为单元测试创建模拟非常有用。对于上面的代码示例,代码只期望实现 jQuery API 和行为的东西。它不一定是 jQuery。
  • 封装模块定义。为您提供避免污染全局命名空间的工具。
  • 定义模块值的清晰路径。使用“return value;”或 CommonJS“exports”习惯用法,这对于循环依赖关系很有用。

它是对 CommonJS 模块的改进,因为

  • 它在浏览器中运行得更好,并且出错的可能性最小。其他方法在调试、跨域/CDN 使用、file:// 使用以及对特定于服务器的工具的需求方面存在问题。
  • 定义了一种将多个模块包含在一个文件中的方法。用 CommonJS 的术语来说,这个术语是“传输格式”,并且该小组尚未就传输格式达成一致。
  • 允许将函数设置为返回值。这对构造函数非常有用。在 CommonJS 中,这比较麻烦,总是必须在 exports 对象上设置一个属性。Node 支持 module.exports = function () {},但这不属于 CommonJS 规范。

模块定义 § 5

使用 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 () {};
});

命名模块 § 6

请注意,上面的模块没有为自己声明名称。这就是使模块非常便携的原因。它允许开发人员将模块放置在不同的路径中,以赋予它不同的 ID/名称。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 () {};
});

您应该避免自己命名模块,并且在开发时只在一个文件中放置一个模块。但是,对于工具和性能,模块解决方案需要一种在构建资源中标识模块的方法。

语法糖 § 7

上面的 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 月,PS 3 和旧版 Opera Mobile 浏览器不支持。这些浏览器更有可能需要对模块进行优化构建以适应网络/设备限制,因此只需使用知道如何将这些文件转换为规范化依赖数组形式的优化器进行构建,例如 RequireJS 优化器

由于无法支持此 toString() 扫描的浏览器数量非常少,因此对所有模块使用这种语法糖形式是安全的,特别是如果您希望将依赖项名称与将保存其模块值的变量对齐。

CommonJS 兼容性 § 8

尽管这种语法糖形式被称为“简化的 CommonJS 包装”,但它与 CommonJS 模块并非 100% 兼容。但是,不支持的情况在浏览器中也可能会中断,因为它们通常假设同步加载依赖项。

根据我(完全不科学的)个人经验,大多数 CJS 模块(约 95%)都与简化的 CommonJS 包装完全兼容。

中断的模块是那些对依赖项进行动态计算的模块,任何没有对 require() 调用使用字符串文字的模块,以及任何看起来不像声明性 require() 调用的模块。所以像这样的事情会失败

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

//BAD
if (someCondition) {
    var a = require('a');
} else {
    var a = require('a1');
}

这些情况由 回调-require 处理,require([moduleName], function (){}) 通常出现在 AMD 加载器中。

AMD 执行模型更符合 ECMAScript Harmony 模块的规范方式。在 AMD 包装器中不起作用的 CommonJS 模块也不能作为 Harmony 模块工作。AMD 的代码执行行为更具未来兼容性。

冗长度与实用性

对 AMD 的批评之一(至少与 CJS 模块相比)是它需要一定级别的缩进和函数包装。

但事实是:使用 AMD 所谓的额外输入和缩进级别无关紧要。以下是您在编码时的时间分配

  • 思考问题。
  • 阅读代码。

您编码的时间主要花在思考上,而不是打字上。虽然通常情况下代码越少越好,但这种方法的收益是有限的,而且 AMD 中的额外输入并不多。

大多数 Web 开发人员都会使用函数包装器,以避免使用全局变量污染页面。看到用函数包装的功能是非常常见的,不会增加模块的阅读成本。

CommonJS 格式也存在隐藏成本

  • 工具依赖成本
  • 在浏览器中失效的边缘情况,例如跨域访问
  • 更糟糕的调试,随着时间的推移,成本会不断增加

AMD 模块需要的工具更少,边缘情况更少,调试支持也更好。

重要的是:能够与他人真正共享代码。AMD 是实现这一目标的最低能耗途径。

拥有一个可以在当今浏览器中运行的、易于调试的模块系统,意味着在为未来构建最佳 JavaScript 模块系统方面获得实际经验。

AMD 及其相关 API 已经为任何未来的 JS 模块系统展示了以下内容

  • 返回一个函数作为模块值,特别是一个构造函数,可以带来更好的 API 设计。Node 有 module.exports 来实现这一点,但是能够使用“return function (){}”要简洁得多。这意味着不必获取“module”的句柄来执行 module.exports,而且代码表达更清晰。
  • 动态代码加载(在 AMD 系统中通过 require([], function (){}) 完成)是一项基本要求。CJS 讨论过它,也有一些提案,但没有被完全接受。Node 对此需求没有任何支持,而是依赖于 require('') 的同步行为,而这种行为无法移植到 Web 上。
  • 加载器插件 非常有用。它有助于避免基于回调的编程中常见的嵌套大括号缩进。
  • 选择性地将一个模块映射到另一个位置加载,可以轻松地为测试提供模拟对象。
  • 每个模块最多只能有一个 IO 操作,而且应该很简单。Web 浏览器无法容忍多次 IO 查找来查找模块。这与 Node 现在执行的多次路径查找相矛盾,并且避免了使用 package.json“main”属性。只需使用根据项目位置轻松映射到一个位置的模块名称,使用合理的默认约定,该约定不需要冗长的配置,但在需要时允许进行简单的配置。
  • 最好有一个“选择加入”调用,以便旧的 JS 代码可以参与到新系统中。

如果一个 JS 模块系统无法提供上述功能,那么与 AMD 及其围绕 回调-require加载器插件 和基于路径的模块 ID 的相关 API 相比,它就处于明显的劣势。

现今使用的 AMD § 9

截至 2011 年 10 月中旬,AMD 已经在网络上得到了很好的采用

您可以做什么 § 10

如果您编写应用程序

如果您是脚本/库作者:

  • 如果可用,可以选择调用 define()。好处是您仍然可以在不依赖 AMD 的情况下编写库,只需在可用时参与即可。这使得您的模块使用者可以
    • 避免在页面中转储全局变量
    • 使用更多代码加载选项,延迟加载
    • 使用现有的 AMD 工具来优化他们的项目
    • 参与到一个可行的浏览器端 JS 模块系统中。

如果您为 JavaScript 编写代码加载器/引擎/环境

  • 实现 AMD API。这里有一个 讨论列表兼容性测试。通过实现 AMD,您将减少多模块系统的样板代码,并帮助在 Web 上验证一个可行的 JavaScript 模块系统。这可以反馈到 ECMAScript 进程中,以构建更好的原生模块支持。
  • 还支持 回调-require加载器插件。加载器插件是减少回调/异步风格代码中常见的嵌套回调综合症的好方法。