Dojo 最强大的一个库,从企业级应用到Mobile上的使用,库的大小也可以根据需求来定制(比Jquery小)。因为它的全面,很适合于我们了解他的源代码而提升自己的能力。
下载的Dojo的源代码,本来想着直接学习Dojo.js 这个加载器源代码,但发现很难看懂,还是先从Dojo要实现的功能及其机制,在带着如果让自己去实现这些功能,去看源代码,可能会更好的掌握。
看本文档的时候还是遇到很多不懂的地方,所以才去写了 dojo loader的入门使用及高级使用
源英文文档http://dojotoolkit.org/reference-guide/1.9/loader/amd.html#id2
介绍
Dojo loader 包括两部分的API:
- 遵循AMD规范的API(“AMD API”), 如require([‘dojo/dom’],function(dom){})
- 兼容dojo 1.7以前的加载方法的API,如dojo.require(),dojo.provide();
@H_301_23@
AMD API 是在1.7版本才出现的, API的实现跟requirejs,curl及bdload是一样的。所以dojo可以不用使用自己的加载器,而直接用requirejs来加载它的核心。 Commonjs 是javascript代码模块化的标准,可以提升代码的移值性及互操作性。Commonjs主要应用于服务器端,比如nodejs. 如 var A=require(‘A’); A.hello(); 因为客户端因为网络加载的原因,不知道js何时加载完成,所以在commonjs的基础上提出了AMD规范, require([‘A’],function(A){ A.hello()}); 当 A.js文件加载完了之后,在调用回调函数。 AMD是一个异步加载的规范,它比同步加载有两点好处:
- 模块可以同时通过异步来加载,而同步加载是一个个的加载进来,所以速度是可以提升10倍。
- 调试上的改进,不在使用debugAtAllCosts来调试程序
@H_301_23@
除了loader的核心API外, 新的Dojo loader还提供了以下特点:
- 多平台支持: 默认的,Dojo loader包含浏览器端,node.js 及 rhino的配置
- Has.js API: Dojo loader实现了has.js的API,使用这个api 可以排除一些不需要的功能, 结合 dojo build工具,可以将整个loader的代码缩小到3kb(压缩及gzip过后)
- 配置API: 加载器包括一个可以由客户端应用程序配置API.
Dojo.js
在 1.7版本前,dojo.js 是加载器,也是整个框架应用的启动程序,现在的loader得到改进,只具有加载功能。实院上,现在的loader也可以加载非dojo库的文件。为了向前兼容,loader 默认是以同步的方式运行, 它会自动加载 dojo的核心库文件
<script src="path/to/dojo/dojo.js"></script> <script> // the dojo base API is available here </script>
为了执行AMD模式, 可以设置配置项 async 为ture.
<script data-dojo-config="async:1" src="path/to/dojo/dojo.js"></script> <script> // ATTENTION: nothing but the AMD API is available here </script>
配置及特征检测
配置
这里有三个方法将配置的参数传递给loader.
<script data-dojo-config="async:true,cacheBust:new Date(),waitSeconds:5" src="path/to/dojo/dojo.js"> </script>
使用全局配置对象dataConfig:
<script> var dojoConfig = { async:true,waitSeconds:5 }; </script> <script src="path/to/dojo/dojo.js"></script>
require({ cacheBust:new Date(),waitSeconds:5 });
注意:async以及has.js测试都必须在loader加载前定义, 不能在require函数里面设置。即只能通过dataConfig 或者data-dojo-config对象。
为了向之前的版本兼容,被弃用的变量djConfig 可以被用来替换dojoConfig. 在这有一点需要注意,当dojoConfig及djConfig被同时定义, djConfig会被忽略。同样也要注意的时,当dojoConfig或者djConfig被定义但同时存在data-dojo-config属性,data-dojo-config的优先级高于前者。
配置对象可以用来设置任意的,应用程序指定的数据。 所有的配置对象的属性会被拷贝到require.rawConfig; 当它们的值可以被loader承认,则被添加到require.config. 因为整个过程是浅拷贝, 如果你需要混合属性对象的子对象而不是完全取代,loader 会包含一个”config” 事件,当配置数据被接受会通过内部的微事件API触发。
“config” 事情会将以下两个参数传递给监听器:
- config: 配置对象传递给加载器时,加载器触发” config” event.
- rawConfig: require.rawConfig的值
@H_301_23@
特征检测
Has.js 有一套标准的 API, 允许特征检测从相关的代码中分离开来(如下的,只需要修改has.add里的测试,而不用在调用这个测试的地方修改, 这就是分离),并且可以使得build system 创建一套合适的dojo版本以适合你的应用程序。
has.add("dom-addeventlistener",!!document.addEventListener); if(has("dom-addeventlistener")){ node.addEventListener("click",handler); }else{ node.attachEvent("onclick",handler); }
在build的时候,通过一个适当的描述文件, 这段代码翻译成
0 && has.add("dom-addeventlistener",!!document.addEventListener); if(1){ node.addEventListener("click",handler); }
当翻译完的代码在传递给 js压缩工具,删除没用的代码,最后的输出为
以下是Dojo实现的has.js的has,这在标准的has.js 规范里面是没有的。
- 测试的结果会缓存在 has.cache
- 函数has.add 包含第四个可选参数,force. 将会被用来重写已经存在的测试(正常的, 第一次定义的值一直使用,借一个常量)
<script> var dojoConfig = { has: { "config-tlmSiblingOfDojo":0,"myApp-someFeature":1 } }; </script>
一个测试也可以被定义为一个函数, 当这个特征第一次被has请求时,函数会被执行,并返回值。
<script> var dojoConfig = { has: { "myApp-someFeature":function(){ return !!document.addEventListener; } } }; </script>
由于has 测试的使用类似于配置对象的变量,所以 loader 的配置API会将has.add应用在它接受的配置变量上。并在这些变量上加上前缀 “config-“,例如:
<script data-dojo-config="tlmSiblingOfDojo:0" src="path/to/dojo/dojo.js"></script>
上面这段代码的结果为has测试的名称为config-tlmSiblingOfDojo,值为0;
选项及特点
下表中的例出的可选项会在loader内部使用。第一列是在loader内定义的选项, 第二列标识这个特征是否被侦测到(通过has.add)或 这仅仅是一个选项及这个选项的默认值。 在这个没有经过build的源文件里面,所有的这些特征及选项都是有效的, 如果loader 被build后,会设置成为staticHasFeatures,而且不能在被配置。
Feature | Default Value | Description |
---|---|---|
dojo-trace-api | True | |
dojo-sync-loader | True | |
dojo-config-api | True | |
dojo-cdn | False | |
dojo-requirejs-api | False | |
dojo-test-sniff | True | |
dojo-combo-api | False | |
dojo-undef-api | False | |
config-tlmSiblingOfDojo | True | |
config-dojo-loader-catches | True | |
dojo-inject-api | True | |
config-stripStrict | False | |
dojo-timeout-api | True | |
dojo-log-api | True | |
dojo-amd-factory-scan | True | |
dojo-publish-privates | True | |
dojo-built | False | |
dojo-loader | True | |
host-node | Detected | Environment is running on the the NodeJS platform |
host-rhino | Detected | Environment is running on the Rhino platform |
dojo-xhr-factory | Detected | |
dojo-force-activex-xhr | Detected | Force XHR provider to use ActiveX API (MSXMLHTTP). |
native-xhr | Detected | Browser has native XHR API,XMLHttpRequest. |
dojo-gettext-api | Detected | Dojo provides API for retrieving text resource contents from a URL. |
dojo-loader-eval-hint-url | Detected | Module location should be used as source hint during eval rather than module identifier. |
ie-event-behavior | Detected | Browser supports legacy IE event behavIoUr API (attachEvent versus attachEventListener). |
AMD API
AMD API 是 dojo的首选API(dojo.require应该为次要的),通过两个全局函数require和defiine暴露,这两个函数都为异步的。 当运行的是之前的loader API模式, AMD会以同步的方式运行。
Require 可以用来配置loader 及加载AMD 模块。以下是它的方法签名
如果提供了configuration 对象,它会被传递给configuration API. 接下来,如果传递了依赖关系,依赖的模块会被加载。 最后,如果传递了依赖关系,加载的依赖模块会被传递给callback.
require( configuration,// (optional; object) configuration object dependencies,// (optional; array of strings) list of module identifiers to load before calling callback callback // (optional; function) function to call when dependencies are loaded ) -> undefined
如果提供了configuration 对象,它会被传递给configuration API. 接下来,如果传递了依赖关系,依赖的模块会被加载。 最后,如果传递了依赖关系,加载的依赖模块会被传递给callback.
require([ "my/app","dojo" ],function(app,dojo){ // do something with app and dojo... });
这里也有别种使用方法, 它有以下方法签名
require( moduleId // (string) a module identifier ) -> any
当指定的moduleId确定被加载的情况下,才使用这个方法。 它将返回被请求的Module. 如果被请求的module没有被加载完成,会抛出一个错误。 这种方法不被推荐使用,因为它将导致 程序的依赖混乱。
define( moduleId,// (optional; string) an explicit module identifier naming the module being defined dependencies,// (optional; array of strings) list of module identifiers to load before calling factory factory // (function or value) the value of the module,or a function that returns the value of the module )
如果factory 是一个函数, 被定义的模块的值为这个函数的返回值。 否则, 这个模块的值为 factory. 不管factory是不是一个函数, 依赖(dependencies)都会在模块定义前解析。
如果仅仅有factory,并且factory 是一个函数,那么它是一个没有依赖的模块. **
moduleId 可以不用被提供,这个参数是早期手动给AMD loader添加的。只要这里有一个模块恰好被定义,loader将会从给定的依赖列表中,自动产生一个正确的模块标识,并且促使模块被加载。例如, 当使用require([‘matthLIb/arithmetic’]),loader 知道被加载的模块的标识为 “mathLib/arithmetic”.
也有可能加载的依赖角本根本就没有通过define来定义,这将导致解析后的值将会是undefined.
- 模块的创建是懒惰和异步的,不会在调用define时立即运行。这个意思就是factory 不会被执行。并且依赖的模块也不会下载并解析。直到有运行的代码需要这个模块。
- 一旦一个模块的值进入模块空间,它将只会计算一次,而不会每次请求而进行重新计算。在实践层面上, 这个意思是factory只会被调用一次,运算完后返回的值会被缓存并共享给所有请求只模块的代码使用。(注意:dojo loader 有一个非标准的函数require.undef,表明没有返回值);
Require函数的Dependencies和callback参数恰恰如同define中的dependencies和factory参数。 例如
require( ["dijit/layout/TabContainer","bd/widgets/stateButton"],function(TabContainer,stateButton){ // do something with TabContainer and stateButton... } );
Define中的如下
define( ["dijit/layout/TabContainer",stateButton){ // do something with TabContainer and stateButton... return definedValue; } );
两者都可以获得dijit/layout/TabContainer和bd/widgets/stateButton两个模块的值。两者的不同在于, 后者会不会立即调用依赖的包,而且会返回一个值给它的调用者。前者就是简单的调用并运行。
模块标识符
下面的配置变量可以控制加载器如何将模块标识符映射为URL:
- baseUrl:(string) 默认指定的根目录为dojo.js所在的目录,如”lib/dojo/dojo.js” 则baseUrl为 lib/dojo/. 在require([‘dojo/dom’]) 下, baseUrl应该指定为lib/dojo. 如果被定义, has的config-tlmSiblingOfDojo为false.否则为true.
- Paths: (object). 自定义路径。会将模块标识符的一部分映射到路径的一部分。 指定的越详细,优先级越高,如,’a/b/c” 的优先级高于a及’a/b’.
dojoConfig={ paths:{ ‘a/b’:”./custom” } } Require([‘a/b/dom’]) 将会加载custom下的dom.js文件。
- Aliases: (object). 将模块标识符映射为另一个模块标识符。如:
require({ aliases:[ ["text","dojo/text"] ] }); require(["text"],function(text){ //... require(["dojo/text"],function(text){ //... define(["text"],function(text){ //... define(["dojo/text"],function(text){ //...
- Packages: (package 对象数据) 明确定义的包列表。Dojo及dijit是两个packages的列子。 一个包对象包含四个属性:
Name: (string) 包名字。(例:’myapp’); Location: (string) 包存在的路径; Main: (可选,string) 包的入口文件,给定的值为一个模块标识符。 { name: 'dijit',location: 'dojo/dijit',main:'lib/main' }
- packageMap(可选,object) 一个映射关系,允许在这个包内,指定的包可以映射到别外一个包;(这个只在dojo的loader里面存在), 如下例,在utill1的包中,dojox包,指向dojox1. 已被弃用,用map代替
{name: "util1",location: "packages/util1",packageMap: {dojox:"dojox1"}
模块标识符的相对关系
在一个模块定义时的 依赖数组里面的模块标识符,可以给出全局路径,可以是相对路径。例如:
// this is "myPackage/myModule/mySubmodule" define( ["myPackage/utils","myPackage/myModule/mySubmodule2"],function(utils,submodule){ // do something spectacular } );
可以写成以下方式:
// this is "myPackage/myModule/mySubmodule" define( ["../utils","./mySubmodule2"],submodule){ // do something spectacular } );
“.” 可以被认为是当前的模块的文件夹, “..” 当前模块的父文件夹。
注意: 这种相对的模块标识只能用于单个包内。 也就是说,”../” 不能用于顶级的模块标识。所以上面的例子, “../../someOtherPackage/otherModule” 将是无效的。
在一个包的内部模块之间,这种方式是重点推荐。因为使用全称的时候可能会具有相同名字的两个不同的包(或者两个不同版本的同一个同)。 这将会在rolocating module namespaces有更具体的解释
上下文相关的 require
// this is "myApp/topLevelHandlers" define(["dojo"],function(dojo){ dojo.connect(dojo.byId("debugButton"),"click",function(){ require(["myApp/perspectives/debug"],function(perspective){ perspective.open(); }); }); });
这段代码是合法的。 但可以优化的更好。由于这段代码在”myApp/topLevelHandlers”模块内。我们可以重写”./perspectives/debug” 来代替”myApp/perspectives/debug”. 可惜, 全局的require 函数是不知道如何去引用这个参照模块(参照模块的解释是require(“dojo/dom”) 而dom里面有需要依赖”./sniff”,当在loader 在加载sniff里,referencesModule指向的就是dom 模块)。所以如果我们尝试去改变标识符,会导致失败。我们需要一种方式能够记住这个模块供以后使用。我们需要指定模块标识符require在 依赖数据里.
// this is "myApp/topLevelHandlers" define(["dojo","require"],function(dojo,require){ dojo.connect(dojo.byId("debugButton"),function(){ require(["./perspectives/debug"],function(perspective){ perspective.open(); }); }); });
现在的调用require方法是在本地的方法,而不是全局的. Loader 安排本地的require去解析模块标识符。 这种本地的require函数被称为” 上下文 require,context-sensitive require”.
一般的javascript角本注入
一个明确的路径或者URL代表的javascript可以作为模块标识符传递 给require函数。假若这样,角本会被简单的执行,并且返加一个undefined的值。 例如:
Loader的解释器会当下列情况作为普通的角本标识:
require(["http://acmecorp.com/stuff.js"],function(){ // etc. });
Loader的解释器会当下列情况作为普通的角本标识:
- 以协议开送的string(如: http 或 https)
- 以 斜线/ 开头的字符串(如”/acmecorp.com/stuff”)
- 以 .js 结尾的字符。
模块别名
有可能为一个模块创造一个别名,有一个例子,当通过load加载了一个文本资源, 整个应用程序需要做用到这个公用的text插件。RequireJS最先定义模块别名,而其它的库都是依赖于RequireJS定义的模块。 Dojo的实现这个功能, 跟 requires 的实现100%兼容, 并且更小,更多的功能。下面是dojo text module 的别名:
require({ aliases:[ ["text","dojo/text"] ] });
现在,当指定一个模块标识符text. Loader将会 解析”dojo/text”. 换句话说, 在给定了上面的配置之后, 下面语句的返回的结果是一样的。
require(["text"],function(text){ //...
有一种情况需要优先考虑别名。 两个不同绝对模块标识符( 在模块标识符处理的第6步之后)将会导致两个不同的模块被实例化,即使解析到的是相同的路径。 这个意思是你不能使用paths 来解决这个问题。 例如 , baseUrl 指向dojo文件例, 你不能通过以下方式,给 "dojo/text" 取别名为 "text", 如下:
require({ paths:{ "text":"./text" } });
在这种情况下, 假设没有相关的模块引用(只是简单的考虑这个问题). "text" 被解析为 "path/to/dojo/text.js". ("text","path/to/dojo/text.js")(经过paths定义了"text"),而正常的"dojo/text" 会解析为("dojo/text","path/to/dojo/text.js"),两者的mid不同,但是它们指向相同的路径 。 因此loader 还是会创建两个独立的模块实例, 这可能是你不想要的。 为给两个不同的模块标识符被解析到为同一个模块值,唯一的方法是在define时指定明确的ID,或者提供一个别名配置。
解析模块标识符(源代码为getModuleInfo_函数)
下面的步骤主要概述loader内部处理标识符的过程, 处理过程会涉及到模块标识ID,特定情况的上下文require, reference module(引用模块:如在dom.js里面, define(["./sniff","./_base/window"],当要加载dom模块时,需要先加载sniff及 _base/window,那么在这时相对于sniff时,引用模块就是dojo/dom,而dom的引用模块是全局函数require,那么引用模块为0),以及产生一个结果路径或URL(返回一个结果)
- 如果moduleId 协议或者斜线开台,又或者以”.js”结尾。假设该请求的是任意数量的javascript代码,而不是一个模块。在这种情况下,剩下的步骤会直接跳过。
- 如果moduleId是一个相对的(例如,第一个字节为”.”),但没有给定reference module。会抛出一个错误:moduleId is not resolvable. 模块无法解析。(注意:源代码里把这种情况归类到第一步,当以”.”开头,而没有referenceModule,会直接相对于网页的路径去加载moduleId. 如果没有moduleId这个路径,会直接找不到相应文件错误。),所以在全局require中是不能使用相对模块,相对模块主要用于包内的模块定义或者上下文。
- 如果moduleId是相对的,而且给定了参考模块, 设置moduleId为 referenceModule + “/../”+ moduleId,并且通过compactPath函数删除所有相对的路径,如 “..”,“.” ; 以过compactPath处理后,moduleId不会在有相对路径的片段。当经过compactPath处理后,还有 “.” 标识,会抛出一个 moduleId不能解析的错误,源代码是 makeError("irrationalPath",mid) .
- 如果给定了referenceModule, 而且referenceModule是packages配置变量下其中的一个包内, 并且那个包也是packageMap 配置变量的一项,那么用在packageMap中的包名替换最左名的部分(第一个”/”前面那一段,即包名), 主要用于多版本的兼容,可以查看一下 loader 高级使用教程下中的map选项。
- 在配置变量aliases 中查找 第3步计算出来来的moduleId,如果这个moduleId存在别名, 从aliases中获得这个moduleId得到的别名,重新计算(调用getModuleInfo_),很重要一点是, aliases是在packageMap生效之后在引用的。
- 如果moduleId 仅有一部会(没有 "/"), 并且这部分与packages内的包名称相同。 在moduleId末尾添加一个"/" 和 包配置中的 "main" 配置变量的值(例如:moduleId 为 "dojo" 那么而这个真正的模块为 "dojo/main")。 @H_301_23@
目前为此,loader 已经将moduleId 完成统一为了一个绝对模块标识符(也就是说,reference module 不在影响绝对标识符)
7.
从moduleId(a/b/dom)的开头部分开始匹配paths{"a/b":"./custom","a":"./custom"}中最长的部分(即匹配"a/b",而不是"a"). 如果匹配成功, 将paths中正确的值替换moduleId中正确的值('a/b'会
被替换为
8.
如果在第7 步没有匹配到paths 并且moduleId 引用的是一个包中的模块。 相关包的location属性会替换 moduleId的第一部分(包名), 如"dojo/dom",dojo包位于 ./lib/js/dojo/,那么结果
为./lib/js/dojo/dom.
9.
如果第7与第8步都没有匹配成功, 并且有(”config-tmlSiblingOfDojo") 为 true. 配置属性tmlSiblingOfDojo 默认为true. 这时结果为 "../" +moduleId。 意思就是不指定packages及path时,路径
为 loader文件 dojo.js的父路径(如果dojo.js 存在于, js/lib/dojo, 那么 ../ 为 js/lib),更多可以查看 loader的高级使用教程中的tmlSiblingOfDojo.
11. 给result 添加.js 后缀
解析模块标识符例子
在所有这些例子中, 假设的使用的都是默认配置.
dojo
dojo ⇒ dojo/main (Step 6) dojo/main ⇒ ./main (Step 8) ./main ⇒ path/to/dojo/ + ./main ⇒ path/to/dojo/main (Step 10) path/to/dojo/main.js (Step 11)
dojo/store/api/Store
dojo/store/api/Store ⇒ ./store/api/Store (Step 8) ./store/api/Store ⇒ path/to/dojo/ + ./store/api/Store ⇒ path/to/dojo/store/api/Store (Step 10) path/to/dojo/store/api/Store.js (Step 11)../../_base/Deferred with reference module dojo/store/util/QueryResults
../../_base/Deferred ⇒ dojo/store/util/QueryResults + /../ + ../../_base/Deferred ⇒ dojo/store/util/QueryResults/../../../_base/Deferred ⇒ dojo/_base/Deferred (Step 3) dojo/_base/Deferred ⇒ ./_base/Deferred (Step 8) ./_base/Deferred ⇒ path/to/dojo/ + ./_base/Deferred ⇒ path/to/dojo/_base/Deferred (Step 10) path/to/dojo/_base/Deferred.js (Step 11)
myApp
myApp ⇒ ../myApp (Step 9) ../myApp ⇒ path/to/dtk + ../myApp ⇒ path/to/myApp (Step 10) path/to/myApp.js (Step 11)
myApp/someSubmodule
myApp/someSubmodule ⇒ ../myApp/someSubmodule (Step 9) ../myApp/someSubmodule ⇒ path/to + ../myApp/someSubmodule ⇒ path/to/myApp/someSubmodule (Step 10) path/to/myApp/someSubmodule.js (Step 11)
请注意, 假设的baseUrl默认值为dojo的树路径(js/lib/dojo/js,库路径为js/lib/dojo),那么顶级模块标识符"myApp" 就跟dojo 树路径在一个文件夹下。就是“ tmlSiblingOfDojo” 这个文字暗示的一个。
如果myApp的根目录(整个应用程序的文件夹)位于"other/path/to/myApp",这时候需要提供一个paths的配置:
var dojoConfig = { paths:{ "myApp":"/other/path/to/myApp" } };
由于 "other/path/to/myApp" 是一个绝对路径(绝对也是相对于index.html),第10步不会发生。
myApp
paths也是可以映射到路径的相对部分上, 例如, 你有以下的文件结构:
myApp ⇒ /other/path/to/myApp (Step 7) /other/path/to/myApp.js (Step 11)myApp/someSubmodule
myApp/someSubmodule ⇒ /other/path/to/myApp/someSubmodule (Step 7) /other/path/to/myApp/someSubmodule.js (Step 11)
paths也是可以映射到路径的相对部分上, 例如, 你有以下的文件结构:
scripts/ dtk/ dojo/ dijit/ dojox/ myApp/ experimental/
在这个情况下, myApp 不在是dojo的姐妹目录, 但是它依然可以自动获得到baseUrl,指向script/dtk/dojo。 如果给myApp指定一个相对于baseUrl的路径,它依然是有效的:
var dojoConfig = { paths:{ "myApp":"../../myApp" } };
作为结果如下:
myApp
myApp ⇒ ../../myApp (Step 7) ../../myApp ⇒ path/to/dtk/dojo/ + ../../myApp ⇒ path/to/myApp (Step 10) path/to/myApp ⇒ path/to/myApp.js (Step 11)
myApp/someSubmodule
myApp ⇒ ../../myApp/someSubmodule (Step 7) ../../myApp/someSubmodule ⇒ path/to/dtk/dojo/ + ../../myApp ⇒ path/to/myApp/someSubmodule (Step 10) path/to/myApp/someSubmodule ⇒ path/to/myApp/someSubmodule.js (Step 11)
这是一个覆盖tlmSiblingOfDojo的行为。 另一个方法是设置tlmSiblingOfDojo 为false 或者明确指定baseUrl. 假设我们依然采用以上的文件结构, 考虑 这个配置:
var dojoConfig = { baseUrl:"scripts",packages:[{ name:'dojo',location:'dtk/dojo' },{ name:'dijit',location:'dtk/dijit' }] }
注意我们不在需要路径映射了,因为我们设置的baseUrl时, tlmSiblingOfDojo被设置为了false. 标识符现在都是直接相对于baseUrl.
myApp
myApp ⇒ scripts/ + myApp ⇒ script/myApp (Step 10) scripts/myApp ⇒ scripts/myApp.js (Step 11)
myAPp/someSubModule
myApp ⇒ scripts/ + myApp/someSubmodule ⇒ script/myApp/someSubmodule (Step 10) scripts/myApp/someSubmodule ⇒ scripts/myApp/someSubmodule.js (Step 11)
dojo
dojo ⇒ dojo/main (Step 4) dojo/main ⇒ dtk/dojo/main (Step 8) dtk/dojo/main ⇒ scripts/dtk/dojo/ + ./main ⇒ scripts/dtk/dojo/main (Step 10) scripts/dtk/dojo/main.js (Step 11)dojo/behavior
dojo/behavior ⇒ dtk/dojo/behavior (Step 8) dtk/dojo/behavior ⇒ scripts/dtk/dojo/ + ./behavior ⇒ scripts/dtk/dojo/behavior (Step 10) scripts/dojo/behavior.js (Step 11)
如果我们指定myApp为了下包,"myApp"的解析过程将会改变:
var dojoConfig = { baseUrl:"scripts" packages:[{ name:'myApp',location:'myApp' },location:'dtk/dijit' },location:'dtk/dijit' }] };"myApp/someSubModule" 不什么改变,但 'myApp' 的解析如下
myApp
myApp ⇒ myApp/main (Step 4) myApp/main ⇒ myApp/main (Step 8) myApp/main ⇒ scripts/ + myApp/main ⇒ scripts/myApp/main (Step 10) scripts/myApp/main.js (Step 11)
配置packages通常要比将直接将文件夹与一推顶级模块混合在一起好。
通常,你可以在任何地方映射一个模块标识符。 例如,你可能体验下将新模块来代替 dojo/coookie. 这种情况下,你希望所有的dojo模块都正常使用, 只是想dojo/cookie 映射为scripts/experimental/dojo/cookie。 为了实现这个目的,需要在配置选项中 paths添加一条目录:
var dojoConfig = { paths:{ "dojo/cookie":"../../experimental/dojo/cookie" } }
现在,第7步中 dojo/cookie会被特别对待,将会映射到scripts/experimental/dojo/cookie.
最后,考虑下当你想映射的模块标识符正好是一些模块的父路径部分, 考虑以下这个结构
scripts/ myApp/ myApi.js myApi/ helper1.js helper2.js一方面, "myApp/myApi是一个模块, 但它同样的模块myApp/myApi/helper1与 myApp/myApi/helper2的父路径部分。 这个意思是paths中的条目"myApp/myApi":"path/to/another/myApi" 也同样会被映射到两个helper模块。 很多时候, 这是你想要的,但如果不是,你可以直接把helpers模块添加到paths中。
var dojoConfig = { paths:{ "myApp/myApi":"path/to/another/myApi","myApp/myApi/helper1":"path/to/original/myApi/helper1","myApp/myApi/helper2":"path/to/original/myApi/helper2" } }
看起来很繁琐,但你很少会使用到这种配置。
重定位模块命名空间
如果你想在同一时间内用同一个名字来使用两个包, 只要包的作者遵循最佳规范 并且在调用define 函数时没有明确指定moduleId,你可需要简单的将两个包放到不同的文件夹下, 然后在packages中给每个文件夹指定一个包的名称。 例如:
var dojoConfig = { baseUrl: "./",packages: [{ name: "util1",location: "packages/util1" },{ name: "util2",location: "packages/util2" }] };
现在你可以通过require 或 define正常访问这两个包:
define(["util1","util2"],function(util1,util2){ // well that was easy. });
另外有的包需要使用到其它包,那么就需要对其它包进行重映射。 例名:
var dojoConfig = { packages: [{ name: "util1",packageMap: {dojox:"dojox1"} },{ name: "util2",location: "packages/util2",packageMap: {dojox:"dojox2"} },{ name: "dojox1",location: "packages/dojox-version-1-6" },{ name: "dojox2",location: "packages/dojox-version-1-4" }] };
上面的代码会确保所有在"util1"中显示引用"dojox"的包会被转向到 "dojox",而所有在 "util2"中引用"dojox"的包会被转向的“dojox" (注, packageMap已弃用,请用map 代替)
var dojoConfig = { packages: [{ name: "util1",},location: "packages/dojox-version-1-4" }],map:{ util1:{ dojox:"dojox1" },util2:{ dojox:"dojox2" } } };
这种设计取代了 在 dojo v.16中所谓的的多版本设计 同时也消除了在RequireJs 实现需要的上下文。 记住,不同于 多版本设计(multi-version). Build不能部署一个迁移包。 只是通过简单的配置来应对所有的问题。 这个功能虽然强大, 但只有dojo实现了这个功能。
实现功能
require.toUrl 它像解析一个模块标识符为一个路径一样,来获得一个资源的路径。
require.toUrl( id // (string) a resource identifier that is prefixed by a module identifier ) -> string
举个例子, 比如,我们已经定义了一个配置,这个配置将会使模块模识符"myApp/widgets/button" 指向到资源 "http://acmeCopy.com/myApp/widgets/button.js",在此情况下, require.toUrl("myApp/widgets/templates/button.html") 将会返回 "http://acmeCopy.com/myApp/widgets/templates/button.html".
当 require是一个 content-sensitive require时,ids可以是相对的标识符。 如:
define(["require",...],function(require,...){ ... require.toUrl("./images/foo.jpg") ... }
请注意URL是以"./"开头。
require.toAbsMid 将给定的模块ID转化为一个绝对的模块Id,这个函数只有结合context-sensitive require使用时,才能有用。
require.toAbsMid( moduleId // (string) a module identifier ) -> string
require.undef 从模块命名空间上删除一个模块。 require.undef 主要用于测试框架,测试时不需要重新加载整个框架,而只加载和卸载个别模块。
require.undef( moduleId // (string) a module identifier ) -> undefined
require.log等介于当前环境下的console.log,每一个传递的参数,会单独输出到一行。
require.log( // (...rest) one or more messages to log ) -> undefined
require.toAbsMid 及 require.undef 都是 dojo特有的功能,扩展了AMD 规范。
Commonjs 的 require,exports,module
AMD 规范中定义了三个特别的 模块标识符: require(用于上下文中的require),exports(循环依赖时使用) 以及module。
require 模块的使用可参考
Context-sensitive require
module 模块返回一个包含以下属性的一个对象:
- id: 一个唯一的模块标识符字符串, 当它被传递给require时,会返回这个模块的值。
- uri: 模块资源被加载完后的一个URI(有时没用)
- exports: 详情如下
define([],function(){ return { someProperty:"hello",someOtherProperty:"world" }; }); define(["exports"],function(exports){ exports.someProperty = "hello"; exports.someOtherProperty = "world"; });
当在循环依赖时, 为了确保模块被正确定义,唯一的方法就是使用exports对象,并将需要对外爆露的方法或数据添加到export对象上。
如果需要,module.exports 可以完成取代export模块的使用:
define(["module"],function(module){ module.exports = dojo.declare(/*...*/); });
最后,AMD 规范定义, 当define只提供一个工厂函数时, loader 必须像有["require","exports","module"]这样的依赖数组,换句话说,以下两个定义是相等的。
define(["require","module"],module){ // define a module }); define(function(require,module){ // define a module });
后者,在require("foo")形式的函数内部将使用 "foo"作为依赖来扫描,并解析。
所有的这些功能主要是兼容其它CommonJS的模块。 你不应该使用这些功能,除非你需要写一些特定的模块(node.js). 而又不想要求用户去加载一个兼容AMD模范的加载器。 当然如果你需要解决循环依赖时,exports是非常有用的。
插件
- dojo/domReady: 延迟模块工厂函数的执行, 直到整个文档被解析(相当于jQuery 中的 $(document).ready(function(){}) )
- dojo/text: 加载文本资源, 它是RequireJS文本插件的一个超集, 并将加载的结果存入 dojo.cache.
- dojo/i18n: 加载国际化资源包, 包括老版本的格式(v1.6 - i18n API) 及AMD的格式, 是RequireJS 国际资源包的超集。
- dojo/has: 允许使用 has.js的表达式,有条件的加载模块。
- dojo/load: 在运行时加载经过计算得到的模块依赖
- dojo/require: 下载一个老版本的模块而不加载它,就是之前版本中的dojo.require。
- dojo/loadInit: 调用dojo.loadinit函数,这样其它的老版本的 API函数可以被执行,特别是dojo.require来下载的模块。
load( id,// the string to the right of the ! require,// AMD require; usually a context-sensitive require bound to the module making the plugin request callback // the function the plugin should call with the return value once it is done ) -> undefined
这里是一个"text" 插件加载文档的例子:
// this is "myApp/myModule" define(["text!./templates/myModule.html"],function(template){ // template is a string loaded from the resource implied by myApp/templates/myModule.html });
一个简间的 ”text" 插件的实现:
define(["dojo/_base/xhr"],function(xhr){ return { load: function(id,require,callback){ xhr.get({ url: require.toUrl(id),load: function(text){ callback(text); } }); } }; });
不像正常模块返回的值, loader不会将缓存的值传递给插件的callback. 若有必要时,插件可以自己维护一个内部的缓存。
define(["dojo"],function(dojo){ var cache = {}; return { load: function(id,callback){ var url = require.toUrl(id); if(url in cache){ callback(cache[url]); }else{ dojo.xhrGet({ url: url,load: function(text){ callback(cache[url] = text); } }); } } }; });
Window 的 Load事件检测
Dojo loader 可以连接 window.onload事件, 如果document.readyState 尚未设置, 则设置document.readyState为"complete" 。 它使得正常的AMD模块可以依赖document.readyState,其至是不支持document.readyState属性的浏览器。
微事件(loader内部事件)API
require.on = function( eventName,// (string) the event name to connect to listener // (function) called upon event ) require.signal = function( eventName,// (string) the event name to signal args // (array) the arguments to apply to each listener )
loader本身使用require.signal 触发它自己的事件, 客户端可以通过传递一个监听函数require.on来监听loader事件。 例如, 一个客户端可以连接"config"事件来监听 改变的配置,如下:
var handle = require.on("config",function(config,rawConfig){ if(config.myApp.myConfigVar){ // do something } });
注意 "config" 事件提供了两个参数 config 和 rawConfig. 更多的描述可以参考 Configuration 章节。
require.on 返回一个不透明的 handle对象, handle对像可以对过调用 handle.remove()来停止监听。
错误报告
function handleError(error){ console.log(error.src,error.id); } require.on("error",handleError);
传递给监听器的第一个参数是一个loader error对象, 它包含src 和 id 属性, src 通常为 "dojoLoader", id 为一个特定错误的标识符。 loader 定义了以下错误标识符:
factoryThrew
xhrFailed
multipleDefine
一个模块已经被创建了,在次调用 define 来创建这个模块时,会抛出这个错误,如 require(['my/app']) ,会请求js/my/app.js文件,并执行app.js里的define函数。如果你通过在loader有效时,直接通过<script src="js/my/app.js"></script> 来加载这个文件,那么define函数会被在次调用, 创建了两次my/app模块, 所以会发现multipleDefine. 通常这个错误都是因为直接在 html 文档中用script来加载模块。 所以加载模块时请使用loader, 而不要使用<script>标签。 第二个主要原因是在define中明确指定了模块标识符,即define的方法签名中的第一个形参。 请勿在任何情况下指定一个模块标识符。
timeout
definele
调试
异步调试本来就很难,如果是一个高度异步处理就更加棘手。 如加载一个模块树(顶点模块A,A依赖B,B依赖C ....)。 这里有一些点使得调试易于管理:
- 老版本的loader API最常见的程序错误是在模块标识符中使用"."而不是"/"。
- 最常见的语法错误是在依赖列表里缺少一个逗号,而有的浏览器不会报告这个错误。
- 常见的程序错误是,依赖列表的顺序跟回调函数或者工厂方法的顺依不一样。 通常会出现" object is not a constructor" 或者 "method dose not exist" 或 相类似的。
- 在一些浏览器的某些情况下,插入一个断点的过程中将会改变异步请求的顺序, 断点被插入时导致应用程序失败。 这表明模块在被定义时已经确定了依赖的顺序。精心设计的AMD应用程序将不会有这些规定。
loader 也可以通过在require object,暴露调试期间的检查的内部状态,如下:
async
Boolean, 标识当前使用的loader 是否为 aysnchronous.
legacyMode
String,表示 loader运行在legacyMode( 如果async为false)
baseUrl
配置中的baseUrl变量
paths
配置中的paths变量
packs
package 配置, 是所有被传递的package的集合
waiting
返回一个loader已经请求但还没成功的模块列表, 如果loader 看起来已经停止, 第一先去查看你浏览器的deugger的网络面板中的404错误,第二在看这里。
execQ (exec queue)
modules
模块的命名空间,loader 通过这个modules中的条目来获得每一个模块的所有信息
- result 模块的值
- injected 加载的状态( 0 表示 "requested",1 表示 "arrived")
- executed 工厂函数执行的状态 ( 0 表示 "executing",1 表示 "executed")
- pid 模块包的名称(如果有的话)
- url 模块中定义的资源的地址
- def 工厂函数
警告:这些被爆露的内部定义只在调试时有用。 不能将它们用于你的代码。 因为它们的结构可能会被改变。
追踪
由于 loader的异步特性,有时最好的技术是可以解决加载中的问题,让loader正常运行,而不需要使用任何的断点及分析loader事件发生的顺序,如一个模块的injecting, defining, executing。 loader 的源版本(从官方网站下载的源文件的版本)包含一个有利于调试技术的 tracing API. 如果有需要, tracing API也可以用于你自己的代码。
tracing API 含有以下的方法签名:
require.trace = function( groupId,// (string) the tracing group identifier to which this trace message belongs args // (array of any) additional data to send with trace ) -> undefined require.trace.set( groupId,// (string) a tracing group identifier enable // (boolean) enable or disable tracing of messages from groupId ) -> undefined require.trace.set( groupMap // (object:groupId --> boolean) a map from trace group identifier to on/off value ) -> undefined require.trace.on // (boolean) enable/disable all tracing require.trace.group // (object) a map from trace group id to boolean
为了触发追踪的消息, 可以传递groupId 以及信息数组传定给require.trace .
当 require.trace(groupId,args) 被调用时, 以下是它的处理过程:
require({ trace:{ "loader-inject":1,// turn the loader-inject group on "loader-define":0 // turn the loader-define group off } });
另外, require.trace.set 可以被直接调用。 有以下两种方式:
require.trace.set({ "loader-inject":1,// turn the loader-inject group on "loader-define":0 // turn the loader-define group off });
或者
require.trace.set("loader-inject", 1); require.trace.set("loader-define", 0);
所有的跟踪暂停可以通过设置require.trace.on 为 false. 设置require.trace.on 为 true 只对groups 单独设置为true的有效。
loader 定义了以下 追踪数组:
loader-inject
当一个模块被注入到应用程序时触发。 如果模块已经被缓存,那么 args[0] 为 "cache",如果模块是通过 XHR(ajax)请求的方式注入,那么args[0]为 "xhr",如果模块是通过script方式,那么args[0]为 "script". Args[1] 为模块标识符; args[2]是 URL/filename; 如果 args[0] 为"xhr",args[3] 将会为 true;
loader-define (完成模块定义)
当define调用完时触发, args[0] 为模块标识符, args[1] 为依赖数组。 注意:args给出的是这些参数的解码值(如A 依赖 B,解码值为运行完 B模块 的define而返回的值),而不是arguments[0]以及arguments[1] (不是 "B" 这样的字符串) 的实际的值。通常在所有的define 调用处理完成之前, loader 不会实际的处理 define函数的调用。 define调用的处理过程可以查看以下的"loader-define-module".
loader-exec-module (等待进入工厂方法,是一个持续进行的一个过程)
当loader 在第一次追踪模块的依赖树并运行所有依赖模块的工厂方法过程进行时,尝试(能)或者不能运行模块的工厂方法时触发(如A依赖 B,当前正在运行 B的define方法, 那么A的状态就是等待尝试运行 A的模块,触发loader-exec-module)。能否成功运行工厂方法是没有保证的。 如果一个依赖的模块不能被解析(可能还没有抵达), 则 attempt 会被中止(abort,代表失败),并在之后重新尝试, args[0] 如果是'exec' 则表示尝试, 'abort' 为失败。 args[1] 则为模块标识符。
loader-run-factory (准备进入工厂方法)
loader-define-module (完成依赖的模块定义,准备返回到上一级模块)
当loader即将处理上一个define(A依赖B,执行完B后,即将执行A的define) 时触发, 可以查看之前的loader-define. args[0] 为模块标识符
loader-circular-dependency
当loader 发现一个循环依赖时触发, 有可能会有一个程序错误。
非浏览器环境
在 v1.7 版本中,可以在Rhino 和 node.js 中直接使用 dojo loader。 如下,在node.js 的命令行加载dojo loader :
#!/bin/bash node dojo/dojo.js load=config load=main
Rhino:
#!/bin/bash java -jar util/shrinksafe/js.jar dojo/dojo.js baseUrl=file:///full/path/to/dojo/dojo load=config load=main
老版本API
为了兼容版本v.16及之前的版本, v1.7的loader 也包括了同步加载API( dojo.provide,dojo.require,dojo.requireLocalization,dojo.requireIf,dojo.requireAfterIf,dojo.platfromRequire,以及dojo.loadInit),跟 之前的loader没有什么区别, 只有一个例外:
dojo.provide("module.that.defines.a.global"); var someVariable = anAwesomeCalculation();
如果上面的代码是在全局作用域内执行,someVariable将会进行到全局空间。 如果是在一个函数作用哉内, someVariable 是一个本地变量 而且会在函数返回时消失。
在 v1.7以上版本, 所有的代码会被当成文本来下载,并且在函数作用域内 通过eval来拆行。 如果你获得的代码如上,并且期望someVariable被定义在全局空间里, 它将在v1.7版本中无效。 为了创建一个全局的变量,可以将属性添加到 dojo.global 上。
dojo.provide("module.that.defines.a.global"); dojo.global.someVariable = anAwesomeCalculation();
工作模式
v1.7 loader 可以在同一个应用里加载 加载老版本的模块和AMD的模块。 它允许老版本API的客户应用程序使用dojo,digit,及被AMD重写的其它库。 这种情况下, loader 必须是同步模式, 因为用老版本 API写的模块不能通过异步加载。
v1.7 loader 的legacy mode 下有两种模式。 synchronous (同步) 和 跨域(异步).
Legacy Synchronous 模式
在这种模式下, 唯一的不同是v1.7 的loader 和 先前的loader 如何处理模块的值。 不同于正常的AMD API 操作, legacy synchronous 模式将导致依赖会被立即解析及工厂函数直接运行。 即使相关的模块还没有被使用(即使这个模块没有被dojo.require请求)。
loader 也可以将dojo.require请求的AMD模块的返回值赋值给 在dojo.require给定的对象, 只要该对象在dojo.require被调用用的时候为undefined. 这种行可以通过has配置config-publishRequireResult为false来限制。
Legacy Cross Domain 模式
一旦loader进入到cross-domain 模式,legacy modules 开始异步执行。 如果loader恰巧是在追踪由几个相互依赖的老版本模块组成的依赖树的中间,之后任何dojo.require的调用会立即返回,而不执行模块。v1.6也有这种特性。
配置参考
配置变量
async (true,false/"sync","legacyAsync")
如果为true,loader 为 AMD模式, 如果为false 或者 "sync",loader 为同步模式, "legacyAsync" loader为遗弃的跨域模式, 默认值为 false.
baseUrl (string)
当模块标识符转化为路径时 baseUrl会添加到路径的最前面。 在浏览器环境下,默认值为 dojo.js的路径。非浏览器下为当前的工作目录。
packages (array of package configuration objects)
包的定义可以查看
模块标识符的章节,默认值的定义可以查看
Default Configuration.
packagePaths (object)
一个简化的符号,可以用来给相同根路径的多个包指定配置。 特别的是包的 location 的计算是 将 map 键(指定的根路径)+ 每个包的name。 一个包的配置对象也可以 string. 字符串就代表包的名字, main 和 packageMap的默认值。 例
packagePaths:{ "path/to/some/place":[ "myPackage",{ name:"yourPackage",main:"base" } ] }
相当于
packages:[{ name:"myPackage",location:"path/to/some/place/myPackage" },{ name:"yourPackage",location:"path/to/some/place/youPackage" }]
别名 (键值对数组(两个一组))
可以查看模
块标识符中的定义,第一个元素可以是一个正则表达式, 表明给多个模块取一个别名。或者为字符串, 表名只是给单个模块取的别名。 在键值对的第二个元素通常为字符串(绝对的模块标识符),即为真实的模块名称。
hasCache : (map: has feature name --> ( 布尔值或者函数) has 特性检测或者值)
提供has 特征的值的一个集合, 默认值可查看
Default Configuration
waitSeconds (number)
loader 请求模块的等待时间(秒),如果在指定的时间内,模块没有抵达, 抛出 timeout error. 任意模块被请求成功后,计时器重新计算。 默认值为 0 (一直等待)
cacheBust (boolean)
deps ( array of module identifier strings) /callback(function)
这些个配置变量只在loader被加载前有效。 一旦loader 被加载, 将导致 laoder去执行 require(deps,callback)。
stripStrict(boolean)
默认的配置
到现在为此整个文档已经写完, 这里是dojo loader的默认配置。 最新的配置可以查看dojo.js文件。
{ // the default configuration for a browser; this will be modified by other environments hasCache:{ "host-browser":1,"dom":1,"dojo-amd-factory-scan":1,"dojo-loader":1,"dojo-has-api":1,"dojo-inject-api":1,"dojo-timeout-api":1,"dojo-trace-api":1,"dojo-log-api":1,"dojo-dom-ready-api":1,"dojo-publish-privates":1,"dojo-config-api":1,"dojo-sniff":1,"dojo-sync-loader":1,"dojo-test-sniff":1,"config-tlmSiblingOfDojo":1 },packages:[{ // note: like v1.6-,this bootstrap computes baseUrl to be the dojo directory name:'dojo',location:'.' },{ name:'tests',location:'./tests' },{ name:'dijit',location:'../dijit' },{ name:'build',location:'../util/build' },{ name:'doh',location:'../util/doh' },{ name:'dojox',location:'../dojox' },{ name:'demos',location:'../demos' }],trace:{ // these are listed so it's simple to turn them on/off while debugging loading "loader-inject":0,"loader-define":0,"loader-exec-module":0,"loader-run-factory":0,"loader-finish-exec":0,"loader-define-module":0,"loader-circular-dependency":0 },async:0 }