Dojo AMD加载器的高级使用教程 <2>

在学习Dojo的时候,第一个问题就是加载器的原理,但在看dojo.js这个加载器的源码的时候,因为没有基础,只知道AMD的规范require 及 define. 所以没办法理清楚整个加载器的代码结构。所有先学习如何来使用 AMD规范才是首要任务,在去学习dojo loader原理,在去学习源代码实现,所以有了这遍文章。


Dojo 已经支持模块用AMD规范来写, 通过这种方式,模块更注意书写及调试,本教程中, 我们将学习所有关于这个新的模块格式,及如何在自己的应用里面使用它。

* 本教程是在 AMD 加载器基础教程之后,所以首先请确定你已经了解了基础 的AMD规范。

贯穿整个教程,我们都将参考一个假设的应用程序, 它的文件结构如下


/
    index.html
    js/
        lib/
            dojo/
            dijit/
            dojox/
        my/
        util/

就像你看到的,整个文件结构跟 上一个教程讨论的不一样。 所以为了更好的使 loader工作,我们将学习如何配置loader. 但首先让我们在学习一下 require 及define更多的功能。

探究更深层次的require

require 函数接受以下三个参数:

  1. configuration(可选,默认为 undefined):  一个loader的配置对象, 它允许你在运行的时候重新配置loader.
  2. dependencies(可选,默认为 空数组[]):  一个包含模块标识符的数组, 如果指定了这个参数, 这些模块会在你的代码运行前被解析。 它们会依据有列表里的顺序加载,也按照列表里的顺序作为参数传递给回调函数。
  3. 回调函数: 一个函数,里面包含你想要运行的代码,这个函数依赖的模块在会上一个参数dependencies指定。 为了支持异步加载及能够使用这些代码的非全局引用,所以你必须将代码封装在这个回调函数里面。
*  第一个configuration 参数可以简单的省略掉,不需要指定一个占位符 , 意思是不需要 require({},['dojo/dom'],function(dom){});

我们会在之后更加详细的了解配置一个loader. 现在我们将演示一个通过require函数来配置loader 的一个例子:

require({
    baseUrl: "/js/",
    packages: [
        { name: "dojo", location: "//ajax.googleapis.com/ajax/libs/dojo/1.9.2/" },
        { name: "my", location: "my" }
    ]
}, [ "my/app" ]);

在这里,我们稍稍的改变了loader的配置,将原本指向本地的包dojo(js/lib/dojo)指向了 google 的CDN. 跨域加载在AMD格式中隐式的实现。

* 注意并不是所有的配置都可以在运行时设置, async, tmlSiblingOfDojo, 以及之前提及的has 测试一定loader 文件被加载后就不能改变了。 别外, 很多配置数据都是浅拷贝,意思就是你就不能使用require来改变配置。 例如,给自定义的配置对象添加更多的键 - 这个对像将会被覆盖掉。

探究更深层次的define

define 函数接受以下参数:
  1. moduleId(可选,默认为undefined): 一个模块标识符, 这个参数主要是较早的AMD loader 的历史遗留问题或者是之前dojo 的AMD版本。建议在现在的版本中不要提供此参数。
  2. dependencies( 可选,默认为空数组[]):  一个你需要依赖的模块标识符的数据。 这些模块会在你代码执行前被解析, 解析后作为参数传递给你的工厂函数。
  3. factory: 模块的值,或者是一个有返回值的工厂函数。
记住一点,当你定义一个模块的时候, 工厂函数永远只会被调用一次,它的返回值会被loader缓存。在实际操作上,加载同一个模块时,模块可以非常简单的共享对象(好比其它语言里面的静态属性)。

当定义一个模块, 它的值可以是一个普通对象。

// in "my/nls/common.js"
define({
    greeting: "Hello!",
    howAreYou: "How are you?"
});

记住,如果你定义的模块没有使用工厂函数, 你将不能引用任何其它依赖的模块。 所以此类形的模块定义很少使用,仅被用于i18n或者简单的定义配置对象。

loader  是如何工作的?

当你调用require去加载某个模块时,  loader  首先查加这个某块的代码,然后将它作为一个参数传递回调函数,这样你可以使用这个模块了。
  1. 首先loader需要解析你传递的模块标识符"dojo/dom", 这涉及到将baseUrl 与模块标识符本身结合。还需要考虑其它配置选项的修改, 比如 map(之后会详细讨论)
  2. 经过第一步, 现在loader已经有了这个模块的URL, 然后可以通过创建一个script元素,设置script的src 模块的url来加载实际的文件。
  3. 一旦文件被加载以及被加载的代码被运行后, 它的结果会被设置为这个模块的值。
  4. loader 维护拥有每个模块的引用,所以下次需要请求这个模块时,loader 将会返回一个已经存在的模块。
当一个AMD模块(文件)被加载完成时,网页会新建一个script元素,代码会被插入到这个script中, 然后define 函数被调用。 上面相同的处理过程会用来加载define中指定的模块。 然后加载器会引用你模块factory返回的值。(如果你传递一个值,而不是一个函数给到define. 那么加载器引用的就是这个值)
// my/app.js

defined("5")

// index.html

require(["my/app"],function(app){
    console.log(app);  //output 5
})


配置Loader

为了兼容老的版本, Dojo 的 loader 默认情况下是以同步的方式运行。如果需要使用异步模式,需要将configuration的属性 async 设置为true: 
<script data-dojo-config="async: true" src="js/lib/dojo/dojo.js"></script>
除非你只想使用同步模式,否则你应该养成上面的输写习惯。下一步我们需要配置一些让loader知道我们模块在哪里的信息。
var dojoConfig = {
    baseUrl: "/js/",  //服务器绝对路径,如果需要相对于index.html, 请指定为./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" }
    ]
};

*  记住,你必须在加载dojo.js之前设置dojoConfig变量, 如果你还不了解, 可以参考 Dojo 配置教程  .

让我们看看使用到的配置选项:

1. baseUrl (默认值为 dojo.js 的文件夹,js/lib/dojo) : 定义要加载的包的根路径。 例如, 如果你需要加载的模块为"my/widget/Person", 加载器会从以下路径加载: 
/js/my/widget/Person.js
它允许我们在文件系统里方便的存储,只需设定一个根目录,其它的部分只要相对于根baseUrl就行了。 而不需要每次都是 require(['js/my/widget/Person'], 我们可以简单如 require(["my/widget/Person"])。

2. tlmSiblingOfDojo( 默认值为true),  默认情况下, loader 期望找到的模块文件所在的文件夹都是 包含loader(dojo.js)文件夹的同级目录。 如果你的文件结构如下所示:
/
    js/
        dojo/
        dijit/
        dojox/
        my/
        util/

这样,你就不需要去配置baseUrl 或者 tlmSiblingOfDojo, 你的顶级模块(top-level modules) 就跟包含dojo.js的文件夹同级(如模块app.js位于my文件夹内,而你没有设置baseUrl, 那么,你可以在index.html 直接require(["my/app"]), dojo与dojox同样的道理 require(['dojo/dom']))。  top-level modules siblings of dojo 的简写为tlmSiblingOfDojo。

3. packages: 一个包配置对象的数组, 在最基本的层面上,packages 就是模块组的简单集合。 dojo, dijit 以及 dojox都是packages的例子。 不像所有的模块集合在一个文件夹内, 包具有许多额外的功能,显著提升了模块的可移值性以及易用性。 一个包是可以独立的手动复制粘贴也可以通过工具来安装(cpm). 一个包的定义可以有如下几个选项:
  • name: 包的名称. 它应该与模块组的文件夹名称相同
  • location: 包的路径; 可以是一个相对于baseUrl的相对路径,也可以是一个绝对路径。 我们更希望于从dojo package来加载模块,而不是路径的方式如”dojo/dom" 而不是"lib/dojo/dom"(再看看本文开头的文件结构), 所以我们指定dojo包的 location属性为"lib/dojo".  它会告知loader,试图加载"dojo/dom"模块都应该是加载"/js/lib/dojo/dom.js"文件(记住, baseUrl "js" 文件夹会被添加到文件名的前面)
  • main (可选,默认为 main.js): 如果只是指定了包的名字,那么指定的这个文件会被当成包的入口文件。 例如, 如果你请求”dojo",那么实际被加载的文件是"js/dojo/main.js".   由于我们在"my" package 里面重写了这个属性, 如果有人请求"my", 他们将实际加载的是 "js/my/app.js".
* 如果我们尝试请求"util", 而它还是一个没有被定义的包, 加载器会尝试加载"js/util.js". 你应该总是定义在loader 的configuration里面定义所有的包。

使用可移值性模块

新的AMD loader 其中最重要的特征之一就是可以创建完全可移值性的包。 例如, 如果你有一个应用程序需要使用dojo两个不同版本的包时,新的加载器很容易实现。

假设你有一个应用程序是在dojo的老本版上开发的,而你想升级到最新的1.9版本, 但有些升级会导致你旧的代码失效。 所以新写的代码可以使用当前的dojo版本,而旧的代码使用老的dojo版本。这个完成可以通过 配置属性configuration来实现:

dojoConfig = {
    packages: [
        { name: "dojo16", location: "lib/dojo16" },
        { name: "dijit16", location: "lib/dijit16" },
        { name: "dojox16", location: "lib/dojox16" },
        { name: "dojo", location: "lib/dojo" },
        { name: "dijit", location: "lib/dijit" },
        { name: "dojox", location: "lib/dojox" },
        { name: "myOldApp", location: "myOldApp" },
        { name: "my", location: "my" }
    ],
    map: {
        myOldApp: {
            dojo: "dojo16",
            dijit: "dijit16",
            dojox: "dojox16"
        }
    }
};

这里发生了什么?
  • (行3-5) 首先我们定义了3个包,3个包的路径指向包含dojo 1.6版本的文件夹
  • (行6-8) 接着我们定义了当前dojo版本的包
  • (行9-10)我们定义了自己开发的老版本及新版本的代码。
  • (行12-18) 我们定了了一个map配置: 它只在 "myOldApp" 模块内有效, 在“myOldApp"内的所有请求"dojo", "dijit' 以及 "dojox" 都映射到"dojo16", "dijit16' 以及 "dojox16". 
  • "my" 包内的模块请求dojo, digit,dojox包中的模块,都将使用dojo当前的版本。

你可以参考AMD 配置文档来了解更多map信息

* 如果你已经对loader比较熟悉,特别是packageMap 已经被弃用。 map配置方法是新的使用方法。


创建可移值性的模块

你应该确保在同一个package内的模块之前相互引用,应当使用相对标识符(relative module identifiers),  下面给出一个模块的示例:

// in "my/widget/NavBar.js"
define([
    "dojo/dom",
    "my/otherModule",
    "my/widget/InfoBox"
], function(dom, otherModule, InfoBox){
    // …
});

将请求 "my package"包内的模块, 使用相对模块标识符替换:

// in "my/widget/NavBar.js"
define([
    "dojo/dom",
    "../otherModule",
    "./InfoBox"
], function(dom, otherModule, InfoBox){
    // …
});


相对于" my/widget/NavBar" :

  • "dojo/dom" 是另外一个包, 所以我们必须使用标识符全称.
  • "my/otherModule" 是一个上级目录, 所以我们使用 "../"
  • "my/widget/InfoBox" 是在同一个同录,所以我们使用"./" . ( 如果你仅指定 " InfoBox" 为一个包名,所以你标识符必须以"./" 开头。
* 记住相对标识符只能用在同一个包之前的模块。 相对标识的ID也是能在define内有效。 它们不能作为依赖列表(dependency)传递 给require. 


考虑一下同一个包中相对标识符的限制。 首先让我们回顾一下map的例子, 你有发现哪有错误吗? 简单的讲,我们只关注了自己写的应用程序使用新老两个版本(myOldApp 使用dojo16, app使用dojo)。  然而我们忽视了一个重要的事情,库的依赖 - dijit 依赖 Dojo, DojoX依赖Dojo及Dijit.  以下的配置将会确保这些依赖可以得到解析。 为了安全起见, 我们也将包映射到了他们自己(map:{ dojo16: {dojo:"dojo16"}}) ,在这种情况下的任何模块都不能使用相对标识符(测试了下,好像可以使用相对标识,不知道说的是什么样的一个情况)。


var map16 = {
    dojo: "dojo16",
    dijit: "dijit16",
    dojox: "dojox16"
};
 
dojoConfig = {
    packages: [
        { name: "dojo16", location: "lib/dojo16" },
        { name: "dijit16", location: "lib/dijit16" },
        { name: "dojox16", location: "lib/dojox16" },
        { name: "dojo", location: "lib/dojo" },
        { name: "dijit", location: "lib/dijit" },
        { name: "dojox", location: "lib/dojox" },
        { name: "myOldApp", location: "myOldApp" },
        { name: "my", location: "my" }
    ],
    map: {
        dojo16: map16,
        dijit16: map16,
        dojox16: map16,
        myOldApp: map16
    }
};


有条件加载模块

有时,你想在满足某条件的情况在加载一个模块。 例如, 你可能想在一个事件(点击)发生后在延迟加载一个模块(不是一开始加载)。 这很简单,如下所示:

// in "my/debug.js"
define([
    "dojo/dom",
    "dojo/dom-construct",
    "dojo/on"
], function(dom, domConstruct, on){
    on(dom.byId("debugButton"), "click", function(){
        require([ "my/debug/console" ], function(console){
            domConstruct.place(console, document.body);
        });
    });
});


上面代码不好的就是可移性不好,所以需要将”my/debug/console ”  转化为相对路径。 仅仅是将"my/debug/console"变为"./debug/console"又不能使require工作, 因为当require被调用的时候原来模块的上下文已经丢失。 为了解决这个问题, Dojo loader 提供了一个上下文加载器 (context-sensitive require).  使用时,需要在define 的依赖列表中指定一个特别的模块标识符"require".

// in "my/debug.js"
define([
    "dojo/dom",
    "dojo/dom-construct",
    "dojo/on",
    "require"
], function(dom, domConstruct, on, require){
    on(dom.byId("debugButton"), "click", function(){
        require([ "./debug/console" ], function(console){
            domConstruct.place(console, document.body);
        });
    });
});

现在,内部的require的调用使用内部绑定,可以安全模块在"my/deubg"中使用相对路径来加载模块。

*   上下文为什么会丢失?
记住, require是一个全局定义的函数。 当你执行"click"时, 唯一的上下文是定义"click"事件模块的作用域,它不知道模块是什么.   在局部作用域内没有require函数, 所有全局的require函数会被调用。 回忆下我们之前说的文件结构, 如果我们传递"./debug/console" 给到全局的require, 它将试着去加载" js/debug/console.js", 但不存在这个文件。 
当你使用content-sensitive require(上下文require), 我们可以局部的引用一个修改的require函数,它可以维持模块的上下文环境, 所以它能正确的加载"/js/my/debug/console.js".

Content-sensitive require 也可以为一个模块来用来加载资源(图片,模板, css)。 如以下的文件结构

/
    js/
	    my/
		    widget/
			    InfoBox.js
				    images/
						info.png

在 InfoBox.js 内,我们可以通过调用require.toUrl来获得"info.png"的完整路径,然后将图片的src设置为这个路径。


处理循环依赖

当你在开发时,你可以会偶尔遇到两个模块之前互相引用的情况,这种互相引用称为循环依赖。为了解析循环依赖,loader会立即解析递归中的第一个模块,以下是代码示例:

// in "my/moduleA.js"
define([ "./moduleB" ], function(moduleB){
    return {
        getValue: function(){
            return "oranges";
        },
 
        print: function(){
            // dependency on moduleB
            log(moduleB.getValue());
        }
    };
});
 
// in "my/moduleB.js"
define([ "./moduleA" ], function(moduleA){
    return {
        getValue: function(){
            // dependency on moduleA
            return "apples and " + moduleA.getValue();
        }
    };
});
 
// in "index.html"
require([
    "my/moduleA"
], function(moduleA) {
    moduleA.print();
});

看起来会输出" apples and oranges", 但是你反而获得一个发生在moduleB的错误: Object  has no mathod ' getValue',  看看在运行"index.html"后, loader会做些什么。

  1. 解析传递给require的依赖(在index.html)的模块:  moduleA
  2. 解析moduleA的依赖:moduleB
  3. 解析moduleB的依赖:moduleA
  4. 检测加载器当前正在尝试处理解析moduleA(即发现这是循环调用)
  5. 临时地将解析moduleA设置为空数组,退出循环依赖
  6. 通过调用moduleB的工厂函数,恢复moduleB的解析。 代表moduleA的空对象会被传到给工厂函数。
  7. 设置moduleB的引用为工厂函数返回的值。
  8. 通过调用moduleA的工厂函数,继续moduleA的解析。
  9. 设置moduleA的引用为工厂函数的返回值, 虽然整个loader现在引用了一个有效的值。但是moduleB依然引用的是一个空对象。
  10. 执行moduleA.print - 由于moduleB 只是引用到一个空对易用。 当调用moduleA.getValue时会获得一个错误。

为了解决这个问题, loader 提供了一个特别的模块标识符 "exports". 当使用时, 使用exports的模块将返回一个 代表模块被定义时的永久对象,这个对象初始化为空, 但任何相关的模块(依赖使用了exports的模块)在解析的时候都会引用这个持久对象(空对象)。 同样的引用(持久对象)也会被传递到依赖列表里的"exports".。 对于这个执久对象的引用, 我们可以直接给这个对象定义属性。 事情发生的顺序有点不好理解。 还是来看看代码:


// in "my/moduleA.js"
define([ "./moduleB", "exports" ], function(moduleB, exports){
    exports.getValue = function(){
        return "oranges";
    };
 
    exports.print = function(){
        log(moduleB.getValue());
    };
});
 
// in "my/moduleB.js"
define([ "./moduleA" ], function(moduleA){
    return {
        getValue: function(){
            return "apples and " + moduleA.getValue();
        }
    };
});
 
// in "index.html"
require([
    "my/moduleA"
], function(moduleA) {
    moduleA.print();
});

现在当你运行index.html 会发生什么?

  1. 解析传递给require函数的依赖: moduleA
  2. 解析 moduleA的依赖: moduleB
  3. 解析  moduleB的依赖:moduleA
  4. 检测加载器当前正在尝试处理解析moduleA(即发现这是循环调用)
  5. 临时地将解析moduleA设置为空对象,退出循环依赖
  6. 通过调用moduleB的工厂函数,恢复moduleB的解析。 代表moduleA的空对象(持久化对象,跟上一个例子不同的一个对象)会被传到给工厂函数
  7. 设置moduleB的引用为工厂函数返回的值。
  8. 通过调用moduleA的工厂函数,继续执行模块A - 已经创建好的moduleA的占位符对象会作为exports参数,传递给工厂函数。
  9. 在解析完一个含及”exports"的依赖关系的模块后(moduleA),loader对这个模块的引用不在是工厂函数返回的值,而是moduleA的那个已经被创建占位符并当作exports参数传递给moduleA工厂函数创建的对象。
  10. 执行moduleA.print - 由于moduleB 含 有一个有效的被moduleA填充的引用。 当它调用moduleA.getValue时,会被正确执行。
一定记住的是,虽然使用导出来提供一个最终有效的引用。 在解析完 依赖模块时(moduleB),它依然只是一个空对象。 当你的工厂函数 (moduleB)被执行时,它将收到一个moduleA的的空对象引用。  这个对象仅会在循环依赖完全解析后(模块A 会解析为空对象{}, 模块B正常解析, 然后模块A正常解板),才会更新为moduleA的方法和属性。之后的调用,才对moduleB工厂函数中的方法有效。 以下的代码演示两者之前的区别:
// in "my/moduleA.js"
define([ "./moduleB", "exports" ], function(moduleB, exports){
    exports.isValid = true;
 
    exports.getValue = function(){
        return "oranges";
    };
 
    exports.print = function(){
        // dependency on moduleB
        log(moduleB.getValue());
    }
});
 
// in "my/moduleB.js"
define([ "./moduleA" ], function(moduleA){
    // this code will run at resolution time, when the reference to  这段代码将运行在模块解析时, 这时引用的模块A为一个空对象,所以moduleA.isValid 是一个undefined.
    // moduleA is an empty object, so moduleA.isValid will be undefined
    if(moduleA.isValid){
        return {
            getValue: function(){
                return "won't happen";
            }
        };
    }
 
    // this code returns an object with a method that references moduleA  这段代码将会返回一个引用了moduleA的方法的对象,getValue不能在moduleA没有被实际解析之前调用。 由于模块A中使用了exports. 所以可以调用moduleA的getValue()方法.
    // the "getValue" method won't be called until after moduleA has       
    // actually been resolved, and since it uses exports, the "getValue"
    // method will be available
    return {
        getValue: function(){
            return "apples and " + moduleA.getValue();
        }
    };
});
 
// in "index.html"
require([
    "my/moduleA"
], function(moduleA) {
    moduleA.print();
});

加载非AMD规范的代码

在教程的模块标识符章节,我们有提到过 AMD Loader 可以通过javascript 文件路径来加载non-AMD 代码。 loader通过三个方法来识别这种特殊的模块标识符。
  • 以 "/" 开头的标识符
  • 以协议开头的标识符(如”http:", "https")
  • 标识符以".js" 结尾
当任意的代码作为模块被加载时,模块被解析的值为undefined. 你需要直接使用这些代码,代码必须在全局中定义。

Dojo 有一个其它AMD加载器没有的功能是,它可以混合使用老版本的dojo模块和AMD格式的模块。 这种方法使得从老版本慢慢过渡到AMD规范。 可以同时使用异步或同步的方法。 当处于异步模式时, 一旦角本被执行时,传统模块解析的值会存在于一个全局作用域变量,变量名为dojo.provide() 提供的名称。

// in "my/legacyModule.js"
dojo.provide("my.legacyModule");
my.legacyModule = {
    isLegacy: true
};


AMD laoder 可以通过调用require(["my/legacyModule"])来加载这段代码, 这个模块解析的值会做为一个对象赋值给my.legacyModule.

服务器端Javascript

新的AMD loader最后一个特点就是可以在服务器端(node.js或者Rhino)加载Javascript. 通过dojo可以通过下面的命令行。
# 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

查看更多 dojo and nodejs 教程

每一个load= 参数,加模块添加到依赖链表,当loader准备好后会自动解析。 在浏览器端相当于
<script data-dojo-config="async: true" src="path/to/dojo.js"></script>
<script>require(["my/serverConfig", "my/app"]);</script>

你可能感兴趣的:(源代码,dojo,amd)