原文: http://www.sitepen.com/blog/2012/06/25/amd-the-definitive-source/
作者:Kris Zyp
译者:Elaine Liu
随着web应用不断发展和对JavaScript依赖的进一步加深,出现了使用模块(Modules)来组织代码和依赖性。模块使得我们创建明确清晰的组件和接口,这些组件和接口能够很容易的加载并连接到其依赖组件。 AMD模块系统提供了使用JavaScript模块来构建Web应用的完美方式,并且这种方式具有形式简单,异步加载和广泛采用的特点。
异步模块定义(AMD)格式是一套API,它用于定义可重用的并能在多种框架使用的模块。开发AMD是为了提供一种定义模块的方式,这种方式可以使用原生的浏览器脚本元素机制来实现模块的异步加载。AMD API由2009年Dojo 社区的讨论中产生,然后移动到讨论CommonJS如何更好的为浏览器适应CommonJS模块格式(被NodeJS使用)。 CommonJS已经发展成为单独的一个标准并有其专门的社区。AMD已经广泛普及,形成了众多模块加载实现并被广泛使用。在SitePen公司,我们广泛的使用Dojo的AMD机制工作,为其提供支持,并积极的建设这一机制。
模块化系统的基础前提是:
AMD格式提供了几个关键的好处。首先,它提供了一种紧凑的声明依赖的方式。通过简单的字符串数组来定义模块依赖,使得开发者能够花很小的代价轻松列举大量模块依赖性。
AMD帮助消除对全局变量的需求。 每个模块都通过局部变量引用或者返回对象来定义其依赖模块以及输出功能。因此,模块不需要引入全局变量就能够定义其功能并实现与其他模块的交互。AMD同时是“匿名的”,意味着模块不需要硬编码指向其路径的引用, 模块名仅依赖其文件名和目录路径,极大的降低了重构的工作量。
通过将依赖性映射为局部变量, AMD鼓励高效能的编码实践。如果没有AMD模块加载器,传统的JavaScript代码必须依赖层层嵌套的对象来“命名”给定的脚本或者模块。如果使用这种方式,通常需要通过一组属性来访问某个功能,这会造成全局变量的查找和众多属性的查找,增加了额外的开发工作同时降低了程序的性能。通过将模块依赖性映射为局部变量,只需要一个简单的局部变量就能访问某个功能,这是极其快速的并且能够被JavaScript引擎优化。
最基础的AMD API是define()方法,用于定义一个模块及其依赖。通常我们这样来写一个模块:
define(dependencyIds, function(dependency1, dependency2,...){
// module code
});
dependencyIds 参数是一个字符串数组,用于表示需要加载的依赖模块。这些依赖模块将会被加载和执行。一旦所有依赖都被执行完毕,它们的输出将作为参数提供给回调函数(define()方法的第二个参数)
为了展示AMD的基础用法,我们可以定义一个使用dojo/query(css选择器查询)和dojo/on(事件处理)的模块。
define(["dojo/query", "dojo/on"],
function(query, on){
return {
flashHeaderOnClick: function(button){
on(button, "click", function(){
query(".header").style("color", "red");
});
}
};
});
一旦dojo/query和dojo/on被加载(当然也必须等到它们本事的依赖也被加载,以此类推), 回调函数将被调用,同时dojo/query的输出(一个负责CSS选择器查询的函数)作为参数query,dojo/on的输出(一个可以添加事件监听器的函数)作为参数on被传到这个回调函数中。回调函数(通常认为是模块的工厂方法)被保证只调用一次。
列在依赖集合中的每个模块标识是一个抽象的模块路径。说它是抽象的因为它被模块加载器转移成真正的URL。正如你所见,模块路径并不需要包含“.js”后缀,这个后缀在加载的时候会自动添加。当模块标识直接由模块名打头时,该名称是模块的绝对标识。相比之下,我们也可以通过由“./”或者"../"打头表示当前目录或者父目录来指定相对标识。这些相对标识会通过标准路径解析规则来解析成绝对标识。你可以定义一个模块路径规则来决定这些模块路径将如何转换成URL。缺省情况下,模块根目录定义为相对于模块加载器包的父目录的路径。例如,如果我们用下面的方法加载Dojo(注意在这里我们设置async属性为true来保证异步AMD加载)
那么,假设根目录到模块的路径为“/path/to/”。如果我们指定依赖于“my/module”,这个依赖将被解析为“/path/to/my/module.js”.
我们已经描述了如何创建一个简单的模块。然而,我们还需要一个入口来触发这些依赖链。我们可以通过使用require() API来做到这一点。这个函数签名基本跟define()一致,区别在于它用于加载依赖但而不需要定义一个模块(当一个模块被定义时,如果它不被别的模块请求它是不会执行的)我们可以像下面这样加载我们的应用程序:
Dojo提供了加载初始模块的快捷方式。初始模块能够通过指定deps配置属性来加载。
这是加载应用程序的一个非常棒的方式,因为JavaScript代码能够完全从HTML中消除,仅需留下一个脚本标记来引导整个剩余的程序。同时,这种方式让你能够轻松的创建强劲的build,它能够将你的应用程序代码和dojo.js组合成为单独的一个文件而不需要在build之后改变HTML脚本标签。RequireJS和其他模块加载器也有类似的加载顶层模块的选项。
上图展示了由require()调用引起的一连串的依赖加载。require()的调用开启加载第一个模块,接着根据需要加载各模块的依赖模块。那些不需要的模块(如上图中的模块d)则永远不会被加载或者执行。
require()函数还可用于配置模块路径查找以及其他选项,但这一般来说对各个模块加载器都有特定的实现。更多信息请参考各个加载器关于配置细节的文档。
AMD还支持加载其它资源的插件。这一点对于加载非AMD依赖非常有价值,例如加载HTML片段和模板,CSS,国际化相关的特定资源等。插件机制让我们在依赖列表中引用这些非AMD资源。语法如下:
"plugin!resource-name"
define(["dojo/_base/declare", "dijit/_WidgetBase", "dijit/_TemplatedMixin", "dojo/text!./templates/foo.html"],
function(declare, _WidgetBase, _TemplatedMixin, template){
return declare([_WidgetBase, _TemplatedMixin], {
templateString: template
});
});
这是一个多层面创建Dojo小部件的范例。首先,它展示了利用Dijit基类创建小部件的标准用法。你可能也注意到我们如何创建一个小部件类并如何返回它。我们没有使用任何命名空间或者类名来使用declare()(类构造方法)。因为AMD消除了对命名空间的需求,我们不再需要用declare()来创建全局的类名。这一点与AMD模块中写匿名模块的策略是一致的。同样的,一个匿名的模块是不需要在其模块内部硬编码任何相对于自身的路径或者名称的。我们可以轻松的对模块重命名或者将其移动到其他的路径而不需要改模块内的任何代码。通常我们推荐使用这种方法来定义匿名类,但如果你需要用声明式的标记来使用这个小部件的话,为了创建一个具有命名空间的全局变量,使其能够让Dojo 解析器在Dojo1.7中引用,你还是需要包含命名空间/类名来定义类。Dojo 1.8中对此作了改进,你可以使用模块标识来做到这一点。
还有一些Dojo包含的插件是非常有用的。dojo/i18n插件在加载国际化区域性包(常用于翻译文本或者区域信息格式化)时使用。另外一个重要的插件是dojo/domReady,它通常被推荐用于取代dojo.ready。如果一个模块除了加载其他依赖还需要等待整个DOM可用才执行的时候,这个插件使得这一过程非常简单,不需要再加一层额外的回调。我们将dojo/domReady作为插件使用时,但需要对应的资源名。
define(["dojo/query", "dojo/domReady!"],
function(query){
// DOM is ready, so we can query away
query(".some-class").forEach(function(node){
// do something with these nodes
});
});
另一个有价值的插件是dojo/has。 这个模块用于辅助检测某些特征,帮助你基于当前浏览器的某些特征来选择不同的代码路径。然而这个模块也常被用作一个标准模块 ,提供一个has()函数,它也同时可以当作插件使用。作插件使用时能够帮助我们根据当前的特性条件性的加载某些依赖。dojo/has插件的语法采用了一个三元操作符,它将特性名称作为条件,而模块标识作为值。例如,我们可以在当然浏览器支持touch事件的条件下加载单独的touch UI模块:
define(["dojo/has!touch?ui/touch:ui/desktop"],
function(ui){
// ui will be ui/touch if touch is enabled,
//and ui/desktop otherwise
ui.start();
});
这个三元操作符可以是嵌套的,空字符串可用于表示不加载任何模块。
使用dojo/has的好处不仅仅是提供一个特征检测的运行时API。如果使用dojo/has,不光在你的代码中有has()形式,同时也作为依赖插件,build系统可以检测这些特性的分支。这意味着我们可以创建设备或者浏览器相关的build,它们能够为某些特定的特征集合进行高度优化,只需要在build里的staticHasFeatures选项定义期望的特性,然后build就会自动的处理相应的代码分支。
define({
foo: "bar"
});
这与JSONP非常类似,支持基于脚本的JSON数据传输。但是,实际上AMD对于JSONP的优势在于它不需要请求任何URL参数,其目标可以是一个静态文件,不需要服务器端任何支持代码来为数据加上参数化的回调函数前缀。然而,这项技术必须小心使用。模块加载器总是会对模块进行缓存,因此后续的针对相同模块标识的require()请求会产生同样的缓存数据。这对你的检索需求可能会造成一定的困扰。
AMD 被设计得非常容易被构建工具解析从而创建出将多个模块代码连接在一起并压缩的一个单独文件。模块化系统在这方面提供了巨大的优势因为构建工具能够基于模块中列出的依赖自动的生成这个构建文件,而不需要依赖任何手写或者更新的脚本来创建。由于请求数目的减少,构建极大的减少了加载时间,并且由于依赖已经清楚的列在代码中,因而使用AMD实现这一点简直轻而易举。
不使用构建
使用构建
(译者注:原文作者选用的实验截图可能是本地资源加载的情况,由于本地资源加载的随机性,在使用构建之后优势不明显。但实际在网络传输中,使用构建会大大减少加载时间。)
就像前面提到的那样,使用脚本元素注入比其他的方法快是因为它更依赖于原生的浏览器脚本加载机制。我们基于dojo.js创建了一些模块的测试用例,脚本元素加载比使用XHR eval的方式快了大概60-90%。在Chrome中,如果有大量的小模块,每个模块加载的时间大概是5-6ms
,而XHR+eval方式平均每个模块加载时间则接近9-10ms。在Firefox中,同步XHR方式比异步方式更快,而在IE中异步XHR比同步的快,但脚本元素加载无疑是最快的一个。让我们感到意外的是IE9是最快的一个浏览器,不过这有可能是因为在Firefox和Chrome中debugger/inspector增加了一些额外的性能开销。
AMD API是开放的,现在已有有多个AMD模块加载器和构造器的实现。这里介绍几个重要的AMD加载器:
my-script.js:
// add this to top of the script
defined([], function(){
// existing script
...
// add this to the end of script
});
require(["dgrid/Grid", "dojo/query", "my-script"], function(Grid, query){
new Grid(config, query("#grid")[0]);
});
require(["dojo", "jquery.js"], function(dojo){ // jquery will be loaded as plain script
dojo.query(...); // the dojo exports will be available in the "dojo" local variable
$(...); // the other script will need to create globals
});
define(function(require){
var query = require("dojo/query");
var on = require("dojo/on");
...
});
// declare modules that we need up front
define(["dojo/dom-create", "require"],
function(domCreate, require){
return function(node){
// create container elements for our widget right away,
// these could be styled for the right width and height,
// and even contain a spinner to indicate the widgets are loading
var slider = domCreate("div", {className:"slider"}, node);
var progress = domCreate("div", {className:"progress"}, node);
// now load the widgets, we load them independently
// so each one can be rendered as it downloads
require(["dijit/form/HorizontalSlider"], function(Slider){
new Slider({}, slider);
});
require(["dijit/Progress"], function(Progress){
new Progress({}, progress);
});
}
main.js:
define(["component", "exports"],
function(component, exports){
// we define our exported values on the exports
// which may be used before this factory is called
exports.config = {
title: "test"
};
exports.start = function(){
new component.Widget();
};
});
component.js:
define(["main", "exports", "dojo/_base/declare"],
function(main, exports, declare){
// again, we define our exported values on the exports
// which may be used before this factory is called
exports.Widget = declare({
showTitle: function(){
alert(main.config.title);
}
});
});
define(function(require, exports){
var query = require("dojo/query");
exports.myFunction = function(){
....
};
});
import {query} from "dojo/query.js";
import {on} from "dojo/on.js";
export function flashHeaderOnClick(button){
on(button, "click", function(){
query(".header").style("color", "red");
});
}