为什么选择AMD?

本页讨论JavaScript模块的异步模块定义(AMD)API的设计力量和用法 ,该模块是RequireJS支持的模块API。

模块用途§ 1

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

  • 定义: 如何将一段代码封装到一个有用的单元中,以及如何注册其函数/为模块导出值。
  • 依赖引用: 如何引用其他代码单元。

今日网络§ 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开发人员实施该格式带来了更多的负担,而权宜之计意味着调试更糟。基于评估的调试或调试串联到一个文件中的多个文件存在实际缺陷。这些弱点可能会在将来的某个时候用浏览器工具解决,但最终结果是:在当今最不流行的JS环境(浏览器)中使用CommonJS模块。

AMD§ 4

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

AMD格式源于想要一种模块格式,这种模块格式要优于当今的“编写一堆具有手动排序的隐式依赖项的脚本标签"和易于直接在浏览器中使用的模块格式。具有良好调试特性的东西,不需要特定于服务器的工具即可上手。它源于Dojo使用XHR + eval的实际经验,并希望避免将来的缺点。

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

  • 使用字符串ID的CommonJS做法进行依赖。明确声明依赖项,并避免使用全局变量。
  • ID可以映射到不同的路径。这允许换出实施。这对于创建用于单元测试的模拟非常有用。对于上面的代码示例,代码仅期望实现jQuery API和行为的内容。它不必是jQuery。
  • 封装模块定义。为您提供避免污染全局名称空间的工具。
  • 清除定义模块值的路径。使用“返回值";或CommonJS"exports"惯用语,这对循环依赖很有用。

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

  • 它在浏览器中效果更好,它的陷阱最少。其他方法在调试,跨域/ CDN使用,file://使用以及对特定于服务器的工具的需求方面存在问题。
  • 定义一种在一个文件中包含多个模块的方法。用CommonJS术语来说,此术语是“传输格式",并且该组尚未就传输格式达成共识。
  • 允许将函数设置为返回值。这对于构造函数非常有用。在CommonJS中,这很尴尬,总是必须在exports对象上设置一个属性。Node支持module.exports = function(){},但这不是CommonJS规范的一部分。

模块定义 § 5

使用JavaScript函数进行封装已记录为模块模式:

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

这种类型的模块依赖于将属性附加到全局对象以导出模块值,并且很难用此模型声明依赖项。假定该函数执行时依赖项立即可用。这限制了依赖项的加载策略。

AMD通过以下方式解决了这些问题:

  • 通过调用define()来注册工厂函数,而不是立即执行它。
  • 将依赖项作为字符串值数组传递,不要获取全局变量。
  • 仅在加载并执行了所有依赖项后,才执行工厂函数。
  • 将相关模块作为参数传递给工厂函数。
//使用依赖数组和工厂函数调用define
define(['dep1', 'dep2'], function (dep1, dep2) {

    //通过返回值定义模块值。
    return function () {};
});

命名模块 § 6

请注意,上面的模块没有为其本身声明名称。这就是使模块非常轻便的原因。它允许开发人员将模块放置在其他路径中,以为其指定不同的ID /名称。AMD加载程序将根据其他脚本如何引用模块来为其提供ID。

但是,将多个模块组合在一起以提高性能的工具需要一种为优化文件中的每个模块命名的方法。为此,AMD允许将字符串作为define()的第一个参数:

//使用模块ID、依赖项数组和工厂函数调用define
define('myModule', ['dep1', 'dep2'], function (dep1, dep2) {

    //通过返回值定义模块值。
    return function () {};
});

您应该避免自己命名模块,而在开发时只能在文件中放置一个模块。但是,为了实现工具和性能,模块解决方案需要一种方法来标识内置资源中的模块。

Sugar § 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包装",它也不是100%与CommonJS模块兼容。但是,不支持的情况无论如何都可能会在浏览器中中断,因为它们通常假定同步加载依赖项。

大多数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([moduleName], function (){})通常存在于AMD装载机。

AMD执行模型与指定ECMAScript Harmony模块的方式更好地保持一致。在AMD包装器中无法使用的CommonJS模块也将无法作为Harmony模块使用。AMD的代码执行行为在将来更加兼容。

详尽与实用

至少与CJS模块相比,对AMD的批评之一是它要求一定程度的缩进和函数包装。

但这是一个简单的事实:可以看到的额外打字和使用AMD的缩进程度并不重要。这是您编码时花费的时间:

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

您的编码时间主要用于思考,而不是打字。虽然通常最好使用较少的单词,但这种方法的回报是有限的,AMD中的额外键入内容并不多。

大多数Web开发人员无论如何都使用函数包装器,以避免用全局变量污染页面。看到围绕函数的函数是很常见的现象,并且不会增加模块的读取成本。

CommonJS格式还存在一些隐藏成本:

  • 工具依赖成本
  • 跨浏览器中断的边缘情况,例如跨域访问
  • 较差的调试,随着时间的推移,成本不断增加

AMD模块需要更少的工具,更少的边缘问题,以及更好的调试支持。

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

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

AMD及其相关的API已帮助显示任何将来的JS模块系统的以下内容:

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

如果JS模块系统无法提供上述函数,则与AMD及其相关的关于callback-require,loader插件和基于路径的模块ID的API相比,它具有明显的劣势。