天天看點

使用AMD,CommonJS和ES Harmony編寫子產品化JavaScript代碼(AMD)子產品化 應用程式解耦的重要性序 概覽腳本加載器(Script Loader)AMD 在浏覽器環境編寫子產品化代碼的規範

原文:https://addyosmani.com/writing-modular-js/

子產品化 應用程式解耦的重要性

當我們說一個程式是子產品化時,通常是指,這個程式是有一組高内聚,低耦合,功能劃厘清晰的子產品組成。就像你知道的那樣,低耦合的應用,可以随時随地的剔除依賴,這就保證了自身的易維護性。如果應用是高度的子產品化,那麼可以輕易的評估部分功能的修改對其餘部分的影響程度。

不幸的是,不像傳統的程式設計語言,在ECMA-262的疊代版本中,并未向程式設計人員提供引入這種子產品化結構的實作方式,以至無法簡潔明了的實作應用的結構化。直到近些年,對JavaScript代碼結構化需求的日益增加,才提出了“子產品化”這個明确的概念。

同時,當下的程式設計人員隻能退而求其次,使用module和對象字面量模式(object literal pattern)來變相的實作子產品化。大多數方法的實作,都是通過定義全局對象(global object),來規定命名空間(namespace),并将其捆綁在DOM元素上。但在整體架構上仍然有可能發生命名沖突(naming collisions)的問題。而且,如果不通過一些額外的工作或是使用第三方插件,仍然無法間接地管理依賴注入。

好消息是,在即将到來的ES Harmony中,已經在原生方法中提供了對應的解決方案。從現在開始,編寫子產品化的JavaScript代碼将變得前所未有的輕松。

在這篇文章中,我們将了解編寫子產品化JavaScript代碼的三種規範:AMD,CommonJS以及JavaScript的下個版本,Harmony。

序 概覽腳本加載器(Script Loader)

如果抛棄腳本加載器而直接讨論AMD和CommonJS子產品化,那無異于盲人摸象。腳本加載器的作用是将子產品化的JavaScript代碼應用于當下的應用之中。是以,使用合适的加載器是必不可少的。為了更好的了解本文介紹的内容,了解子產品化規範的原理,我建議讀者應首先對一些常見的加載器的工作原理有最基本的認識。

現在已經有很多非常優秀的加載器實作AMD和CommonJS的規範。我個人推薦RequireJS和curl.js。關于這些加載器的完整教程不在本文介紹的範圍之内。但是我推薦 John Hann關于curl.js的介紹和James Burke關于RequireJS的API文檔。

從産品的角度來說,值得推薦的是,在使用這些子產品化工具的同時,利用其自有的優化工具(如RequireJS optimizer)去壓縮拼接JavaScript檔案。值得注意的是,通過使用AMD插件Almond,RequireJS不需要摻雜在需要部署的網站中,腳本加載器可以簡潔的排除在開發之外。

即使如此,James Burke有可能會說,在頁面加載完成後動态加載腳本檔案仍然有用武之地,是以RequireJS也支援這種方式。有了這些大緻的了解,讓我們開始對子產品化做進一步了解。

AMD 在浏覽器環境編寫子產品化代碼的規範

AMD (Asynchronous Module Definition) 規範的根本目的是提供一種子產品化JavaScript程式設計的解決方案,以供目前開發者使用。該規範的靈感是來源于Dojo在現實應用中使用XHR+eval的經驗。它的倡導者們希望能夠在未來的解決方案中解決和規避那些已經發現的缺陷。

AMD子產品化規範自身是對定義子產品的建議,實作子產品和依賴能夠異步加載。它有很多明顯的優點,通過移除代碼和子產品定義的耦合性,以實作異步和高自由度。很多開發人員欣然接受這種子產品化方式,并且在ES Harmony的規範制定中被認為是極具參考價值的基石。

最初AMD是作為CommonJS子產品化規範備選方案中的一個,但是由于關于它的實作無法保證一緻性,是以後續的開發有amdjs小組負責。

現在它已經被Dojo(1.7),MooTools(2.0),FireBug(1.8)以及jQuery(1.7)接受并實作。也是以,CommonJS AMD 規範在某些情況被認為是不合理的術語,更好的應稱之為AMD或者異步子產品。

從子產品開始

首先需要了解兩個關鍵的概念:

define

方法用來便捷地定義子產品,

require

方法用來管理依賴加載。

define

使用如下格式來定義命名或匿名的子產品。

define(
    module_id /*可選*/, 
    [dependencies] /*可選*/, 
    definition function /*執行個體化子產品的函數或對象*/
);
           

從行内注釋中可以看到,

module_id

是可選參數,它的典型應用是在使用非AMD壓縮工具時,保證子產品定義的準确性(當然可能還有其他的極限情況需要必須填寫)。當省略該參數時,子產品被稱之為匿名子產品。

子產品辨別的理念是DRY(譯者注:原文中隻有簡寫,個人認為是Don’t repeat yourself),在使用匿名子產品時,應盡量簡潔以避免重複的檔案名或編碼。因為代碼的可移植性,可以将代碼輕松的移動到其他位置(或者在檔案系統中移動代碼檔案),而不需更改自身代碼或者ID。在使用簡單的包或者不使用包時,

module_id

等同于檔案的路徑。通過使用運作在CommonJS環境下的AMD優化器,如r.js,開發者可以在多個不同的環境運作相同的代碼。

回到定義文法,

dependencies

參數是一個由目前子產品所依賴的子產品組成的字元串數組,而第三個參數(

definition function

),是用于執行個體化子產品的執行函數。一個基本的子產品定義如下所示:

了解AMD:define()

//myModule隻是為了印證module_id的使用
define('myModule', 
    ['foo', 'bar'], 
    // 子產品定義函數,依賴(foo和bar)被映射到函數的參數上
    function ( foo, bar ) {
        // 傳回定義了子產品export的值(例如:我們需要暴露給外部使用的功能)

        //在這裡建立你的子產品
        var myModule = {
            doStuff:function(){
                console.log('Yay! Stuff');
            }
        }
        return myModule;
});

// 另一個例子
define('myModule', 
    ['math', 'graph'], 
    function ( math, graph ) {
        // 注意這是和AMD略微不同的地方,因為它相當的自由,
        // 是以可能存在多種定義子產品的方法,但是應該符合核心的文法
        return {
            plot: function(x, y){
                return graph.drawPie(math.randomGrid(x,y));
            }
        }
    };
});
           

另一方面,require是用來從頂層的JavaScript檔案中加載代碼或者在子產品内容希望動态的加載依賴。下面是一個使用的例子:

了解AMD:require()

// 考慮'foo' and 'bar'是兩個外部子產品
// 在這個例子中,兩個外部子產品作為回調函數的參數,它們通過'exports'暴露的屬性同樣可以被通路
require(['foo', 'bar'], function ( foo, bar ) {
        //在這裡編寫自定義邏輯
        foo.doSomething();
});
           

動态加載依賴

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

    // 注意這裡的require在子產品定義的内部
    require(['foo', 'bar'], function (foo, bar) {
        isReady = true;
        foobar = foo() + bar();
    });

    // 我們仍然可以傳回一個子產品
    return {
        isReady: isReady,
        foobar: foobar
    };
});
           

了解AMD:plugins

下面的例子展示了如何定義AMD-compatible插件:

// 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 some fun template stuff here.
    }
});
           
注意:雖然在上面的例子中,為了加載CSS依賴而包含了

css!

,但是這種方式有一些注意事項,首先當CSS完全加載完成時,根據建構方式的不同,可能CSS依賴無法完全加載。而且可能會導緻CSS被作為依賴被寫入到壓縮優化後的代碼檔案中,是以在将CSS作為被加載的依賴時,需要特别注意。

通過require.js加載AMD子產品

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

通過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();
});
           

為什麼使用AMD編寫JavaScript子產品化代碼是個很好的選擇?

  • 提供了靈活定義子產品的清晰規則
  • 相較于使用全局命名空間和

    <script>

    标簽等解決方案顯得更為簡潔。具有更簡潔的方式去聲明獨立子產品和可能需要的依賴。
  • 使用封裝的子產品化定義,有助于避免全局命名空間的污染
  • 相較于其他解決方案(例如随後将要介紹的CommonJS)工作的更好。不存在跨域(cross-domain),本地化以及調試(debugging)等問題,同時無需依賴服務端工具的使用。大多數的AMD加載器支援在浏覽器環境中不通過建構(build)直接加載子產品
  • 為多子產品包含在同一檔案的情況提供輸送方式(transport)。像CommonJS等其他解決方式還未實作輸送的格式
  • 隻在需要時實作對腳本的懶加載(lazy load)

相關閱讀

1.The RequireJS Guide To AMD

2.What’s the fastest way to load AMD modules?

3.AMD vs. CJS, what’s the better format?

4.AMD Is Better For The Web Than CommonJS Modules

5.The Future Is Modules Not Frameworks

6.AMD No Longer A CommonJS Specification

7.On Inventing JavaScript Module Formats And Script Loaders

8.The AMD Mailing List

AMD子產品化和Dojo

通過Dojo定義AMD相容的子產品是相當簡單的。像之前所有的例子一樣,在定義任意子產品時,将依賴數組作為第一個參數,并提供在依賴加載完成後調用一個回調函數(factory)。例如:

define(["dijit/Tooltip"], function( Tooltip ){
    //Our dijit tooltip is now available for local use
    new Tooltip(...);
});
           

注意匿名子產品現在可以被Dojo異步加載器,RequireJS或者之前習慣使用的标準dojo.require()方法所使用。

對于那些好奇子產品引用的開發人員,這裡有幾個既有趣又有用的知識點可以加以了解。AMD僅僅對AMD适用的加載器有效-它提倡在依賴清單中使用相比對的參數聲明,作為引用子產品的方式。而Dojo 1.6的建構系統中并不支援。例如:

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

這比嵌套的命名空間更具優越性,因為引用子產品無需每次輸入完整的命名空間。我們隻需要保證

'dojo/cookie'

的路徑在依賴中,而一旦它被關聯到參數時,則可以通過該變量進行引用。這就避免了在程式中重複輸入

'dojo.'

的麻煩。

注意:雖然Dojo 1.6 官方并未支援基于使用者的AMD子產品(亦或是異步加載),但是可以通過使用多種不同的腳本加載器使其正常工作。到現在為止,Dojo的核心和Dijit子產品已經向AMD文法靠攏,并且即将在1.7和2.0版本之間綜合提升對AMD的支援。

最後需要了解的是,如果希望繼續使用Dojo建構系統,或者将過時的子產品遷移到這種新的AMD形式,下面這種冗長的寫法能夠更簡單的實作相容。注意

dojo

dijit

同樣被作為依賴引用:

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

AMD子產品化設計模式(Dojo)

如果你看過我之前發表的關于設計模式的優勢的文章,你就會了解到,在處理普遍的開發問題上,它們有效得改善我們處理結構方案的方式。最近John Hann精彩地陳述了AMD子產品化設計模式,包含了單例(Singleton),裝飾者(Decorator),中繼者(Mediator)以及其他的模式。如果你有機會,我強烈推薦去浏覽一下他的幻燈片。

部分模式如下所示:

裝飾者模式

// mylib/UpdatableObservable: a decorator for dojo/store/Observable
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; // makes `new` optional
    };
});


// decorator consumer
// a consumer for mylib/UpdatableObservable

define(['mylib/UpdatableObservable'], function ( makeUpdatable ) {
    var observable, updatable, someItem;
    // ... here be code to get or create `observable`

    // ... make the observable store updatable
    updatable = makeUpdatable(observable); // `new` is optional!

    // ... later, when a cometd message arrives with new data item
    updatable.updated(updatedItem);
});
           

擴充卡模式

// 'mylib/Array' adapts `each` function to mimic jQuery's:
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
            })
        }
    });
});

// adapter consumer
// 'myapp/my-module':
define(['mylib/Array'], function ( array ) {
    array.each(['uno', 'dos', 'tres'], function (i, esp) {
        // here, `this` == item
    });
});
           

AMD子產品化和jQuery

基礎

與Dojo不同,jQuery隻提供一個檔案,然而考慮到基于插件庫的性質,我們可以通過下面的例子,看出定義AMD子產品是多麼的直接明了。

define(['js/jquery.js','js/jquery.color.js','js/underscore.js'],
    function($, colorPlugin, _){
        // Here we've passed in jQuery, the color plugin and Underscore
        // None of these will be accessible in the global scope, but we
        // can easily reference them below.

        // Pseudo-randomize an array of colors, selecting the first
        // item in the shuffled array
        var shuffleColor = _.first(_.shuffle(['#666','#333','#111']));

        // Animate the background-color of any elements with the class
        // 'item' on the page using the shuffled color
        $('.item').animate({'backgroundColor': shuffleColor });

        return {};
        // What we return can be used by other modules
    });
           

在這個例子中缺少了注冊的概念。

将jQuery注冊為異步相容的子產品

在jQuery 1.7版本中一個關鍵的特性是支援将jQuery注冊為異步子產品。有許多兼用的腳本加載器(包括RequireJS和curl)可以實作對子產品的異步加載,這也就意味着不需要使用hack就可以保證其正确運作。

由于jQuery的普遍使用,版本多種多樣,但多數情況并不希望不同的版本在同一時間進行加載,是以AMD加載器需要考慮在同一頁面加載多個不同版本的庫的情況。加載器具備不同的模式,可以特别處理這種問題,或者是通知使用者其第三方腳本和他們的類庫存一些已知的問題。

1.7版本的顯著提升,是避免了在頁面中加載某個第三方代碼時,意外加載了某個不符合開發者所預期的jQuery版本。因為你并不希望其他的執行個體重寫你自己的,是以這個提升是很有意義的。

其工作原理是,腳本加載器通過配置屬性

define.amd.jQuery=true

,指明其支援多個jQuery版本。為了更好的了解實作細節,我們将jQuery注冊為一個命名的子產品。這存在一個風險,就是有可能使用AMD

define()

方法将它和别的檔案進行拼接

,而非使用能夠了解異步AMD子產品定義的正确拼接腳本。

命名的AMD子產品為保證多數使用情況魯棒性和安全性,添加了安全的限制層。

// 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$ = $,
amdDefined;

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

// The very easy to implement flag stating support which 
// would be used by the AMD loader
define.amd = {
    jQuery: true
};
           

更智能的jQuery插件

不久前,我讨論過關于使用UMD(Universal Module Definition )模式編寫jQuery插件。就像其他流行的腳本加載器一樣,UMD定義的子產品同時相容服務端和用戶端。同時,這仍然是新的領域,有許多概念正在被最終确定。請随意檢視後續章節AMD && CommonJS中的代碼樣例,如果有好多建議請及時告訴我。

支援AMD的腳本加載器和架構

浏覽器環境

  • RequireJS http://requirejs.org
  • curl.js http://github.com/unscriptable/curl
  • bdLoad http://bdframework.com/bdLoad
  • Yabble http://github.com/jbrantly/yabble
  • PINF http://github.com/pinf/loader-js

服務端

  • RequireJS http://requirejs.org
  • PINF http://github.com/pinf/loader-js

AMD 總結

以上是對AMD子產品化規範用途的基本示例,但願能夠提供有助于了解AMD工作原理的基礎。

也許你有興趣知道目前許多大型應用和公司已經将AMD子產品化作為他們應用結構的一部分。其中包括IBM,BBC iPlayer,這從企業級層面有力的證明了此規範的重要性。

對于為什麼更多的開發者願意将AMD子產品化用于自己的應用,若有興趣的話,可以參看 James Burke的文章《On inventing JS module formats and script loaders》。

未完待續。。。

繼續閱讀