为何用 AMD 规范?

模块目的

什么是 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() {} 的话会更简洁。这意味着不需要持有 module 以module.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 和 加载器插件。 加载器插件是个很好的减轻回调、异步风格的代码中常见的嵌套回调症的方式。
原文地址:https://www.cnblogs.com/zhepama/p/3092450.html