AMD異步模塊定義

2021-09-15 11:17 更新

AMD

在瀏覽器中編寫模塊化Javascript的格式

AMD (異步模塊定義Asynchronous Module Definition)格式的最終目的是提供一個(gè)當(dāng)前開(kāi)發(fā)者能使用的模塊化Javascript方案。它出自于Dojo用XHR+eval的實(shí)踐經(jīng)驗(yàn),這種格式的支持者想在以后的項(xiàng)目中避免忍受過(guò)去的這些弱點(diǎn)。

AMD模塊格式本身是模塊定義的一個(gè)建議,通過(guò)它模塊本身和模塊之間的引用可以被異步的加載。它有幾個(gè)明顯的優(yōu)點(diǎn),包括異步的調(diào)用和本身的高擴(kuò)展性,它實(shí)現(xiàn)了解耦,模塊在代碼中也可通過(guò)識(shí)別號(hào)進(jìn)行查找。當(dāng)前許多開(kāi)發(fā)者都喜歡使用它,并且認(rèn)為它朝ES Harmony提出模塊化系統(tǒng) 邁出了堅(jiān)實(shí)的一步。

最開(kāi)始AMD在CommonJs的列表中是作為模塊化格式的一個(gè)草案,但是由于它不能達(dá)到與模塊化完全一致,更進(jìn)一步的開(kāi)發(fā)被移到了在amdjs組中。

現(xiàn)在,它包含工程Dojo、MooTools、Firebug以及jQuery。盡管有時(shí)你會(huì)看見(jiàn)CommonJS AMD 格式化術(shù)語(yǔ),但最好的和它相關(guān)的是AMD或者是異步模塊支持,同樣不是所有參與到CommonJS列表的成員都希望與它產(chǎn)生關(guān)系。

注意:曾有一段時(shí)間涉及Transport/C模塊的提議規(guī)劃沒(méi)有面向已經(jīng)存在的CommonJS模塊,但是對(duì)于定義模塊來(lái)說(shuō),它對(duì)選擇AMD命名空間約定產(chǎn)生了影響。

從模塊開(kāi)始

關(guān)于AMD值得特別注意的兩個(gè)概念就是:一個(gè)幫助定義模塊的define方法和一個(gè)處理依賴加載的require方法。define被用來(lái)通過(guò)下面的方式定義命名的或者未命名的模塊:

define(
    module_id /*可選的*/,
    [dependencies] /*可選的*/,
    definition function /*用來(lái)實(shí)例化模塊或者對(duì)象的方法*/
);

通過(guò)代碼中的注釋我們可以發(fā)現(xiàn),module_id 是可選的,它通常只有在使用非AMD連接工具的時(shí)候才是必須的(可能在其它不是特別常見(jiàn)的情況下,它也是有用的)。當(dāng)不存在module_id參數(shù)的時(shí)候,我們稱這個(gè)模塊為匿名模塊。

當(dāng)使用匿名模塊的時(shí)候,模塊認(rèn)定的概念是DRY的,這樣使它在避免文件名和代碼重復(fù)的時(shí)候顯得很微不足道。因?yàn)檫@樣一來(lái)代碼方便切換,你可以很容易地把它移動(dòng)到其它地方(或者文件系統(tǒng)的其他位置),而不需要更改代碼內(nèi)容或者它的模塊ID。你可以認(rèn)為模塊id跟文件路徑的概念是相似的。

注意:開(kāi)發(fā)者們可以將同樣的代碼放到不同的環(huán)境中運(yùn)行,只要他們使用一個(gè)在CommonJS環(huán)境下工作的AMD優(yōu)化器(比如r.js)就可以了。

在回來(lái)看define方法簽名, dependencies參數(shù)代表了我們正在定義的模塊需要的dependency數(shù)組,第三個(gè)參數(shù)("definition function" or "factory function") 是用來(lái)執(zhí)行的初始化模塊的方法。 一個(gè)正常的模塊可以像下面那樣定義:

Understanding AMD: define()

// A module_id (myModule) is used here for demonstration purposes only
define( "myModule",

    ["foo", "bar"],

    // module definition function
    // dependencies (foo and bar) are mapped to function parameters
    function ( foo, bar ) {
        // return a value that defines the module export
        // (i.e the functionality we want to expose for consumption)

        // create your module here
        var myModule = {
            doStuff:function () {
                console.log( "Yay! Stuff" );
            }
        };

    return myModule;
});

// An alternative version could be..
define( "myModule",

    ["math", "graph"],

    function ( math, graph ) {

        // Note that this is a slightly different pattern
        // With AMD, it's possible to define modules in a few
        // different ways due to it's flexibility with
        // certain aspects of the syntax
        return {
            plot: function( x, y ){
                return graph.drawPie( math.randomGrid( x, y ) );
            }
        };
});

另一方面,require被用來(lái)從一個(gè)頂級(jí)文件或者模塊里加載代碼,而這是我們?cè)揪拖M膭?dòng)態(tài)加載依賴的位置。它的一個(gè)用法如下:

理解AMD: require()

// Consider "foo" and "bar" are two external modules
// In this example, the "exports" from the two modules
// loaded are passed as function arguments to the
// callback (foo and bar) so that they can similarly be accessed

require(["foo", "bar"], function ( foo, bar ) {
        // rest of your code here
        foo.doSomething();
});

動(dòng)態(tài)加載依賴

define(function ( require ) {
    var isReady = false, foobar;

    // note the inline require within our module definition
    require(["foo", "bar"], function ( foo, bar ) {
        isReady = true;
        foobar = foo() + bar();
    });

    // we can still return a module
    return {
        isReady: isReady,
        foobar: foobar
    };
});

理解 AMD: 插件

下面是定義一個(gè)兼容AMD插件的例子:

// With AMD, it's possible to load in assets of almost any kind
// including text-files and HTML. This enables us to have template
// dependencies which can be used to skin components either on
// page-load or dynamically.

define( ["./templates", "text!./template.md","css!./template.css" ],

    function( templates, template ){
        console.log( templates );
        // do something with our templates here
    }

});

注意:盡管上面的例子中css!被包含在在加載CSS依賴的過(guò)程中,要記住,這種方式有一些問(wèn)題,比如它不完全可能在CSS完全加載的時(shí)候建立模塊. 取決于我們?nèi)绾螌?shí)現(xiàn)創(chuàng)建過(guò)程,這也可能導(dǎo)致CSS被作為優(yōu)化文件中的依賴被包含進(jìn)來(lái),所以在這些情況下把CSS作為已加載的依賴應(yīng)該多加小心。如果你對(duì)上面的做法感興趣,我們也可以從這里查看更多@VIISON的RequireJS CSS 插件:https://github.com/VIISON/RequireCSS

使用RequireJS加載AMD模塊

require(["app/myModule"],

    function( myModule ){
        // start the main module which in-turn
        // loads other modules
        var module = new myModule();
        module.doStuff();
});

這個(gè)例子可以簡(jiǎn)單地看出asrequirejs(“app/myModule”,function(){})已被加載到頂層使用。這就展示了通過(guò)AMD的define()函數(shù)加載到頂層模塊的不同,下面通過(guò)一個(gè)本地請(qǐng)求allrequire([])示例兩種類型的裝載機(jī)(curl.js和RequireJS)。

使用curl.js加載AMD模塊

curl(["app/myModule.js"],

    function( myModule ){
        // start the main module which in-turn
        // loads other modules
        var module = new myModule();
        module.doStuff();

});

延遲依賴模塊

// This could be compatible with jQuery's Deferred implementation,
// futures.js (slightly different syntax) or any one of a number
// of other implementations

define(["lib/Deferred"], function( Deferred ){
    var defer = new Deferred();

    require(["lib/templates/?index.html","lib/data/?stats"],
        function( template, data ){
            defer.resolve( { template: template, data:data } );
        }
    );
    return defer.promise();
});

使用Dojo的AMD模塊

使用Dojo定義AMD兼容的模塊是相當(dāng)直接的.如上所述,就是在一個(gè)數(shù)組中定義任何的模塊依賴作為第一個(gè)參數(shù),并且提供回調(diào)函數(shù)來(lái)執(zhí)行一次依賴已經(jīng)被加載進(jìn)來(lái)的模塊.例如:

define(["dijit/Tooltip"], function( Tooltip ){

    //Our dijit tooltip is now available for local use
    new Tooltip(...);

});

請(qǐng)注意模塊的匿名特性,現(xiàn)在它可以在一個(gè)Dojo匿名裝載裝置中的被處理,RequireJS或者標(biāo)準(zhǔn)的dojo.require()模塊裝載器。

了解一些有趣的關(guān)于模塊引用的陷阱是非常有用的.雖然AMD倡導(dǎo)的引用模塊的方式宣稱它們?cè)谝唤M帶有一些匹配參數(shù)的依賴列表里面,這在版本更老的Dojo 1.6構(gòu)建系統(tǒng)中并不被支持--它真的僅僅對(duì)AMD兼容的裝載器才起作用.例如:

define(["dojo/cookie", "dijit/Tooltip"], function( cookie, Tooltip ){

    var cookieValue = cookie( "cookieName" );
    new Tooltip(...);

});

越過(guò)嵌套的命名空間定義方式有許多好處,模塊不再需要每一次都直接引用完整的命名空間了--所有我們所需要的是依賴中的"dojo/cookie"路徑,它一旦賦給一個(gè)作為別名的參數(shù),就可以用變量來(lái)引用了.這移除了在我們的應(yīng)用程序中重復(fù)打出"dojo."的必要。

最后需要注意到的難點(diǎn)是,如果我們希望繼續(xù)使用更老的Dojo構(gòu)建系統(tǒng),或者希望將老版本的模塊遷移到更新的AMD形式,接下來(lái)更詳細(xì)的版本會(huì)使得遷移更加容易.注意dojo和dijit也是作為依賴被引用的:

define(["dojo", "dijit', "dojo/cookie", "dijit/Tooltip"], function( dojo, dijit ){
    var cookieValue = dojo.cookie( "cookieName" );
    new dijit.Tooltip(...);
});

AMD 模塊設(shè)計(jì)模式 (Dojo)

正如在前面的章節(jié)中,設(shè)計(jì)模式在提高我們的結(jié)構(gòu)化構(gòu)建的共同開(kāi)發(fā)問(wèn)題非常有效。 John Hann已經(jīng)給AMD模塊設(shè)計(jì)模式,涵蓋單例,裝飾,調(diào)解和其他一些優(yōu)秀的設(shè)計(jì)模式,如果有機(jī)會(huì),我強(qiáng)烈建議參考一下他的 幻燈片。 AMD設(shè)計(jì)模式的選擇可以在下面找到。

一段AMD設(shè)計(jì)模式可以在下面找到。

修飾設(shè)計(jì)模式

// mylib/UpdatableObservable: dojo/store/Observable的一個(gè)修飾器
define(["dojo", "dojo/store/Observable"], function ( dojo, Observable ) {
    return function UpdatableObservable ( store ) {

        var observable = dojo.isFunction( store.notify ) ? store :
                new Observable(store);

        observable.updated = function( object ) {
            dojo.when( object, function ( itemOrArray) {
                dojo.forEach( [].concat(itemOrArray), this.notify, this );
            });
        };

        return observable;
    };
});

// 修飾器消費(fèi)者
// mylib/UpdatableObservable的消費(fèi)者

define(["mylib/UpdatableObservable"], function ( makeUpdatable ) {
    var observable,
        updatable,
        someItem;

    // 讓observable 儲(chǔ)存 updatable
    updatable = makeUpdatable( observable ); // `new` 關(guān)鍵字是可選的!

    // 如果我們想傳遞修改過(guò)的data,我們要調(diào)用.update()
    //updatable.updated( updatedItem );
});

適配器設(shè)計(jì)模式

// "mylib/Array" 適配`each`方法來(lái)模仿 jQuerys:
define(["dojo/_base/lang", "dojo/_base/array"], function ( lang, array ) {
    return lang.delegate( array, {
        each: function ( arr, lambda ) {
            array.forEach( arr, function ( item, i ) {
                lambda.call( item, i, item ); // like jQuery's each
            });
        }
    });
});

// 適配器消費(fèi)者
// "myapp/my-module":
define(["mylib/Array"], function ( array ) {
    array.each( ["uno", "dos", "tres"], function ( i, esp ) {
        // here, `this` == item
    });
});

使用jQuery的AMD模塊

不像Dojo,jQuery真的存在于一個(gè)文件中,而是基于插件機(jī)制的庫(kù),我們可以在下面代碼中證明AMD模塊是如何直線前進(jìn)的。

define(["js/jquery.js","js/jquery.color.js","js/underscore.js"],

    function( $, colorPlugin, _ ){
        // <span></span>這里,我們通過(guò)jQuery中,顏色的插件,并強(qiáng)調(diào)沒(méi)有這些將可在全局范圍內(nèi)訪問(wèn),但我們可以很容易地在下面引用它們。
        // 偽隨機(jī)一系列的顏色,在改組后的數(shù)組中選擇的第一個(gè)項(xiàng)目 

 <div>

 </div>
        var shuffleColor = _.first( _.shuffle( "#666","#333","#111"] ) );

        // 在頁(yè)面上有class為"item" 的元素隨機(jī)動(dòng)畫改變背景色
        $( ".item" ).animate( {"backgroundColor": shuffleColor } );

        // 我們的返回可以被其他模塊使用
        return {};
    });

然而,這個(gè)例子中缺失了一些東西,它只是注冊(cè)的概念。

將jQuery當(dāng)做一個(gè)異步兼容的模塊注冊(cè)

jQuery1.7中落實(shí)的一個(gè)關(guān)鍵特性是支持將jQuery當(dāng)做一個(gè)異步兼容的模塊注冊(cè)。有很多兼容的腳本加載器(包括RequireJS 和 curl)可以使用異步模塊形式加載模塊,而這意味著在讓事物起作用的時(shí)候,更少的需要使用取巧的特殊方法。

如果開(kāi)發(fā)者想要使用AMD,并且不想將他們的jQuery的版本泄露到全局空間中,他們就應(yīng)該在使用了jQuery的頂層模塊中調(diào)用noConflict方法.另外,由于多個(gè)版本的jQuery可能在一個(gè)頁(yè)面上,AMD加載器就必須作出特殊的考慮,以便jQuery只使用那些認(rèn)識(shí)到這些問(wèn)題的AMD加載器來(lái)進(jìn)行注冊(cè),這是使用加載器特殊的define.amd.jQuery來(lái)表示的。RequireJS和curl是兩個(gè)這樣做了的加載器。

這個(gè)叫做AMD的家伙提供了一種安全的魯棒的封包,這個(gè)封包可以用于絕大多數(shù)情況。

// Account for the existence of more than one global
// instances of jQuery in the document, cater for testing
// .noConflict()

var jQuery = this.jQuery || "jQuery",
$ = this.$ || "$",
originaljQuery = jQuery,
original$ = $;

define(["jquery"] , function ( $ ) {
    $( ".items" ).css( "background","green" );
    return function () {};
});

為什么AMD是寫模塊化Javascript代碼的好幫手呢?

  • 提供了一個(gè)清晰的方案,告訴我們?nèi)绾味x一個(gè)可擴(kuò)展的模塊。
  • 和我們常用的前面的全局命名空間以及 <script> 標(biāo)簽解決方案相比較,非常清晰。有一個(gè)清晰的方式用于聲明獨(dú)立的模塊,以及它們所依賴的模塊。
  • 模塊定義被封裝了,有助于我們避免污染全局命名空間。
  • 比其它替代方案能更好的工作(例如CommonJS,后面我們就會(huì)看到)。沒(méi)有跨域問(wèn)題,局部以及調(diào)試問(wèn)題,不依賴于服務(wù)器端工具。大多數(shù)AMD加載器支持在瀏覽器中加載模塊,而不需要構(gòu)建過(guò)程。
  • 提供一個(gè)“透明”的方法用于在單個(gè)文件中包含多個(gè)模塊。其它方式像 CommonJS 要求必須遵循一個(gè)傳輸格式。 再有需要的時(shí)候,可以惰性加載腳本。

注意:上面的很多說(shuō)法也可以說(shuō)做事YUI模塊加載策略。

相關(guān)閱讀

有哪些腳本加載器或者框架支持AMD?

瀏覽器端:

(and more)

服務(wù)器端:

AMD 總結(jié)

在很多項(xiàng)目中使用過(guò)AMD,我的結(jié)論就是AMD符合了很多條一個(gè)構(gòu)建嚴(yán)肅應(yīng)用的開(kāi)發(fā)者所想要的一個(gè)好的模塊的格式要求。不用擔(dān)心全局,支持命名模塊,不需要服務(wù)端轉(zhuǎn)換來(lái)工作,在依賴管理中也很方便。

同時(shí)也是使用Bacbon.js,ember.js 或者其它結(jié)構(gòu)化框架來(lái)開(kāi)發(fā)模塊時(shí)的利器,可以保持項(xiàng)目的組織架構(gòu)。 在Dojo和CommonJS世界中,AMD已經(jīng)被討論了兩年了,我們直到它需要時(shí)間去逐漸成熟和進(jìn)化。我們也知道在外面有很多大公司也在實(shí)戰(zhàn)中使用了AMD用于構(gòu)建非凡的系統(tǒng)(IBM, BBC iPlayer),如果它不好,那么可能現(xiàn)在它們就已經(jīng)被丟棄了,但是沒(méi)有。

但是,AMD依然有很多地方有待改善。使用這些格式一段時(shí)間的開(kāi)發(fā)者可能已經(jīng)感受到了AMD 樣板和封裝代碼很討厭。盡管我也有這樣的憂慮,但是已經(jīng)存在一些工具例如Volo 可以幫助我們繞過(guò)這些問(wèn)題,同時(shí)我也要說(shuō)整體來(lái)看,AMD的優(yōu)勢(shì)遠(yuǎn)遠(yuǎn)勝過(guò)其缺點(diǎn)。

以上內(nèi)容是否對(duì)您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)