原文:https://addyosmani.com/resources/essentialjsdesignpatterns/book/#detailamd

用于在浏览器中编写模块化 JavaScript 的格式

AMD(Asynchronous Module Definition)格式总体目标是为开发者提供一个能在今天使用模块化 JavaScript 的方案。它源自 Dojo 使用 XHR + eval 的真实经验,这个格式的倡导者希望避免任何未来的方案受到它过去方案缺点的影响。

AMD 模块格式本身是一个提案,它用于定义那些模块和依赖都能异步加载的模块。它有很多明显的优点,包括异步性和高度灵活性,这消除了代码和模块标识之间的通常会出现的强耦合性。很多开发者都喜欢使用它,有人甚至认为它是我们迈向 ES Harmony 的模块系统的可靠的过渡方案。

AMD 最初只是 CommonJS 模块规范列表中的一个草案规范,但是由于没能达成一致的意见,这个格式的进一步开发移到了 amdjs 组中了。

今天它已经被 Dojo、MooTools、Firebug 甚至是 jQuery 等项目所接受。尽管有时会以 CommonJS AMD 格式 的名字出现,但是最好简称它为 AMD 或者 Async Module support,因为并非 CommonJS 列表所有的参与者都希望支持它。

注意: 这个方案有段时间是被称作 Modules Transport/C,然而因为该规范并适合于传输现有的 CommonJS 模块,而是定义模块,所以选择 AMD 命名约定更有意义。

从模块开始

关于 AMD 有两个值得关注的概念,简化模块定义的 define 和用于处理依赖加载的 requiredefine 使用下面这样的语法来定义命名或未命名的模块:

  1. define(
  2. module_id /* 可选的 */,
  3. [dependencies] /* 可选的 */,
  4. definition function /* 用于实例化模块或对象的函数 */
  5. );

正如行内注释所写的, module_id 是一个可选的参数,它只有在使用非 AMD 串联工具时才需要(可能还有一些其他极端的情况会用到它)。当这个参数置空时,我们称这个模块为 匿名模块

当使用匿名模块时,模块定义的思路是 DRY,使得避免文件名和代码重复变得更简单。因为代码更具有可移植性,它可以轻松的移动到其他位置(或者文件系统其他地方)而不需要修改代码本身或者改变它的模块 ID。把 module_id 当作文件路径的概念。

注意:开发者可以在多个环境中运行相同的代码,只需要使用 AMD optimizer,它可以在 r.js 这样的 CommonJS 环境中工作。

回到 define 的签名,dependencies 参数表示一组我们定义的模块所需要的依赖。第三个参数(“definition 函数” 或者 “工厂函数”)是一个函数,它是用于实例化我们的模块。一个大致的模块定义大概是这个样式。

理解 AMD:define()

  1. define(
  2. 'myModule', // 这里的模块 id (myModule)只是用于演示的作用
  3. ['foo', 'bar'], // 模块定义函数
  4. function(foo, bar) { // 依赖(foo 和 bar)被映射成函数的参数
  5. // 返回模块的导出值
  6. // (如我们希望公开的功能)
  7. // 在这里创建你的模块
  8. var myModule = {
  9. doStuff: function() {
  10. console.log('Yay! Stuff');
  11. }
  12. };
  13. return myModule;
  14. }
  15. );
  16. // 另个一个可替换的方式是..
  17. define(
  18. 'myModule',
  19. ['math', 'graph'],
  20. function(math, graph) {
  21. // 注意这里与 AMD 有点细微的却别,
  22. // 由于语法的灵活性,
  23. // 定义模块有几种方式实现
  24. return {
  25. plot: function(x, y) {
  26. return graph.drawPie(math.randomGrid(x, y));
  27. }
  28. };
  29. }
  30. );

另一方面,require 通常用于在顶层 JavaScript 文件中加载代码或者是在模块内我们希望动态加载依赖。它的用法示例是:

理解 AMD:require()

  1. // 考虑 "foo" 和 "bar" 是两个外部模块
  2. // 在这个例子中,已加载的两个模块的 “导出” 传递给了回调函数的参数(foo 和 bar)
  3. // 这样它们就都可以被访问
  4. require(['foo', 'bar'], function(foo, bar) {
  5. // 剩下的你的代码
  6. foo.doSomething();
  7. });

动态加载的依赖

  1. define(function ( require ) {
  2. var isReady = false, foobar;
  3. // 注意在我们模块定义的行内 require
  4. require(["foo", "bar"], function ( foo, bar ) {
  5. isReady = true;
  6. foobar = foo() + bar();
  7. });
  8. // 我们仍然可以返回一个模块
  9. return {
  10. isReady: isReady,
  11. foobar: foobar
  12. };
  13. });

理解 AMD:plugins

下面是一个定义符合 AMD 的插件的示例:

  1. // 通过 AMD,可以加载包括文本文件和 HTML 等大多数资源。
  2. // 这就使得我们拥有模板依赖,
  3. // 它可以用于在页面加载或者动态地为组件添加皮肤
  4. define(['./templates', 'text!./template.md', 'css!./template.css'], function(
  5. templates,
  6. template
  7. ) {
  8. console.log(templates);
  9. // 使用我们的 templates 做一些事
  10. });

注意: 尽管上例中的 css! 是用来加载 CSS 依赖的,但是要记住这个方式有些警告,例如不能完全确定 CSS 何时加载完。根据我们如何处理构建过程,也可能导致 CSS 作为依赖项包含在优化后的文件中,所以在这种使用 CSS 作为已加载的依赖的情况要格外小心。如果对上面的做法感兴趣,我们可以在这里 探究 @VIISON 的 RequireJS CSS 插件。

使用 RequireJS 加载 AMD 模块

  1. require(['app/myModule'], function(myModule) {
  2. // 启动主模块,它负责加载其他模块
  3. var module = new myModule();
  4. module.doStuff();
  5. });

这个示例可以被简单的看作 requirejs(['app/myModule'], function(){}) ,它表示正在使用加载器顶层的全局变量。This is how to kick off top-level loading of modules with different AMD loaders however with a define() function, if it’s passed a local require all require([]) examples apply to both types of loader (curl.js and RequireJS).

使用 curl.js 加载 AMD

  1. curl(
  2. ['app/myModule.js'],
  3. function(myModule) {
  4. // 启动主模块,它负责加载其他模块
  5. var module = new myModule();
  6. module.doStuff();
  7. }
  8. );

有延迟依赖的模块

  1. // 我们也可以使用 jQuery、future.js(语法有点不同)
  2. // 或者其他类似的方案来实现 Deferred 的功能
  3. define(['lib/Deferred'], function(Deferred) {
  4. var defer = new Deferred();
  5. require(['lib/templates/?index.html', 'lib/data/?stats'], function(
  6. template,
  7. data
  8. ) {
  9. defer.resolve({ template: template, data: data });
  10. });
  11. return defer.promise();
  12. });

与 Dojo 一起使用 AMD 模块

使用 dojo 定义符合 AMD 的模块相当简单。如上所述,将模块所有依赖定义成一个数组作为函数的第一个参数,并提供一个回调函数(工厂),它会在依赖被加载完后执行。如:

  1. define(["dijit/Tooltip"], function(Tooltip){
  2. // 现在可以使用我们的 dijit tooltip 了
  3. new Tooltip(...);
  4. });

注意这是一个匿名模块,它可以被 Dojo asynchronous loader、RequireJS 或者标准的 dojo.require() 模块加载。

这里有一些模块引用的有趣的点值得了解一下。尽管 AMD 支持的模块引用方式是在一个依赖列表中声明它们,并映射成对应的参数,但它在 Dojo 1.6 以前的构建系统中是不支持的,需要与一些符合 AMD 的加载器使用。如:

  1. define(["dojo/cookie", "dijit/Tooltip"], function(cookie, Tooltip){
  2. var cookieValue = cookie("cookieName");
  3. new Tooltip(...);
  4. });

这样比嵌套的命名空间的方式更好,因为模块不再需要每次都直接引用完整的命名空间 - 我们要做到就是在依赖中引用路径 “dojo/cookie“,一旦它被映射成一个参数,我们就可以通过变量来引用它。这就避免了我们在程序中重复的输入 ”dojo.“。

最后一个需要注意的地方是如果我们希望继续使用旧的 Dojo 构建系统,或者想将旧的模块升级到新的 AMD 风格,以下更详细的版本能让迁移更轻松。注意 dojo 和 dijit 也被引用作了依赖:

  1. define(['dojo', 'dijit', 'dojo/cookie', 'dijit/Tooltip'], function(dojo, dijit){
  2. var cookieValue = dojo.cookie('cookieName');
  3. new dijit.Tooltip(...);
  4. });

AMD 模块设计模式(Dojo)

正如我们在上一节所看到的,设计模式可以非常有效地改善我们解决常见开发问题的结构化解决方案。John Hann 已经做了很多很好的关于 AMD 设计模式演讲,涵盖了单例、修饰器、调解器和其他模式,如果有机会的话,我强烈推荐你看看它的 ppt

下面是一组 AMD 设计模式。

修饰器模式:
**

  1. // mylib/UpdatableObservable: 一个用于修饰 dojo/store/Observable 的修饰器
  2. define(['dojo', 'dojo/store/Observable'], function(dojo, Observable) {
  3. return function UpdatableObservable(store) {
  4. var observable = dojo.isFunction(store.notify)
  5. ? store
  6. : new Observable(store);
  7. observable.updated = function(object) {
  8. dojo.when(object, function(itemOrArray) {
  9. dojo.forEach([].concat(itemOrArray), this.notify, this);
  10. });
  11. };
  12. return observable;
  13. };
  14. });
  15. // 修饰器使用者
  16. // mylib/UpdatableObservale 的使用者
  17. define(['mylib/UpdatableObservable'], function(makeUpdatable) {
  18. var observable, updatable, someItem;
  19. // 让可观察的存储可更新
  20. updatable = makeUpdatable(observable); // `new` 是可选的!
  21. // 如果我们希望传递过去的数据变更时,我们可以调用 .updated()
  22. //updatable.updated( updatedItem );
  23. });

适配器模式

  1. // "mylib/Array": 接收类似 jQuery 的 each 函数参数:
  2. define(['dojo/_base/lang', 'dojo/_base/array'], function(lang, array) {
  3. return lang.delegate(array, {
  4. each: function(arr, lambda) {
  5. array.forEach(arr, function(item, i) {
  6. lambda.call(item, i, item); // 像 jQuery 的 each 一样
  7. });
  8. },
  9. });
  10. });
  11. // 适配器使用者:
  12. // "myapp/my-module":
  13. define(['mylib/Array'], function(array) {
  14. array.each(['uno', 'dos', 'tres'], function(i, esp) {
  15. // 这里, `this` == item
  16. });
  17. });

与 jQuery 使用的 AMD 模块

与 Dojo 不同,jQuery 实际上只有一个文件,但是鉴于它基于插件的性质,我们下面演示把当 AMD 定义并使用它如何简单。

  1. define(['js/jquery.js', 'js/jquery.color.js', 'js/underscore.js'], function(
  2. $,
  3. colorPlugin,
  4. _
  5. ) {
  6. // 这里我们传入了 jQuery、颜色插件和 Underscore
  7. // 它们在全局作用域下都是不能访问的,
  8. // 但是在这里我们可以轻易地访问到它们
  9. // 伪随机颜色数组,选择随机后的第一项
  10. var shuffleColor = _.first(_.shuffle(['#666', '#333', '#111']));
  11. // 为页面中所有含 "item" 样式类的元素设置动画背景色为 shuffleColor
  12. $('.item').animate({ backgroundColor: shuffleColor });
  13. // 我们返回的内容可以供其他模块使用
  14. return {};
  15. });

然而这个例子中缺少了一样东西,那就是注册的概念。

将 jQuery 注册为兼容异步的模块

即将到来的 jQuery 1.7 的一个新特性是支持将 jQuery 注册成异步模块。有很多脚本加载器(包括 RequireJS 和 curl)都能加载异步模块格式的模块,这就意味着不需要太多的技巧就能到达目的。

如果一个开发者想使用 AMD 但是又不希望它们的 jQuery 版本泄漏到全部作用域,他们可以在他们的根模块中调用 jQuery 的 noConflict 。另外,因为一个页面可以存在多版本的 jQuery,所以 AMD 加载器必须要考虑一些特殊情况,因此 jQuery 只注册到那些已经有发现并解决这个问题的 AMD 加载器,它是通过加载器的 define.amd.jQuery 来判定是否可以被注册。RequireJS 和 curl 就是这样的加载器。

已命名的 AMD 为大多数情况提供了一个既健壮又安全的保护层。

  1. // document 中存在多个全局 jQuery 实例,
  2. // 以测试 .noConflict()
  3. var jQuery = this.jQuery || 'jQuery',
  4. $ = this.$ || '$',
  5. originaljQuery = jQuery,
  6. original$ = $;
  7. define(['jquery'], function($) {
  8. $('.items').css('background', 'green');
  9. return function() {};
  10. });

为什么 AMD 是编写 JavaScript 模块更好的选择?

  • 为如何定义灵活的模块提供了清晰的方案。
  • 比全局命名空间和我们大多数使用的 <script> 标签方案明显要干净得多。它有干净的方式来声明一个独立的模块以及它需要的依赖。
  • 模块的定义是预封装好的,帮助我们避免全局命名空间污染。
  • 可以说比一些可替代方案更有效(如 CommonJS,我们随后将会讲到它)。它不存在跨域、本地或者调试问题,并且不需要配合服务端工具来使用。大多数 AMD 加载器支持在不借助其他构建流程直接在浏览器中加载模块。
  • 提供了一个“传输”方式,以便在单个文件中包含多个模块。像 CommonJS 等其它方案在传输方案上未达成统一意见。
  • 如果有需要的话可以还可以做模块懒加载。

注意: YUI 的模块加载策略也基本是这样。

相关阅读

服务端:

AMD 总结

在一些项目使用过 AMD 之后,我的结论是它符合大多数开发者创建大量应用时渴望的更好模块格式。它让我们不再需要关心全局问题、支持具名模块、不需要在服务端做函数转换并且便于做依赖管理。

它同时还是使用 Backbone.js、ember.js 或者任何其他架构框架时让应用变得更具有组织的绝佳的搭配。

由于近两年来 AMD 在 Dojo 和 CommonJS 的世界里被大量的讨论,我们知道它不断地成长和进化了。我们还知道许多大型公司(IBM、BBC iPlayer)已经在构建大型应用的过程中对其进行了实战测试,所以如果它不好用的话,他们可能早就抛弃它了,但是他们并没有。

也就是说,AMD 仍有可以改进的空间。使用了这个格式一段时间的开发者可能觉得 AMD 样板代码/包装代码是个烦人的开销。尽管我也有同样的担心,但是有像 Volo 这样的工具可以帮助解决这个问题,我认为总的来说,使用 AMD 的好处远远大于坏处。