原文: http://www.sitepen.com/blog/2012/06/25/amd-the-definitive-source/
作者:Kris Zyp
譯者:Elaine Liu
究竟什么是AMD?
隨著web應(yīng)用不斷發(fā)展和對(duì)JavaScript依賴的進(jìn)一步加深,出現(xiàn)了使用模塊(Modules)來組織代碼和依賴性。模塊使得我們創(chuàng)建明確清晰的組件和接口,這些組件和接口能夠很容易的加載并連接到其依賴組件。 AMD模塊系統(tǒng)提供了使用JavaScript模塊來構(gòu)建Web應(yīng)用的完美方式,并且這種方式具有形式簡單,異步加載和廣泛采用的特點(diǎn)。
異步模塊定義(AMD)格式是一套API,它用于定義可重用的并能在多種框架使用的模塊。開發(fā)AMD是為了提供一種定義模塊的方式,這種方式可以使用原生的瀏覽器腳本元素機(jī)制來實(shí)現(xiàn)模塊的異步加載。AMD API由2009年Dojo 社區(qū)的討論中產(chǎn)生,然后移動(dòng)到討論CommonJS如何更好的為瀏覽器適應(yīng)CommonJS模塊格式(被NodeJS使用)。 CommonJS已經(jīng)發(fā)展成為單獨(dú)的一個(gè)標(biāo)準(zhǔn)并有其專門的社區(qū)。AMD已經(jīng)廣泛普及,形成了眾多模塊加載實(shí)現(xiàn)并被廣泛使用。在SitePen公司,我們廣泛的使用Dojo的AMD機(jī)制工作,為其提供支持,并積極的建設(shè)這一機(jī)制。
本文中用到的一些重要詞匯
- 模塊(module) —— 一個(gè)經(jīng)過封裝的JavaScript文件,它遵循模塊的格式,指定依賴和提供模塊輸出。
- 模塊標(biāo)識(shí)(module ID)——唯一標(biāo)識(shí)模塊的字符串,相對(duì)模塊標(biāo)識(shí)將根據(jù)當(dāng)前模塊的標(biāo)識(shí)解釋為絕對(duì)模塊標(biāo)識(shí)
- 模塊路徑 (module path)——用于檢索模塊的URL。一個(gè)模塊標(biāo)識(shí)對(duì)應(yīng)于一個(gè)模塊路徑,該路徑是由加載器配置規(guī)則設(shè)定的(缺省情況下,模塊路徑假定為該模塊對(duì)于根路徑的相對(duì)路徑,根路徑通常是模塊加載器包所在的父目錄)。
- 模塊加載器(module loader)——解析和加載模塊以及相關(guān)依賴的JavaScript代碼,它與插件交互,并處理加載配置。
- 包(package)——一組模塊集合。例如dojo,dijit以及dgrid都是包。
- 構(gòu)建器(builder)——用于將模塊(或者多個(gè)模塊)以及其依賴連接在一起產(chǎn)生單個(gè)JavaScript文件的工具,這樣使得一個(gè)應(yīng)用程序能夠包含多個(gè)模塊,并能創(chuàng)建多個(gè)構(gòu)建層次,從而使得它們?cè)诒患虞d時(shí)實(shí)現(xiàn)HTTP請(qǐng)求數(shù)目最小化。
- 層(layer)——一個(gè)文件,它包含若干模塊并由構(gòu)建器優(yōu)化生成單個(gè)文件。
- 依賴(dependency)——為了使另一個(gè)模塊正常工作而必須加載的模塊。
- AMD——異步模塊定義,一種為瀏覽器開發(fā)提供最優(yōu)體驗(yàn)的模塊定義格式。
- 工廠方法(factory)——通過define定義的并提供給模塊加載器的函數(shù),它在所有依賴加載完后執(zhí)行一次。
為什么需要AMD模塊?
模塊化系統(tǒng)的基礎(chǔ)前提是:
- 允許創(chuàng)建被封裝的代碼片段,也就是所謂的模塊
- 定義本模塊與其他模塊之間的依賴
- 定義可以被其他模塊使用的輸出的功能
- 謹(jǐn)慎的使用這些模塊提供的功能
AMD滿足以上需求,并將依賴模塊設(shè)置為其回調(diào)函數(shù)的參數(shù)從而實(shí)現(xiàn)在模塊代碼被執(zhí)行前異步的加載這些依賴模塊。AMD還提供了加載非AMD資源的插件系統(tǒng)。
雖然有其他的load JavaScript的替代方法,但使用腳本元素來加載JavaScript有特有的優(yōu)勢,包括性能,減少調(diào)試(尤其在一些老版本的瀏覽器上)以及跨域的支持。因此AMD致力于提供基于瀏覽器開發(fā)的最優(yōu)體驗(yàn)。
AMD格式提供了幾個(gè)關(guān)鍵的好處。首先,它提供了一種緊湊的聲明依賴的方式。通過簡單的字符串?dāng)?shù)組來定義模塊依賴,使得開發(fā)者能夠花很小的代價(jià)輕松列舉大量模塊依賴性。
AMD幫助消除對(duì)全局變量的需求。 每個(gè)模塊都通過局部變量引用或者返回對(duì)象來定義其依賴模塊以及輸出功能。因此,模塊不需要引入全局變量就能夠定義其功能并實(shí)現(xiàn)與其他模塊的交互。AMD同時(shí)是“匿名的”,意味著模塊不需要硬編碼指向其路徑的引用, 模塊名僅依賴其文件名和目錄路徑,極大的降低了重構(gòu)的工作量。
通過將依賴性映射為局部變量, AMD鼓勵(lì)高效能的編碼實(shí)踐。如果沒有AMD模塊加載器,傳統(tǒng)的JavaScript代碼必須依賴層層嵌套的對(duì)象來“命名”給定的腳本或者模塊。如果使用這種方式,通常需要通過一組屬性來訪問某個(gè)功能,這會(huì)造成全局變量的查找和眾多屬性的查找,增加了額外的開發(fā)工作同時(shí)降低了程序的性能。通過將模塊依賴性映射為局部變量,只需要一個(gè)簡單的局部變量就能訪問某個(gè)功能,這是極其快速的并且能夠被JavaScript引擎優(yōu)化。
使用AMD
最基礎(chǔ)的AMD API是define()方法,用于定義一個(gè)模塊及其依賴。通常我們這樣來寫一個(gè)模塊:
- define(dependencyIds, function(dependency1, dependency2,...){
-
- });
dependencyIds 參數(shù)是一個(gè)字符串?dāng)?shù)組,用于表示需要加載的依賴模塊。這些依賴模塊將會(huì)被加載和執(zhí)行。一旦所有依賴都被執(zhí)行完畢,它們的輸出將作為參數(shù)提供給回調(diào)函數(shù)(define()方法的第二個(gè)參數(shù))
為了展示AMD的基礎(chǔ)用法,我們可以定義一個(gè)使用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被加載(當(dāng)然也必須等到它們本事的依賴也被加載,以此類推), 回調(diào)函數(shù)將被調(diào)用,同時(shí)dojo/query的輸出(一個(gè)負(fù)責(zé)CSS選擇器查詢的函數(shù))作為參數(shù)query,dojo/on的輸出(一個(gè)可以添加事件監(jiān)聽器的函數(shù))作為參數(shù)on被傳到這個(gè)回調(diào)函數(shù)中?;卣{(diào)函數(shù)(通常認(rèn)為是模塊的工廠方法)被保證只調(diào)用一次。
列在依賴集合中的每個(gè)模塊標(biāo)識(shí)是一個(gè)抽象的模塊路徑。說它是抽象的因?yàn)樗荒K加載器轉(zhuǎn)移成真正的URL。正如你所見,模塊路徑并不需要包含“.js”后綴,這個(gè)后綴在加載的時(shí)候會(huì)自動(dòng)添加。當(dāng)模塊標(biāo)識(shí)直接由模塊名打頭時(shí),該名稱是模塊的絕對(duì)標(biāo)識(shí)。相比之下,我們也可以通過由“./”或者"../"打頭表示當(dāng)前目錄或者父目錄來指定相對(duì)標(biāo)識(shí)。這些相對(duì)標(biāo)識(shí)會(huì)通過標(biāo)準(zhǔn)路徑解析規(guī)則來解析成絕對(duì)標(biāo)識(shí)。你可以定義一個(gè)模塊路徑規(guī)則來決定這些模塊路徑將如何轉(zhuǎn)換成URL。缺省情況下,模塊根目錄定義為相對(duì)于模塊加載器包的父目錄的路徑。例如,如果我們用下面的方法加載Dojo(注意在這里我們?cè)O(shè)置async屬性為true來保證異步AMD加載)
- <script src="/path/to/dojo/dojo.js" data-dojo-config="async:true"></script>
那么,假設(shè)根目錄到模塊的路徑為“/path/to/”。如果我們指定依賴于“my/module”,這個(gè)依賴將被解析為“/path/to/my/module.js”.
初始模塊加載
我們已經(jīng)描述了如何創(chuàng)建一個(gè)簡單的模塊。然而,我們還需要一個(gè)入口來觸發(fā)這些依賴鏈。我們可以通過使用require() API來做到這一點(diǎn)。這個(gè)函數(shù)簽名基本跟define()一致,區(qū)別在于它用于加載依賴但而不需要定義一個(gè)模塊(當(dāng)一個(gè)模塊被定義時(shí),如果它不被別的模塊請(qǐng)求它是不會(huì)執(zhí)行的)我們可以像下面這樣加載我們的應(yīng)用程序:
- <script src="/path/to/dojo/dojo.js"></script>
- <script type="text/javascript"></script>
Dojo提供了加載初始模塊的快捷方式。初始模塊能夠通過指定deps配置屬性來加載。
- <script src="/path/to/dojo/dojo.js"></script>
這是加載應(yīng)用程序的一個(gè)非常棒的方式,因?yàn)镴avaScript代碼能夠完全從HTML中消除,僅需留下一個(gè)腳本標(biāo)記來引導(dǎo)整個(gè)剩余的程序。同時(shí),這種方式讓你能夠輕松的創(chuàng)建強(qiáng)勁的build,它能夠?qū)⒛愕膽?yīng)用程序代碼和dojo.js組合成為單獨(dú)的一個(gè)文件而不需要在build之后改變HTML腳本標(biāo)簽。RequireJS和其他模塊加載器也有類似的加載頂層模塊的選項(xiàng)。
上圖展示了由require()調(diào)用引起的一連串的依賴加載。require()的調(diào)用開啟加載第一個(gè)模塊,接著根據(jù)需要加載各模塊的依賴模塊。那些不需要的模塊(如上圖中的模塊d)則永遠(yuǎn)不會(huì)被加載或者執(zhí)行。
require()函數(shù)還可用于配置模塊路徑查找以及其他選項(xiàng),但這一般來說對(duì)各個(gè)模塊加載器都有特定的實(shí)現(xiàn)。更多信息請(qǐng)參考各個(gè)加載器關(guān)于配置細(xì)節(jié)的文檔。
插件和Dojo最優(yōu)化
AMD還支持加載其它資源的插件。這一點(diǎn)對(duì)于加載非AMD依賴非常有價(jià)值,例如加載HTML片段和模板,CSS,國際化相關(guān)的特定資源等。插件機(jī)制讓我們?cè)谝蕾嚵斜碇幸眠@些非AMD資源。語法如下:
dojo/text插件就是一個(gè)常用的插件,它允許你直接將一個(gè)文件加載為一段文本。使用這個(gè)插件時(shí),我們將目標(biāo)文件列為資源名。在小部件加載它們的HTML模板時(shí)經(jīng)常使用這個(gè)插件。例如,我們用下面的方式可以通過Dojo創(chuàng)建我們自己的小部件:
- define(["dojo/_base/declare", "dijit/_WidgetBase", "dijit/_TemplatedMixin", "dojo/text!./templates/foo.html"],
- function(declare, _WidgetBase, _TemplatedMixin, template){
- return declare([_WidgetBase, _TemplatedMixin], {
- templateString: template
- });
- });
這是一個(gè)多層面創(chuàng)建Dojo小部件的范例。首先,它展示了利用Dijit基類創(chuàng)建小部件的標(biāo)準(zhǔn)用法。你可能也注意到我們?nèi)绾蝿?chuàng)建一個(gè)小部件類并如何返回它。我們沒有使用任何命名空間或者類名來使用declare()(類構(gòu)造方法)。因?yàn)锳MD消除了對(duì)命名空間的需求,我們不再需要用declare()來創(chuàng)建全局的類名。這一點(diǎn)與AMD模塊中寫匿名模塊的策略是一致的。同樣的,一個(gè)匿名的模塊是不需要在其模塊內(nèi)部硬編碼任何相對(duì)于自身的路徑或者名稱的。我們可以輕松的對(duì)模塊重命名或者將其移動(dòng)到其他的路徑而不需要改模塊內(nèi)的任何代碼。通常我們推薦使用這種方法來定義匿名類,但如果你需要用聲明式的標(biāo)記來使用這個(gè)小部件的話,為了創(chuàng)建一個(gè)具有命名空間的全局變量,使其能夠讓Dojo 解析器在Dojo1.7中引用,你還是需要包含命名空間/類名來定義類。Dojo 1.8中對(duì)此作了改進(jìn),你可以使用模塊標(biāo)識(shí)來做到這一點(diǎn)。
還有一些Dojo包含的插件是非常有用的。dojo/i18n插件在加載國際化區(qū)域性包(常用于翻譯文本或者區(qū)域信息格式化)時(shí)使用。另外一個(gè)重要的插件是dojo/domReady,它通常被推薦用于取代dojo.ready。如果一個(gè)模塊除了加載其他依賴還需要等待整個(gè)DOM可用才執(zhí)行的時(shí)候,這個(gè)插件使得這一過程非常簡單,不需要再加一層額外的回調(diào)。我們將dojo/domReady作為插件使用時(shí),但需要對(duì)應(yīng)的資源名。
- define(["dojo/query", "dojo/domReady!"],
- function(query){
-
- query(".some-class").forEach(function(node){
-
- });
- });
另一個(gè)有價(jià)值的插件是dojo/has。 這個(gè)模塊用于輔助檢測某些特征,幫助你基于當(dāng)前瀏覽器的某些特征來選擇不同的代碼路徑。然而這個(gè)模塊也常被用作一個(gè)標(biāo)準(zhǔn)模塊 ,提供一個(gè)has()函數(shù),它也同時(shí)可以當(dāng)作插件使用。作插件使用時(shí)能夠幫助我們根據(jù)當(dāng)前的特性條件性的加載某些依賴。dojo/has插件的語法采用了一個(gè)三元操作符,它將特性名稱作為條件,而模塊標(biāo)識(shí)作為值。例如,我們可以在當(dāng)然瀏覽器支持touch事件的條件下加載單獨(dú)的touch UI模塊:
- define(["dojo/has!touch?ui/touch:ui/desktop"],
- function(ui){
-
-
- ui.start();
- });
這個(gè)三元操作符可以是嵌套的,空字符串可用于表示不加載任何模塊。使用dojo/has的好處不僅僅是提供一個(gè)特征檢測的運(yùn)行時(shí)API。如果使用dojo/has,不光在你的代碼中有has()形式,同時(shí)也作為依賴插件,build系統(tǒng)可以檢測這些特性的分支。這意味著我們可以創(chuàng)建設(shè)備或者瀏覽器相關(guān)的build,它們能夠?yàn)槟承┨囟ǖ奶卣骷线M(jìn)行高度優(yōu)化,只需要在build里的staticHasFeatures選項(xiàng)定義期望的特性,然后build就會(huì)自動(dòng)的處理相應(yīng)的代碼分支。
數(shù)據(jù)模塊
對(duì)于沒有任何依賴的模塊,他們被簡單的定義為一個(gè)對(duì)象(就像數(shù)據(jù)一樣)。你可以使用僅有一個(gè)參數(shù)的define()調(diào)用,該參數(shù)就是那個(gè)對(duì)象。這是非常簡單和直接明了的。
這與JSONP非常類似,支持基于腳本的JSON數(shù)據(jù)傳輸。但是,實(shí)際上AMD對(duì)于JSONP的優(yōu)勢在于它不需要請(qǐng)求任何URL參數(shù),其目標(biāo)可以是一個(gè)靜態(tài)文件,不需要服務(wù)器端任何支持代碼來為數(shù)據(jù)加上參數(shù)化的回調(diào)函數(shù)前綴。然而,這項(xiàng)技術(shù)必須小心使用。模塊加載器總是會(huì)對(duì)模塊進(jìn)行緩存,因此后續(xù)的針對(duì)相同模塊標(biāo)識(shí)的require()請(qǐng)求會(huì)產(chǎn)生同樣的緩存數(shù)據(jù)。這對(duì)你的檢索需求可能會(huì)造成一定的困擾。
構(gòu)建(builds)
AMD 被設(shè)計(jì)得非常容易被構(gòu)建工具解析從而創(chuàng)建出將多個(gè)模塊代碼連接在一起并壓縮的一個(gè)單獨(dú)文件。模塊化系統(tǒng)在這方面提供了巨大的優(yōu)勢因?yàn)闃?gòu)建工具能夠基于模塊中列出的依賴自動(dòng)的生成這個(gè)構(gòu)建文件,而不需要依賴任何手寫或者更新的腳本來創(chuàng)建。由于請(qǐng)求數(shù)目的減少,構(gòu)建極大的減少了加載時(shí)間,并且由于依賴已經(jīng)清楚的列在代碼中,因而使用AMD實(shí)現(xiàn)這一點(diǎn)簡直輕而易舉。
不使用構(gòu)建
使用構(gòu)建
(譯者注:原文作者選用的實(shí)驗(yàn)截圖可能是本地資源加載的情況,由于本地資源加載的隨機(jī)性,在使用構(gòu)建之后優(yōu)勢不明顯。但實(shí)際在網(wǎng)絡(luò)傳輸中,使用構(gòu)建會(huì)大大減少加載時(shí)間。)
性能
就像前面提到的那樣,使用腳本元素注入比其他的方法快是因?yàn)樗蕾囉谠臑g覽器腳本加載機(jī)制。我們基于dojo.js創(chuàng)建了一些模塊的測試用例,腳本元素加載比使用XHR eval的方式快了大概60-90%。在Chrome中,如果有大量的小模塊,每個(gè)模塊加載的時(shí)間大概是5-6ms
,而XHR+eval方式平均每個(gè)模塊加載時(shí)間則接近9-10ms。在Firefox中,同步XHR方式比異步方式更快,而在IE中異步XHR比同步的快,但腳本元素加載無疑是最快的一個(gè)。讓我們感到意外的是IE9是最快的一個(gè)瀏覽器,不過這有可能是因?yàn)樵贔irefox和Chrome中debugger/inspector增加了一些額外的性能開銷。
模塊加載器
AMD API是開放的,現(xiàn)在已有有多個(gè)AMD模塊加載器和構(gòu)造器的實(shí)現(xiàn)。這里介紹幾個(gè)重要的AMD加載器:
- Dojo – 這是一個(gè)完全的包括插件和構(gòu)造器的AMD加載器。這是我們通常用來實(shí)現(xiàn)Dojo工具包的加載器。
- RequireJS – 這是AMD加載器的元老也是AMD加載器的典范。其作者James Burke是AMD的主要作者和倡導(dǎo)者。這也是一個(gè)完整的包含構(gòu)造器的加載器。
- curl.js – 這是一個(gè)快速的AMD加載器,具有超級(jí)棒的插件支持(以及它自帶的插件庫)和自帶的構(gòu)造器。
- lsjs – 這是一個(gè)專門設(shè)計(jì)用于在本地存儲(chǔ)緩存模塊的AMD模塊加載器。其作者同時(shí)還寫了一個(gè)獨(dú)立的優(yōu)化器。
- NeedJS – 一個(gè)輕量級(jí)的AMD模塊加載器。
- brequire – 另一個(gè)輕量級(jí)的AMD模塊加載器。
- inject – 它是由LinkedIn創(chuàng)建并使用的,是一個(gè)快速輕量級(jí)的加載器,不提供對(duì)插件的支持。
- Almond – 這是RequireJS的輕量級(jí)版本。
獲取AMD模塊
現(xiàn)在可以找到越來越多的AMD格式的包和模塊。Dojo 基礎(chǔ)包網(wǎng)站集中的給出了一個(gè)可以找到的包的列表。CPM 安裝器 可以用于安裝任何通過Dojo基礎(chǔ)包網(wǎng)站注冊(cè)的包(同時(shí)自動(dòng)安裝他們的依賴)。
James Burke, RequireJS的作者創(chuàng)建了 Volo——一個(gè)支持直接從github上面安裝包的安裝器。當(dāng)然你也可以直接從他們的項(xiàng)目網(wǎng)站(在github或者其他地方)上直接下載模塊,然后自己組織你的目錄結(jié)構(gòu)。 有了AMD,我們就能夠輕松的使用任何包而不僅僅是Dojo模塊來構(gòu)造應(yīng)用程序。將普通的腳本轉(zhuǎn)換成AMD也是非常簡單的。你只需要在define中用一個(gè)空數(shù)組表面依賴,然后將腳本直接包含在define的回調(diào)函數(shù)體中。當(dāng)然如果該腳本必須在其他某些腳本之后執(zhí)行,你也可以添加依賴。例如:
- my-script.js:
-
- defined([], function(){
-
- ...
-
- });
我們可以構(gòu)造用各種方式導(dǎo)入模塊的應(yīng)用程序。
- require(["dgrid/Grid", "dojo/query", "my-script"], function(Grid, query){
- new Grid(config, query("#grid")[0]);
- });
必須提醒的一點(diǎn)是當(dāng)將腳本轉(zhuǎn)換成模塊時(shí),如果腳本含有頂層的函數(shù)或者變量,它們?cè)臼侨趾瘮?shù)或者變量,但是在define()回調(diào)函數(shù)之內(nèi)它們變成了回調(diào)函數(shù)的局部函數(shù)和變量,因此不會(huì)產(chǎn)生全局的函數(shù)或者變量。你或者顯示的改變你的代碼來創(chuàng)建一個(gè)全局函數(shù)或者變量(刪掉那些變量前面的var或者function前綴(當(dāng)你知道該腳本要與其他依賴于這些全局變量的腳本共同工作時(shí)你很可能需要這么做),或者改變轉(zhuǎn)換后的模塊使其返回函數(shù)或者變量作為模塊輸出并且讓其依賴模塊來使用這些輸出(這種方法使你能夠追求無全局變量的AMD典范)。
直接加載非AMD腳本
大多數(shù)模塊加載器同時(shí)支持直接加載非AMD腳本。我們可以將一個(gè)普通腳本包含在我們的依賴中,并用“.js”后綴或者提供一個(gè)URL絕對(duì)路徑或者用“\”開頭的URL來表示它們是非AMD的。加載的腳本不能夠提供直接的AMD輸出,但能夠通過標(biāo)準(zhǔn)的創(chuàng)建全局變量或者函數(shù)的形式來提供它自身的功能。例如我們可以加載Dojo和jQuery:
- require(["dojo", "jquery.js"], function(dojo){
- dojo.query(...);
- $(...);
- });
保持小的代碼體積
AMD能夠輕松的與多種腳本庫協(xié)同組合工作。然而,雖然能夠很容易實(shí)現(xiàn)這一點(diǎn),但在某些方面你必須小心。將Dojo和jQuery這樣的腳本庫組合也許能夠正常工作,但因?yàn)镈ojo和jQuery在功能上絕大部分是重合的,它會(huì)導(dǎo)致增加很多不必要的下載量。事實(shí)上,Dojo新模塊策略的一個(gè)關(guān)鍵之處就在于避免下載任何多余的模塊。除了向AMD轉(zhuǎn)換,Dojo基礎(chǔ)功能也被拆分成多個(gè)能夠單獨(dú)使用的模塊,使得Dojo能夠盡可能的為某個(gè)應(yīng)用程序保持最小的體積。事實(shí)上,最新的Dojo 應(yīng)用程序和組件開發(fā)(像dgrid)經(jīng)常使得整個(gè)應(yīng)用程序比原先版本的Dojo基類還要小。
AMD的反對(duì)意見
當(dāng)然也有一些對(duì)AMD的反對(duì)聲音存在。一個(gè)反對(duì)意見是使用原來的CommonJS格式(AMD正是由此產(chǎn)生)比AMD更簡單明了,更不容易出錯(cuò)。CommonJS格式的確沒有繁復(fù)的使用“禮節(jié)”。然而,這種格式也是有一些問題的。我們可以讓源文件原封不動(dòng)的傳給瀏覽器。這需要模塊加載器將代碼封裝在一個(gè)注入了必須的CommonJS變量的頭部中,用這種方式來調(diào)用XHR和eval來加載模塊。這種方法的缺點(diǎn)我們之前已經(jīng)討論過了,包括降低性能,難于在老版本的瀏覽器上調(diào)試,以及跨域訪問的限制。另一種方法是使用一個(gè)實(shí)時(shí)的構(gòu)造過程,或者在服務(wù)器端按需封裝的機(jī)制對(duì)CommonJS模塊只提供必要的封裝,這實(shí)際上跟AMD的思想是一致的。這些方法在很多情況下不一定會(huì)帶來太多麻煩,也是合理的開發(fā)方式。但是為了滿足最廣泛的用戶需求,這里用戶可以在工作在一個(gè)非常簡單的服務(wù)器上,或者面對(duì)跨瀏覽器的情況,或者使用老版本的瀏覽器,AMD減少了這些問題發(fā)生的幾率,這也是Dojo的使命之一。
AMD的依賴列舉機(jī)制也因?yàn)槿菀壮鲥e(cuò)而經(jīng)常被詬病,因?yàn)樗枰S護(hù)依賴列表和回調(diào)函數(shù)的參數(shù)列表時(shí)刻一致。如果這兩個(gè)列表不一致,模塊的引用就會(huì)大錯(cuò)特錯(cuò)。實(shí)際應(yīng)用中,我們?cè)谶@個(gè)問題上并沒有碰見太多的困難。另外還有一種使用AMD的替代方法可以解決這一問題。AMD支持調(diào)用僅有單個(gè)回調(diào)參數(shù)的define(),回調(diào)參數(shù)是一個(gè)require工廠方法,它包含了多個(gè)require()調(diào)用而不是一個(gè)依賴列表。這種方法不僅幫助解決如何保持依賴列表的同步問題,而且還使得添加CommonJs封裝變得輕而易舉。因?yàn)檫@個(gè)工廠函數(shù)的函數(shù)體是符合CommonJS模塊形式的。下面就是一個(gè)使用這種方法來定義模塊的例子:
- define(function(require){
- var query = require("dojo/query");
- var on = require("dojo/on");
- ...
- });
當(dāng)提供一個(gè)參數(shù)時(shí),請(qǐng)求,輸出和模塊就會(huì)自動(dòng)的提供給該工廠函數(shù)。AMD模塊加載器將會(huì)掃描該工廠函數(shù)的require調(diào)用,并自動(dòng)的在運(yùn)行該工廠方法之前加載他們。因?yàn)閞equire調(diào)用直接內(nèi)聯(lián)在變量賦值語句中,你可以很容易的刪除某個(gè)依賴聲明而不需要任何保持依賴同步的操作。
關(guān)于require() API的一個(gè)說明:當(dāng)使用一個(gè)字符串來調(diào)用require(),它是同步執(zhí)行的,但是如果把它放在隊(duì)列里面,它是一部執(zhí)行的。因此例子中的依賴模塊仍然是在執(zhí)行回調(diào)函數(shù)之前異步加載的,當(dāng)依賴都加載到內(nèi)存中,代碼中單字符串的require調(diào)用就被以同步的方式執(zhí)行。
AMD的局限
AMD為我們提供了一個(gè)模塊加載并協(xié)同工作的重要層面。然而,AMD僅僅是模塊定義。它并不能為模塊創(chuàng)建的API開出任何通用的“特別處方”。比如,你不能指望模塊加載器給你提供查詢引擎,并期望它從一堆可替換的查詢模塊中給你返回一個(gè)通用的API。當(dāng)然定義這樣的API更利于模塊交互,但這不在AMD的范疇內(nèi)。大多數(shù)模塊加載器不支持將模塊標(biāo)識(shí)映射到不同的路徑,因此如果你有可替換的模塊,你最好自己定義一個(gè)模塊標(biāo)識(shí)到不同目標(biāo)路徑的映射來解決這一點(diǎn)。
漸進(jìn)式加載
目前我們看到的AMD最大的問題不在于API,而是在實(shí)際應(yīng)用中有種趨勢是預(yù)先聲明所有的依賴(這一點(diǎn)上文中我們一直這么提,真抱歉我們現(xiàn)在才解釋這個(gè)問題?。?。然而,很多模塊能夠正常的工作,而將某些依賴的加載延遲到它們實(shí)際需要的時(shí)候再加載。采用延遲加載策略是非常對(duì)提供一個(gè)漸進(jìn)式加載頁面是非常有價(jià)值的。有了這樣一個(gè)漸進(jìn)式加載頁面,頁面上的組件能夠在其組件代碼下載完后就顯示,整個(gè)頁面不需要等到所有JavaScript代碼都下載完就能渲染和使用。我們可以通過使用異步請(qǐng)求require([])API,將我們的模塊編寫成支持延遲加載特定模塊的形式。在下面的例子中,我們只為該函數(shù)加載必須的代碼來創(chuàng)建子容器節(jié)點(diǎn),延遲加載容器內(nèi)小部件的代碼,從而實(shí)現(xiàn)即時(shí)的視覺交互:
-
- define(["dojo/dom-create", "require"],
- function(domCreate, require){
- return function(node){
-
-
-
- var slider = domCreate("div", {className:"slider"}, node);
- var progress = domCreate("div", {className:"progress"}, node);
-
-
- require(["dijit/form/HorizontalSlider"], function(Slider){
- new Slider({}, slider);
- });
- require(["dijit/Progress"], function(Progress){
- new Progress({}, progress);
- });
- }
這種方式提供了絕妙的用戶體驗(yàn),因?yàn)槿藗兛梢栽诮M件一下載完成是就與之交互,而不需要等到整個(gè)頁面全部加載完。如果用戶看到頁面逐步渲染,很可能會(huì)感覺程序運(yùn)行得比原來快,更富有響應(yīng)。
require,exports
在上面的例子中,我們使用了一個(gè)特殊的依賴——“請(qǐng)求(require)”,它是一個(gè)模塊內(nèi)的require()函數(shù)的引用,允許我們使用相對(duì)于該模塊的引用。(如果你使用全局的“require”,相對(duì)模塊標(biāo)識(shí)則不是相對(duì)于當(dāng)前模塊的)
另外一個(gè)特殊的依賴是“輸出(exports)”。有了輸出依賴,輸出對(duì)象體現(xiàn)在參數(shù)中,而不是返回輸出對(duì)象。模塊可以向輸出對(duì)象添加屬性。這一點(diǎn)在模塊有循環(huán)引用的情況時(shí)特別有用。因?yàn)槟K工廠函數(shù)可以開始運(yùn)行,并添加輸出對(duì)象,而另外一個(gè)函數(shù)可以在前者運(yùn)行結(jié)束前就使用前者的輸出對(duì)象。關(guān)于循環(huán)引用使用輸出對(duì)象的一個(gè)簡單例子如下:
- main.js:
- define(["component", "exports"],
- function(component, exports){
-
-
- exports.config = {
- title: "test"
- };
- exports.start = function(){
- new component.Widget();
- };
- });
- component.js:
- define(["main", "exports", "dojo/_base/declare"],
- function(main, exports, declare){
-
-
- exports.Widget = declare({
- showTitle: function(){
- alert(main.config.title);
- }
- });
- });
如果我們只是依賴于函數(shù)的返回值,這個(gè)例子就不可能正常的工作,因?yàn)樵谘h(huán)中工廠函數(shù)需要先執(zhí)行完畢,不可能訪問另一個(gè)函數(shù)的返回值。
就像再前面的例子提到的那樣,如果我們省略依賴性列表,那么依賴就被認(rèn)做是缺省的”require“和”exports“,require()調(diào)用會(huì)被掃描,因此上例可以寫成:
- define(function(require, exports){
- var query = require("dojo/query");
- exports.myFunction = function(){
- ....
- };
- });
展望
EcmaScript 委員會(huì)致力于在JavaScript中增加原生模塊的支持。它們提供的添加方法是基于JavaScript語言中定義和引用模塊的新語法。這種新語法包括了一個(gè)module關(guān)鍵字用于在腳本中定義模塊,一個(gè)expoert關(guān)鍵字用于定義輸出,一個(gè)import關(guān)鍵字用以定義需要引入的模塊屬性。這些操作符與AMD中的概念一一對(duì)應(yīng),使得轉(zhuǎn)換變得相對(duì)容易。如果我們要將本文的第一個(gè)例子用Harmony 模塊系統(tǒng)定義的方法來寫,下面是一種寫法: - 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");
- });
- }
現(xiàn)在提出的新模塊系統(tǒng)包括支持定制的模塊加載器,它能夠與新的模塊系統(tǒng)交互,還能夠用于保留某些AMD現(xiàn)存的某些特性,例如用插件訪問非JavaScript資源。
結(jié)論
AMD為基于瀏覽器的網(wǎng)絡(luò)應(yīng)用提供了強(qiáng)大的模塊化系統(tǒng),它利于原生的瀏覽器加載方式實(shí)現(xiàn)異步加載,支持對(duì)各種形式的資源靈活訪問的插件,同時(shí)用一個(gè)簡單明了的格式來實(shí)現(xiàn)它的功能。由于有了類似Dojo,RequireJS等眾多出色的AMD項(xiàng)目,AMD世界是讓人興奮的,并且為發(fā)展快速,可操作的JavaScript模塊帶來栩栩生機(jī)。.