原文地址:https://dojotoolkit.org/documentation/tutorials/1.10/modules_advanced/index.html
GitBook地址:https://www.gitbook.com/book/limeng1900/dojo1-11-tutorials-translation-in-chinese/details
轉載請註明出處http://blog.csdn.net/taijiedi13/ – 碎夢道
Dojo現在支持以異步模塊定義格式來編寫模塊,從而使代碼更容易編寫和調試。本教程中,我們將全面瞭解這個新模塊格式並探索如何用它來編寫一個應用。
本教程是 Introduction to AMD的後續,所以請先確認已理解AMD基礎。
本教程中,我們將涉及一個假設的應用,它的文件系統結構如下:
/
index.html
js/
lib/
dojo/
dijit/
dojox/
my/
util/
你可以看到這個結構和之前教程中討論的結構並不一樣,我們將解釋如何配置加載器讓它工作。不過首先通過更多的細節先回顧下require
和define
…
深入require
require
函數接受以下參數:
- 配置(可選,default=undefined):一個帶加載器配置選項的對象——允許你在運行時重新配置加載器。
- 依賴關係(可選,default=[]):模塊標識符的數組。指定的模塊會在你的代碼生成之前解析 。它們會按照列出的順序加載,並且按順序以參數的方式傳遞給你的回調函數。
- 回調:包含你想要運行的代碼的一個函數,這個函數依賴於依賴關係裏的模塊。你需要將你的代碼包裹在回調函數裏以支持異步加載和使用模塊的非全局引用。
配置參數可以省略,但是必須加上空的佔位符。
接下來將涉及加載器配置的更多細節;現在有一個使用配置參數的require
:
require({
baseUrl: "/js/",
packages: [
{ name: "dojo", location: "//ajax.googleapis.com/ajax/libs/dojo/1.10.4/" },
{ name: "my", location: "my" }
]
}, [ "my/app" ]);
這裏我們略微修改配置信息讓dojo包指向Google CDN。跨域加載支持隱含在AMD格式裏。
請注意不不是所有的配置選項可以在運行時配置。尤其是一旦加載器加載完成,
async
、tlmSiblingOfDojo
和已有的has
測試不能改變。此外,大部分的配置數據是淺拷貝,就是說你不能使用這種機制,例如向自定義配置對象添加更多的鍵值——對象將被覆蓋。
深入define
`define` 函數接受以下參數:- moduleId (可選,default=undefined):一個模塊標識符。這個參數很大程度上是早期AMD加載器的歷史遺留或爲支持 pre-AMD Dojo,不應該提供它。
- dependencies (可選,default=[]):是你模塊依賴關係的模塊標識符的一個數組。如果指定它,這些模塊將在你的模塊解析之前生成,它們將按順序以參數的方式傳遞給你的工廠函數。
- factory:你模塊的值,或者將返回值的工廠函數。
重要的是要記住,在定義模塊的時候,工廠函數只調用一次——返回值會被加載器緩存。在實踐層面,這意味着模塊可以通過加載相同的模塊很容易地共享對象(類似其他語言的靜態屬性)。
在定義模塊的時候,值可以用一個簡單的對象:
// in "my/nls/common.js"
define({
greeting: "Hello!",
howAreYou: "How are you?"
});
請記住,如果你不使用工廠函數定義模塊。你將無法引用任何依賴關係,所以這種類型定義是罕見的,所以這種定義的類型是很少見的,通常只有被用在i18n綁定或者簡單配置對象。
加載器如何工作?
當你調用require
來加載模塊,加載器會找到模塊的代碼然後將它當做一個參數傳遞給你的回調函數,這樣你就可以使用這些模塊了。
- 首先加載器要先解決你傳遞的模塊標識符。這涉及到將模塊標識符和
baseUrl
聯繫起來,還要考慮加載的其他配置選項的修改,比如map
(稍後討論更多細節)。 - 此時加載器擁有模塊的URL並且可以通過在頁面上創建一個新的
script
元素來加載真實文件和將src
屬性設置爲模塊的URL。 - 一旦文件被加載並解析,它將設置爲模塊的值。
- 加載器會保留每個模塊的引用,因此在下次請求該模塊時,加載器會返回已經存在的引用。
當加載一個AMD模塊的時候,代碼被插入到頁面裏一個新的sceipt
元素,然後會調用define
函數。在加載傳遞給define
的任何依賴模塊時,都會經過上面同樣的處理過程,然後將加載器對模塊的引用設置爲你傳遞給define
的工廠函數的返回值。(如果你向define
傳遞一個值,而不是函數,加載器對你的模塊的引用將設爲該值。)
配置加載器
因爲遺留的兼容性原因,Dojo的加載器默認爲同步模式。爲了實現異步,我們需要進行顯式的配置。通過將async
屬性配置爲true
:
<script data-dojo-config="async: true" src="js/lib/dojo/dojo.js"></script>
你要習慣把開啓異步作爲標準做法——只有你知道你需要同步行爲的時候再關閉它。接下來要做的是將模塊的位置信息配置給加載器:
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" }
]
};
記住你必須在加載
dojo.js
之前設置dojoConfig
。先看看Configuring Dojo tutorial 如果你還沒看的話。
讓我們檢查下我們使用的配置選項:
baseUrl
(default=dojo.js文件加載的路徑):爲庫的加載定義基礎URL。例如,如果你想要加載”my/widget/Person”,加載器會試着從下面的路徑加載:
/js/my/widget/Person.js
我們可以在文件系統裏方便的在任何地方存放我們的文件(在本例中,“js”文件夾),並且只使用模塊id相關的部分路徑。比如我們不需要require(["js/my/widget/Person"])
,只要更簡單的require(["my/widget/Person"])
。因爲在實際加載資源文件的時候,我們將“/js/”配置爲所有模塊id的預先基礎。
tlmSiblingOfDojo
(default = true):默認情況下,加載器會希望從加載器所在文件夾的兄弟文件夾來查找模塊(記住,在Dojo中當你的script元素加載dojo.js
時,就加載了加載器)。如果你的文件結構像下面這樣:
/
js/
dojo/
dijit/
dojox/
my/
util/
這樣你就不需要配置baseUrl
或者 tlmSiblingOfDojo
,你的最高級模塊是dojo.js
所在文件夾的兄弟文件夾,所以tlmSiblingOfDojo
就是true。
- packages:一個包配置對象的數組。在最基本的層面上,包只是模塊的簡單集合。dojo, dijit和dojox都是包的範例。不像在一個目錄裏的一個簡單模塊集合,packages充滿了一些額外功能,它顯著提高了模塊的可移植性和易用性。一個便攜包是自包含的,也可以通過cpm這樣的工具安裝。你可以配置一個包以下項:
- name:包的名字,應該和包含該模塊的文件名一致。
- location:包的位置;可以是相對於
baseUrl
的路徑或者一個絕對路徑。相比”lib/dojo/dom” ,我們更希望從 “dojo/dom”來加載模塊(再看一眼本教程開頭的文件結構)。所以我們將location
屬性指定爲”lib/dojo”。就是說嘗試加載 “dojo/dom”模塊的加載器會加載 “/js/lib/dojo/dom.js” 文件(記住,因爲baseUrl
裏預設了“js”)。 - main(可選,default=main.js):當試圖require包本身時,用來加載正確的模塊。例如,如果你想要require“dojo”,實際加載的文件是”/js/dojo/main.js”。由於我們已經爲“my”包重寫了這個屬性,如果require“my”,實際會加載”/js/my/app.js”。
如果我們嘗試require一個沒有定義的包”util”,加載器會試着加載”/js/util.js”。你應該總在加載器配置裏定義你全部的包。
使用便攜模塊
新AMD加載器的一個最重要的特性是能夠創建完全的便攜包。例如,如果你有一個應用需要用到兩個不同版本Dojo中的模塊,新加載器就非常方便。
假設你有一個建立在舊版本Dojo上的應用,你想要更新到最新的1.10版本,但是Dojo有一些更新導致你的舊代碼無法使用。在爲舊代碼使用舊版Dojo的同時,你可以將新代碼升級到當前Dojo的發佈版本。這可以通過map
配置屬性完成:
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行)首先定義了三個包,它們指向包含舊版Dojo的文件夾。
- (6-8行)接下來定義三個當前發行版本的包。
- (9-10行)爲舊代碼和當前代碼定義包
- (12-18行)定義一個
map
配置:它將應用到 “myOldApp”模塊,並且將模塊對 “dojo”、”dijit”和”dojox” 包的請求分別映射到”dojo16”、”dijit16”和”dojox16”。 - 來源於“my“包的模塊將從當前Dojo發行版本的 dojo、 dijit、 dojox加載模塊。
你可以從 AMD Configuration documentation 獲得更多關於map
的信息。
如果你已經很熟悉加載器,特別是packageMap
屬性,它已經棄用了,map
是更先進的配置選項。
編寫便攜模塊
你可以(也應該)確保的是,你創建的包內部的模塊總是從相同的包里加載文件,通過用相對 的模塊標識符指定依賴關係。下面給出在“my”包裏的模塊的代碼:
// in "my/widget/NavBar.js"
define([
"dojo/dom",
"my/otherModule",
"my/widget/InfoBox"
], function(dom, otherModule, InfoBox){
// …
});
用相對關係標識符代替從my包明確的模塊請求:
// 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也只在定義模塊時有效,在傳遞給require
的依賴列表中是不起作用的。
考慮相對標識符對同一個包的作用,再回頭看
map
的例子是不是發現了一些問題?爲了簡單起見,我們把重點放在讓你的應用一部分使用舊版Dojo而另一部分用當前版本的實現上。但是,我們漏掉了一些重要的東西,Dijit依賴於Dojo,DojoX 又同時依賴於Dojo和Dijit。下面的配置將確保這些依賴項正確解析。爲了安全起見,將Dojo包映射到他們自己 (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
}
};
有條件地require模塊
有時候,你可能想要有條件地require一個模塊來回應一些狀況。例如,你想要延遲加載一個可選模塊,直到一個事件發生。如果你使用顯式模塊定義,這就很簡單: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”需要變成一個相對標識符。單改變它的話不起作用,因爲` require ` 調用的時候丟失了原始模塊的上下文。爲了解決這個問題,Dojo加載器提供一個叫做**上下文相關require(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` 使用局部綁定、上下文相關的`require` 函數,所以我們能夠相對於“my/debug”安全的require模塊。
require
的上下文是怎麼消失的?
記住require
是一個全局定義的函數。當click時間的處理器執行時,它唯一從模塊獲得的上下文是局部定義的。它不知道自己定義在什麼模塊裏。在本地作用域沒有“require”,所以將調用全局的“require”。回想本教程自始至終的文件結構,如果我們傳遞 “./debug/console”給require
,它將嘗試加載並不存在的”/js/debug/console.js”文件。通過使用上下文相關的require
,我們擁有一個改進的require
的本地引用,它可以保持模塊的上下文,所以能正確的加載”/js/my/debug/console.js”。
上下文相關的require
在模塊加載資源(images, templates, CSS)時也非常有用。先給出以下文件結構:
/
js/
my/
widget/
InfoBox.js
images/
info.png
在`InfoBox.js` 裏我們可以調用`require.toUrl` 得到一個定位”info.png”的完整URL,這個URL可以用在`img` 元素裏設置`src` 屬性。
// in my/widget/InfoBox.js
define([
"dojo/dom",
"require"
], function(dom, require){
// assume DOM structure where #infoBoxImage is an img element
dom.byId("infoBoxImage").src = require.toUrl("./images/info.png");
});
處理循環依賴項
你編程的時候偶爾會碰到這樣的情況,兩個模塊之間互相引用,而這種引用形成了一個循環依賴。爲了解決這樣的循環依賴,加載器優先解析遞歸模塊。例如,下面的例子:// 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();
});
View Demo
它看起來應該print “apples and oranges”,但是卻出現了moduleB: Object has no method 'getValue'
的錯誤。下面看下當你加載和運行“index.html”時,加載器做了什麼:
- 解析傳遞給require的依賴項(在
index.html
裏):moduleA
- 解析
moduleA
的依賴項:moduleB
- 解析
moduleB
的依賴項:moduleA
- 檢測到程序試圖解析
moduleA
- 通過臨時將
moduleA
解析成一個空對象來打破循環依賴。 - 通過調用
moduleB
的工廠函數重新解析它,空對象將作爲moduleA
傳遞給工廠函數。 - 將工廠函數的返回值傳遞給加載器作爲對
moduleB
的引用。 - 調用工廠函數重新解析
moduleA
。 - 將
moduleA
工廠函數的返回值傳遞加載器作爲對moduleA
的引用,現在加載器都引用了有效值。moduleB
仍引用空對象。 - 執行
moduleA.print
,由於moduleB
對moduleA
有一個壞的引用,當它調用moduleA.getValue
時就拋出一個錯誤。
爲了解決這個問題,加載器提供一個特殊的“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();
});
View Demo
這次加載運行”index.html”時發生的是:
- 解析傳遞給require的依賴項(在
index.html
裏):moduleA
- 解析
moduleA
的依賴項:moduleB
- 解析
moduleB
的依賴項:moduleA
- 檢測到程序試圖解析
moduleA
- 通過臨時將
moduleA
解析成一個空對象來打破循環依賴。 - 通過調用
moduleB
的工廠函數重新解析它,空對象將作爲moduleA
傳遞給工廠函數。 - 將工廠函數的返回值傳遞給加載器作爲對
moduleB
的引用。 - 通過調用工廠函數重新解析
moduleA
,空對象將作爲moduleA
的佔位符以exports
參數的方式傳遞給工廠函數。 - 在解析將“exports”列爲依賴項的模塊之後,加載器對模塊的引用不在指向工廠函數的返回值。相反,加載器假設模塊在創建的作爲佔位符的空對象上設置了一些必要屬性,並將它作爲
exports
參數傳遞工廠函數。 - 執行
moduleA.print
,由於moduleB
擁有一個指向moduleA
填充對象的有效引用,當它調用moduleA.getValue
時就可以像預期的那樣執行。
// 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
// 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
// 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加載器也可以通過將JavaScript文件的路徑作爲標識符傳遞來加載非AMD代碼。加載器這些同屬標識符的三種方式:- 以“/”開頭的標識符
- 以協議(如“http:”、“https:”)開頭的標識符
- 以“.js”結尾的標識符
當任意代碼作爲模塊加載的時候,模塊的解析值爲undefined
,你需要直接訪問定義爲全局腳本的代碼。
Dojo加載器的一個專屬特性是混搭舊Dojo模塊和AMD風格模塊的能力。這讓它可以穩步有序地從舊的代碼庫轉換到AMD代碼庫,而不是立馬改變所有的代碼。加載器在同步和異步模式下都可以這樣。異步模式下,舊模塊的解析值是全局作用域裏的對象,這個對象對應dojo.provide
調用文件的腳本的解析值。例如:
// in "my/legacyModule.js"
dojo.provide("my.legacyModule");
my.legacyModule = {
isLegacy: true
};
當使用require(["my/legacyModule"])
通過AMD加載器加載這個代碼時,分配給my.legacyModule
的對象將作爲這個模塊的解析值。
服務器端JavaScript
新AMD加載器的最後一個特性時能夠在服務器端加載JavaScript使用node.js or Rhino。如下通過命令行加載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 Node.js 。
一個加載器準備完畢,每個load=
參數都會向自動解析的依賴項列表裏添加一個模塊。在瀏覽器裏,同樣的代碼是這樣的:
<script data-dojo-config="async: true" src="path/to/dojo.js"></script>
<script>require(["my/serverConfig", "my/app"]);</script>
小結
新AMD格式給Dojo帶來了許多激動人心的特性和能力,篇幅有限,本教程只對新加載器做了一個簡單概述。想了解AMD加載器的新特性請查看 Dojo loader reference guide。