综述
在Dojo1.7及之后的版本,模块以Asynchronous Module Definition (AMD)的格式书写,取代了dojo.provide,dojo.require,dojo.requireIf,dojo.requireAfterIf,dojo.platformRequire和dojo.requireLocalization,包含完全的异步操作,真正的包的可移植性,更好的依赖性管理和改进对调试的支持。这也是社区驱动的标准,这意味着写入AMD规范的模块可用于任何其它的AMD兼容的加载器或库。
介绍AMD模块标识符
新的AMD语法使得模块标识符看起来像是路径,而不是对象引用。这些新的标识符工作起来也很像路径,在相同的包中可以使用类似./和../的相对片段指向其它模块。为了加载任意的,非AMD代码甚至可以使用完整的URL作为模块标识符。
配置加载器
假定demo应用的文件系统结构如下:
- /
- index.html
- js/
- lib/
- dojo/
- dijit/
- dojox/
- my/
- util/
- <script data-dojo-config="async: true" src="js/lib/dojo/dojo.js"></script>
在异步模式中,加载器只定义了两个全局函数:require用于加载模块,define用于定义模块。
接下来需要配置加载器,其中包含模块位置的信息:
- var dojoConfig = {
- baseUrl: "/js/",
- tlmSiblingOfDojo: false,
- packages: [
- { name: "dojo", location: "lib/dojo" },
- { name: "dijit", location: "lib/dijit" },
- { name: "dojox", location: "lib/dojox" },
- { name: "my", location: "my", main: "app" }
- ]
- };
包
有三个主要的包配置选项。name是包的名称;location为包的位置,可以是相对于baseUrl的路径或是一个绝对路径;main为可选的,默认为main,当某人试图请求包本身时,用于发现正确的模块来加载。例如若试图请求dojo,实际加载的文件是/js/dojo/main.js。因为已将my包的该属性覆盖了,所以某人请求my,实际加载的是/js/my/app.js。若要请求util,没有对其定义,加载器将尝试加载/js/util.js。
请求模块
使用例子来解释AMD风格的模块加载:
- require([ "dojo/_base/declare", "dijit/_WidgetBase", "dijit/_TemplatedMixin" ], function(declare, _WidgetBase, _TemplatedMixin){
- // "declare" holds the dojo declare function
- // "_WidgetBase" holds the dijit _WidgetBase constructor
- // "_TemplatedMixin" holds the dijit _TemplatedMixin constructor
- // Do whatever you want with these modules here.
- });
require函数也可以用于在运行时重新配置加载器,通过传递一个配置对象作为第一个参数:
- require({
- baseUrl: "/js/",
- packages: [
- { name: "dojo", location: "//ajax.googleapis.com/ajax/libs/dojo/1.8/" },
- { name: "my", location: "my" }
- ]
- }, [ "my/app" ]);
当提供了配置对象,依然可以传递依赖数组作为第二个参数,而回调函数作为第三个参数。
注意async,tlmSiblingOfDojo,和已存在的has测试不能在运行时设置。此外,大多数配置数据是浅拷贝的,不能使用机制给定制的配置对象增加更多的键,该对象将会被重写。
定义模块
使用define函数定义模块。define调用和require调用相同,不同的只是回调函数返回一个被保存的值,用于模块的resolved值。
- // in "my/_TemplatedWidget.js"
- define([ "dojo/_base/declare", "dijit/_WidgetBase", "dijit/_TemplatedMixin" ], function(declare, _WidgetBase, _TemplatedMixin){
- return declare([ _WidgetBase, _TemplatedMixin ], {});
- });
值得一提的是,定义模块时回调函数仅调用一次,其返回值被加载器缓存。从实践的角度看,这意味着通过依赖相同的模块,模块很容易共享对象。
相同的代码在遗留模块格式中:
- dojo.provide("my._TemplatedWidget");
- dojo.require("dijit._WidgetBase");
- dojo.require("dijit._TemplatedMixin");
- dojo.declare("my._TemplatedWidget", [ dijit._WidgetBase, dijit._TemplatedMixin ], {});
- define({
- greeting: "Hello!",
- howAreYou: "How are you?"
- });
不使用回调函数将不能引用任何依赖,这通常仅用于i18n包中。
使用可移植模块
新的AMD加载器一个最重要的特征是能够创建完全可移植的包。新的加载器使得一个应用中使用来自两个不同Dojo版本的模块很容易实现。通过在包配置中加入packageMap对象,使得在该包中隐式的重映射引用到其他包成为可能。加载两个不同Dojo版本的包配置示例如下:
- var map16 = { dojo: "dojo16", dijit: "dijit16", dojox: "dojox16" },
- dojoConfig = {
- packages: [
- { name: "dojo16", location: "lib/dojo16", packageMap: map16 },
- { name: "dijit16", location: "lib/dijit16", packageMap: map16 },
- { name: "dojox16", location: "lib/dojox16", packageMap: map16 },
- { name: "my16", location: "my16", packageMap: map16 },
- { name: "dojo", location: "lib/dojo" },
- { name: "dijit", location: "lib/dijit" },
- { name: "dojox", location: "lib/dojox" },
- { name: "my", location: "my" }
- ]
- };
也可以使用paths配置属性重映射整个路径。paths从字符串的开头匹配模块标识符的任何部分,以最长的匹配路径为准。例如:
- var dojoConfig = {
- paths: {
- "my/debugger/engine": "my/debugger/realEngine",
- "my/debugger": "other/debugger"
- }
- };
- my/debugger => other/debugger
- my/debugger/foo => other/debugger/foo
- my/debugger/engine/ie => my/debugger/realEngine/ie
- not/my/debugger => not/my/debugger
新的加载器也提供了一个aliases配置属性,与paths不同,只匹配完整的模块标识符。Aliases也递归的匹配aliases,直到没有新的匹配为止。例如:
- var dojoConfig = {
- aliases: [
- [ "text", "dojo/text" ],
- [ "dojo/text", "my/text" ],
- [ "i18n", "dojo/i18n" ],
- [ /.*\/env$/, "my/env" ]
- ]
- };
- text => dojo/text
- dojo/text => my/text
- i18n => dojo/i18n
- foo => foo
- [anything]/env => my/env
使用aliases,目标别名必须是完全的模块标识符,源别名必须是完全的模块标识符或正则表达式。
写可移植模块
为了实现可移植性,任何内部包模块引用要使用相对模块标识符,例如:
- // in "my/foo/blah.js"
- define([ "my/otherModule", "my/foo/bar" ], function(otherModule, bar){
- // …
- });
- // in "my/foo/blah.js"
- define([ "../otherModule", "./bar" ], function(otherModule, bar){
- // …
- });
- // in "my/debug.js"
- define([ "dojo/dom", "dojo/dom-construct", "dojo/on" ], function(dom, domConstruct, on){
- on(dom.byId("debugButton"), "click", function(evt){
- require([ "my/debug/console" ], function(console){
- domConstruct.place(console, document.body);
- });
- });
- });
- // in "my/debug.js"
- define([ "dojo/dom", "dojo/dom-construct", "dojo/on", "require" ], function(dom, domConstruct, on, require){
- on(dom.byId("debugButton"), "click", function(evt){
- require([ "./debug/console" ], function(console){
- domConstruct.place(console, document.body);
- });
- });
- });
使用插件
插件可用于为加载器扩展新的特征,而不只是简单的加载一个AMD模块。插件的加载方式或多或少和常规的模块相同,但是在模块标识符的最后加入了一个特殊的!,作为插件请求的标志。在!之后的数据直接传递给插件来处理。Dojo默认含有插件,其中最重要是dojo/text,dojo/i18n,dojo/has和dojo/domReady。
dojo/text
dojo/text是dojo.cache的替代,用于需要从文件(如一个HTML模板)加载一个字符串的时候。例如为模板化部件加载模板,可能定义如下模块:
- // in "my/Dialog.js"
- define([ "dojo/_base/declare", "dijit/Dialog", "dojo/text!./templates/Dialog.html" ], function(declare, Dialog, template){
- return declare(Dialog, {
- templateString: template // template contains the content of the file "my/templates/Dialog.html"
- });
- });
- dojo.require("dijit.Dialog");
- dojo.declare("my.Dialog", dijit.Dialog, {
- templateString: dojo.cache("my", "templates/Dialog.html")
- });
dojo/i18n
dojo/i18n为dojo.requireLocalization和dojo.i18n.getLocalization的替代,用法如下:
- // in "my/Dialog.js"
- define([ "dojo/_base/declare", "dijit/Dialog", "dojo/i18n!./nls/common"], function(declare, Dialog, i18n){
- return declare(Dialog, {
- title: i18n.dialogTitle
- });
- });
- dojo.require("dijit.Dialog");
- dojo.requireLocalization("my", "common");
- dojo.declare("my.Dialog", dijit.Dialog, {
- title: dojo.i18n.getLocalization("my", "common").dialogTitle
- });
dojo/has
Dojo新的加载器包括has.js特征检测API的实现,dojo/has插件为有条件的请求模块调节了该功能,用法如下:
- // in "my/events.js"
- define([ "dojo/dom", "dojo/has!dom-addeventlistener?./events/w3c:./events/ie" ], function(dom, events){
- // events is "my/events/w3c" if the "dom-addeventlistener" test was true, "my/events/ie" otherwise
- events.addEvent(dom.byId("foo"), "click", function(evt){
- console.log("Foo clicked!");
- });
- });
- dojo.requireIf(!window.addEventListener, "my.events.ie");
- my.events.addEvent(dom.byId("foo"), "click", function(evt){
- console.log("Foo clicked!");
- });
dojo/domReady
dojo/domReady是dojo.ready的替代,该模块直到DOM加载完成才解析,用法如下:
- // in "my/app.js"
- define(["dojo/dom", "dojo/domReady!"], function(dom){
- // This function does not execute until the DOM is ready
- dom.byId("someElement");
- });
- dojo.byId("someElement");
- });
- define([ "b" ], function(b){
- var a = {};
- a.stuff = function(){
- return b.useStuff ? "stuff" : "things";
- };
- return a;
- });
- // in "my/b.js"
- define([ "a" ], function(a){
- return {
- useStuff: true
- };
- });
- // in "my/circularDependency.js"
- require([ "a" ], function(a){
- a.stuff(); // "things", not "stuff"
- });
这里加载器试图加载模块A,然后是模块B,然后又是模块A,注意模块A是循环依赖的部分。为了打破循环依赖,模块A将自动解析为空的对象,该空对象将作为A的值传递给模块B,然后模块A的回调函数被调用,其返回值被丢弃。在上面的例子中,这意味着A将是空的对象,而不是有着stuff函数的对象,因此代码不会按预期工作。
为了解决这个问题,加载器提供了特殊的exports模块标识符。这样的话,该模块将返回空的对象,用于解决循环依赖。当回调函数被调用,可以附加属性到exports。这时,stuff函数依然可以成功定义并在后面使用:
- // in "my/a.js"
- define([ "b", "exports" ], function(b, exports){
- exports.stuff = function(){
- return b.useStuff ? "stuff" : "things";
- };
- return exports;
- });
- // in "my/b.js"
- define([ "a" ], function(a){
- return {
- useStuff: true
- };
- });
- // in "my/circularDependency.js"
- require([ "a" ], function(a){
- a.stuff(); // "stuff"
- });
加载非AMD代码
AMD加载器也可以通过传递一个标识符用于加载非AMD代码,那实际上就是一个指向JavaScript文件的路径。加载器以以下三种方式之一识别这些特殊的标识符:
- 标识符以 “/" 开头
- 标识符以一个协议开头 (例如 “http:”, “https:”)
- 标识符以 “.js” 结束
当任意代码被当作模块加载,模块的解析值为undefined,将需要直接访问由脚本全局定义的任何代码。
Dojo加载器的一个独有的特征是以AMD风格的模块混合和匹配遗留的Dojo模块的能力。这使得缓慢而有条理的从遗留的代码库过渡到AMD代码库成为可能,而不需要立即转变所有东西,无论加载器工作在同步模式还是异步模式。
服务端JavaScript
- # node.js:
- node path/to/dojo.js load=my/serverConfig load=my/app
- # rhino:
- java -jar rhino.jar path/to/dojo.js load=my/serverConfig load=my/app
- <script data-dojo-config="async: true" src="path/to/dojo.js"></script>
- <script>require(["my/serverConfig", "my/app"]);</script>