AMD終極揭秘
原文: https://www.sitepen.com/blog/2012/06/25/amd-the-definitive-source/
作者:Kris Zyp
譯者:Elaine Liu
究竟什麼是AMD?
隨著web應用不斷發展和對JavaScript依賴的進一步加深,出現了使用模塊(Modules)來組織代碼和依賴性。模塊使得我們創建明確清晰的組件和接口,這些組件和接口能夠很容易的加載並連接到其依賴組件。 AMD模塊係統提供了使用JavaScript模塊來構建Web應用的完美方式,並且這種方式具有形式簡單,異步加載和廣泛采用的特點。
異步模塊定義(AMD)格式是一套API,它用於定義可重用的並能在多種框架使用的模塊。開發AMD是為了提供一種定義模塊的方式,這種方式可以使用原生的瀏覽器腳本元素機製來實現模塊的異步加載。AMD API由2009年Dojo 社區的討論中產生,然後移動到討論CommonJS如何更好的為瀏覽器適應CommonJS模塊格式(被NodeJS使用)。 CommonJS已經發展成為單獨的一個標準並有其專門的社區。AMD已經廣泛普及,形成了眾多模塊加載實現並被廣泛使用。在SitePen公司,我們廣泛的使用Dojo的AMD機製工作,為其提供支持,並積極的建設這一機製。
本文中用到的一些重要詞匯
- 模塊(module) —— 一個經過封裝的JavaScript文件,它遵循模塊的格式,指定依賴和提供模塊輸出。
- 模塊標識(module ID)——唯一標識模塊的字符串,相對模塊標識將根據當前模塊的標識解釋為絕對模塊標識
- 模塊路徑 (module path)——用於檢索模塊的URL。一個模塊標識對應於一個模塊路徑,該路徑是由加載器配置規則設定的(缺省情況下,模塊路徑假定為該模塊對於根路徑的相對路徑,根路徑通常是模塊加載器包所在的父目錄)。
- 模塊加載器(module loader)——解析和加載模塊以及相關依賴的JavaScript代碼,它與插件交互,並處理加載配置。
- 包(package)——一組模塊集合。例如dojo,dijit以及dgrid都是包。
- 構建器(builder)——用於將模塊(或者多個模塊)以及其依賴連接在一起產生單個JavaScript文件的工具,這樣使得一個應用程序能夠包含多個模塊,並能創建多個構建層次,從而使得它們在被加載時實現HTTP請求數目最小化。
- 層(layer)——一個文件,它包含若幹模塊並由構建器優化生成單個文件。
- 依賴(dependency)——為了使另一個模塊正常工作而必須加載的模塊。
- AMD——異步模塊定義,一種為瀏覽器開發提供最優體驗的模塊定義格式。
- 工廠方法(factory)——通過define定義的並提供給模塊加載器的函數,它在所有依賴加載完後執行一次。
為什麼需要AMD模塊?
模塊化係統的基礎前提是:
- 允許創建被封裝的代碼片段,也就是所謂的模塊
- 定義本模塊與其他模塊之間的依賴
- 定義可以被其他模塊使用的輸出的功能
- 謹慎的使用這些模塊提供的功能
AMD格式提供了幾個關鍵的好處。首先,它提供了一種緊湊的聲明依賴的方式。通過簡單的字符串數組來定義模塊依賴,使得開發者能夠花很小的代價輕鬆列舉大量模塊依賴性。
AMD幫助消除對全局變量的需求。 每個模塊都通過局部變量引用或者返回對象來定義其依賴模塊以及輸出功能。因此,模塊不需要引入全局變量就能夠定義其功能並實現與其他模塊的交互。AMD同時是“匿名的”,意味著模塊不需要硬編碼指向其路徑的引用, 模塊名僅依賴其文件名和目錄路徑,極大的降低了重構的工作量。
通過將依賴性映射為局部變量, AMD鼓勵高效能的編碼實踐。如果沒有AMD模塊加載器,傳統的JavaScript代碼必須依賴層層嵌套的對象來“命名”給定的腳本或者模塊。如果使用這種方式,通常需要通過一組屬性來訪問某個功能,這會造成全局變量的查找和眾多屬性的查找,增加了額外的開發工作同時降低了程序的性能。通過將模塊依賴性映射為局部變量,隻需要一個簡單的局部變量就能訪問某個功能,這是極其快速的並且能夠被JavaScript引擎優化。
使用AMD
最基礎的AMD API是define()方法,用於定義一個模塊及其依賴。通常我們這樣來寫一個模塊:
- define(dependencyIds, function(dependency1, dependency2,...){
- // module code
- });
dependencyIds 參數是一個字符串數組,用於表示需要加載的依賴模塊。這些依賴模塊將會被加載和執行。一旦所有依賴都被執行完畢,它們的輸出將作為參數提供給回調函數(define()方法的第二個參數)
為了展示AMD的基礎用法,我們可以定義一個使用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被加載(當然也必須等到它們本事的依賴也被加載,以此類推), 回調函數將被調用,同時dojo/query的輸出(一個負責CSS選擇器查詢的函數)作為參數query,dojo/on的輸出(一個可以添加事件監聽器的函數)作為參數on被傳到這個回調函數中。回調函數(通常認為是模塊的工廠方法)被保證隻調用一次。
列在依賴集合中的每個模塊標識是一個抽象的模塊路徑。說它是抽象的因為它被模塊加載器轉移成真正的URL。正如你所見,模塊路徑並不需要包含“.js”後綴,這個後綴在加載的時候會自動添加。當模塊標識直接由模塊名打頭時,該名稱是模塊的絕對標識。相比之下,我們也可以通過由“./”或者"../"打頭表示當前目錄或者父目錄來指定相對標識。這些相對標識會通過標準路徑解析規則來解析成絕對標識。你可以定義一個模塊路徑規則來決定這些模塊路徑將如何轉換成URL。缺省情況下,模塊根目錄定義為相對於模塊加載器包的父目錄的路徑。例如,如果我們用下麵的方法加載Dojo(注意在這裏我們設置async屬性為true來保證異步AMD加載)
- <script src="/path/to/dojo/dojo.js" data-dojo-config="async:true"></script>
那麼,假設根目錄到模塊的路徑為“/path/to/”。如果我們指定依賴於“my/module”,這個依賴將被解析為“/path/to/my/module.js”.
初始模塊加載
我們已經描述了如何創建一個簡單的模塊。然而,我們還需要一個入口來觸發這些依賴鏈。我們可以通過使用require() API來做到這一點。這個函數簽名基本跟define()一致,區別在於它用於加載依賴但而不需要定義一個模塊(當一個模塊被定義時,如果它不被別的模塊請求它是不會執行的)我們可以像下麵這樣加載我們的應用程序:
- <script src="/path/to/dojo/dojo.js"><!--mce:1--></script>
- <script type="text/javascript"><!--mce:2--></script>
Dojo提供了加載初始模塊的快捷方式。初始模塊能夠通過指定deps配置屬性來加載。
- <script src="/path/to/dojo/dojo.js"><!--mce:3--></script>
這是加載應用程序的一個非常棒的方式,因為JavaScript代碼能夠完全從HTML中消除,僅需留下一個腳本標記來引導整個剩餘的程序。同時,這種方式讓你能夠輕鬆的創建強勁的build,它能夠將你的應用程序代碼和dojo.js組合成為單獨的一個文件而不需要在build之後改變HTML腳本標簽。RequireJS和其他模塊加載器也有類似的加載頂層模塊的選項。
上圖展示了由require()調用引起的一連串的依賴加載。require()的調用開啟加載第一個模塊,接著根據需要加載各模塊的依賴模塊。那些不需要的模塊(如上圖中的模塊d)則永遠不會被加載或者執行。
require()函數還可用於配置模塊路徑查找以及其他選項,但這一般來說對各個模塊加載器都有特定的實現。更多信息請參考各個加載器關於配置細節的文檔。
插件和Dojo最優化
AMD還支持加載其它資源的插件。這一點對於加載非AMD依賴非常有價值,例如加載HTML片段和模板,CSS,國際化相關的特定資源等。插件機製讓我們在依賴列表中引用這些非AMD資源。語法如下:
- "plugin!resource-name"
- define(["dojo/_base/declare", "dijit/_WidgetBase", "dijit/_TemplatedMixin", "dojo/text!./templates/foo.html"],
- function(declare, _WidgetBase, _TemplatedMixin, template){
- return declare([_WidgetBase, _TemplatedMixin], {
- templateString: template
- });
- });
這是一個多層麵創建Dojo小部件的範例。首先,它展示了利用Dijit基類創建小部件的標準用法。你可能也注意到我們如何創建一個小部件類並如何返回它。我們沒有使用任何命名空間或者類名來使用declare()(類構造方法)。因為AMD消除了對命名空間的需求,我們不再需要用declare()來創建全局的類名。這一點與AMD模塊中寫匿名模塊的策略是一致的。同樣的,一個匿名的模塊是不需要在其模塊內部硬編碼任何相對於自身的路徑或者名稱的。我們可以輕鬆的對模塊重命名或者將其移動到其他的路徑而不需要改模塊內的任何代碼。通常我們推薦使用這種方法來定義匿名類,但如果你需要用聲明式的標記來使用這個小部件的話,為了創建一個具有命名空間的全局變量,使其能夠讓Dojo 解析器在Dojo1.7中引用,你還是需要包含命名空間/類名來定義類。Dojo 1.8中對此作了改進,你可以使用模塊標識來做到這一點。
還有一些Dojo包含的插件是非常有用的。dojo/i18n插件在加載國際化區域性包(常用於翻譯文本或者區域信息格式化)時使用。另外一個重要的插件是dojo/domReady,它通常被推薦用於取代dojo.ready。如果一個模塊除了加載其他依賴還需要等待整個DOM可用才執行的時候,這個插件使得這一過程非常簡單,不需要再加一層額外的回調。我們將dojo/domReady作為插件使用時,但需要對應的資源名。
- define(["dojo/query", "dojo/domReady!"],
- function(query){
- // DOM is ready, so we can query away
- query(".some-class").forEach(function(node){
- // do something with these nodes
- });
- });
另一個有價值的插件是dojo/has。 這個模塊用於輔助檢測某些特征,幫助你基於當前瀏覽器的某些特征來選擇不同的代碼路徑。然而這個模塊也常被用作一個標準模塊 ,提供一個has()函數,它也同時可以當作插件使用。作插件使用時能夠幫助我們根據當前的特性條件性的加載某些依賴。dojo/has插件的語法采用了一個三元操作符,它將特性名稱作為條件,而模塊標識作為值。例如,我們可以在當然瀏覽器支持touch事件的條件下加載單獨的touch UI模塊:
- define(["dojo/has!touch?ui/touch:ui/desktop"],
- function(ui){
- // ui will be ui/touch if touch is enabled,
- //and ui/desktop otherwise
- ui.start();
- });
使用dojo/has的好處不僅僅是提供一個特征檢測的運行時API。如果使用dojo/has,不光在你的代碼中有has()形式,同時也作為依賴插件,build係統可以檢測這些特性的分支。這意味著我們可以創建設備或者瀏覽器相關的build,它們能夠為某些特定的特征集合進行高度優化,隻需要在build裏的staticHasFeatures選項定義期望的特性,然後build就會自動的處理相應的代碼分支。
數據模塊
- define({
- foo: "bar"
- });
這與JSONP非常類似,支持基於腳本的JSON數據傳輸。但是,實際上AMD對於JSONP的優勢在於它不需要請求任何URL參數,其目標可以是一個靜態文件,不需要服務器端任何支持代碼來為數據加上參數化的回調函數前綴。然而,這項技術必須小心使用。模塊加載器總是會對模塊進行緩存,因此後續的針對相同模塊標識的require()請求會產生同樣的緩存數據。這對你的檢索需求可能會造成一定的困擾。
構建(builds)
AMD 被設計得非常容易被構建工具解析從而創建出將多個模塊代碼連接在一起並壓縮的一個單獨文件。模塊化係統在這方麵提供了巨大的優勢因為構建工具能夠基於模塊中列出的依賴自動的生成這個構建文件,而不需要依賴任何手寫或者更新的腳本來創建。由於請求數目的減少,構建極大的減少了加載時間,並且由於依賴已經清楚的列在代碼中,因而使用AMD實現這一點簡直輕而易舉。
不使用構建
使用構建
(譯者注:原文作者選用的實驗截圖可能是本地資源加載的情況,由於本地資源加載的隨機性,在使用構建之後優勢不明顯。但實際在網絡傳輸中,使用構建會大大減少加載時間。)
性能
就像前麵提到的那樣,使用腳本元素注入比其他的方法快是因為它更依賴於原生的瀏覽器腳本加載機製。我們基於dojo.js創建了一些模塊的測試用例,腳本元素加載比使用XHR eval的方式快了大概60-90%。在Chrome中,如果有大量的小模塊,每個模塊加載的時間大概是5-6ms
,而XHR+eval方式平均每個模塊加載時間則接近9-10ms。在Firefox中,同步XHR方式比異步方式更快,而在IE中異步XHR比同步的快,但腳本元素加載無疑是最快的一個。讓我們感到意外的是IE9是最快的一個瀏覽器,不過這有可能是因為在Firefox和Chrome中debugger/inspector增加了一些額外的性能開銷。
模塊加載器
AMD API是開放的,現在已有有多個AMD模塊加載器和構造器的實現。這裏介紹幾個重要的AMD加載器:
- Dojo – 這是一個完全的包括插件和構造器的AMD加載器。這是我們通常用來實現Dojo工具包的加載器。
- RequireJS – 這是AMD加載器的元老也是AMD加載器的典範。其作者James Burke是AMD的主要作者和倡導者。這也是一個完整的包含構造器的加載器。
- curl.js – 這是一個快速的AMD加載器,具有超級棒的插件支持(以及它自帶的插件庫)和自帶的構造器。
- lsjs – 這是一個專門設計用於在本地存儲緩存模塊的AMD模塊加載器。其作者同時還寫了一個獨立的優化器。
- NeedJS – 一個輕量級的AMD模塊加載器。
- brequire – 另一個輕量級的AMD模塊加載器。
- inject – 它是由LinkedIn創建並使用的,是一個快速輕量級的加載器,不提供對插件的支持。
- Almond – 這是RequireJS的輕量級版本。
獲取AMD模塊

- my-script.js:
- // add this to top of the script
- defined([], function(){
- // existing script
- ...
- // add this to the end of script
- });
- require(["dgrid/Grid", "dojo/query", "my-script"], function(Grid, query){
- new Grid(config, query("#grid")[0]);
- });
- require(["dojo", "jquery.js"], function(dojo){ // jquery will be loaded as plain script
- dojo.query(...); // the dojo exports will be available in the "dojo" local variable
- $(...); // the other script will need to create globals
- });
保持小的代碼體積
AMD的反對意見
- define(function(require){
- var query = require("dojo/query");
- var on = require("dojo/on");
- ...
- });
漸進式加載
- // declare modules that we need up front
- define(["dojo/dom-create", "require"],
- function(domCreate, require){
- return function(node){
- // create container elements for our widget right away,
- // these could be styled for the right width and height,
- // and even contain a spinner to indicate the widgets are loading
- var slider = domCreate("div", {className:"slider"}, node);
- var progress = domCreate("div", {className:"progress"}, node);
- // now load the widgets, we load them independently
- // so each one can be rendered as it downloads
- require(["dijit/form/HorizontalSlider"], function(Slider){
- new Slider({}, slider);
- });
- require(["dijit/Progress"], function(Progress){
- new Progress({}, progress);
- });
- }
- main.js:
- define(["component", "exports"],
- function(component, exports){
- // we define our exported values on the exports
- // which may be used before this factory is called
- exports.config = {
- title: "test"
- };
- exports.start = function(){
- new component.Widget();
- };
- });
- component.js:
- define(["main", "exports", "dojo/_base/declare"],
- function(main, exports, declare){
- // again, we define our exported values on the exports
- // which may be used before this factory is called
- exports.Widget = declare({
- showTitle: function(){
- alert(main.config.title);
- }
- });
- });
- define(function(require, exports){
- var query = require("dojo/query");
- exports.myFunction = function(){
- ....
- };
- });
展望
- 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");
- });
- }
結論
最後更新:2017-04-03 05:40:07