JavaScript模組化開發一瞥

高翌翔發表於2012-02-22

對於那些正在構建大型應用程式,而對JavaScript不甚瞭解的開發者而言,他們最初必須要面對的挑戰之一就是如何著手組織程式碼。起初只要在標記之間嵌入幾百行程式碼就能跑起來,不過很快程式碼就會變得一塌糊塗……


對於那些正在構建大型應用程式,而對JavaScript不甚瞭解的開發者而言,他們最初必須要面對的挑戰之一就是如何著手組織程式碼。起初只要在<script>標記之間嵌入幾百行程式碼就能跑起來,不過很快程式碼就會變得一塌糊塗。而問題是,JavaScript沒有為組織程式碼提供任何明顯幫助。從字面上看,C#有using,Java有import——而JavaScript一無所有。這迫使JavaScript編寫者試驗不同的約定,並使用現有的語言建立了一些切實可行的方法來組織大型JavaScript應用程式。

各種模式(patterns)、工具(tools)及慣例(practices)會形成現代JavaScript的基礎,它們必將來自於語言本身實現之外。

——Rebecca Murphy

模組模式(The Module Pattern)

用於解決組織程式碼問題、使用最為廣泛的方法之一是模組模式(Module Pattern)。我嘗試在下面解釋一個基本示例,並討論其若干特性。要想閱讀更精彩的說明,並瞭解用盡各種不同方法的怪人,那麼請參閱Ben Cherry的帖子——JavaScript Module Pattern: In-Depth(深入理解JavaScript模組模式)

(function(lab49) {

    function privateAdder(n1, n2) {
        return n1 + n2;
    }

    lab49.add = function(n1, n2) {
        return privateAdder(n1, n2);
    };

})(window.lab49 = window.lab49 || {});

在上例中,我們使用了一些來自語言的基本功能,從而創造出在C#及Java等語言中見過的類似結構。

隔離(Isolation)

請注意,這段程式碼包在被立即呼叫的函式裡(仔細看最後一行)。由於在瀏覽器中,預設情況下會把JavaScript檔案置於全域性作用域級別上進行計算(evaluated),因此在我們在檔案中宣告的任何內容都是隨處可用的。想象一下,要是先在lib1.js中宣告瞭var name = '...',然後又在lib2.js宣告瞭var name = '...' 。那麼後一句var宣告就會替掉前一句的值——這可不太妙。然而,由於JavaScript擁有函式作用域級別,上例中所宣告的一切都位於函式自身作用域內,與全域性作用域毫無瓜葛。這意味著,無論系統將來如何變化,位於函式中的任何內容都不會受到影響。

名稱空間(Namespacing)

在最後一行程式碼中會看到,我們要麼將window.lab49賦給其自身,要麼將空物件{}賦給它。儘管看起來有點兒怪,不過讓我們一起來看下這樣一個虛構系統,系統中的那些js檔案一律使用了上例中的函式包裝器(function wrapper)。

首個被引入的檔案會計算那個或語句(...||...),並發現左側的表示式undefined(未定義)。由於undefined會被判定為假,因此或語句會進一步計算右側表示式,在本例中就是空物件。或語句實際上是個表示式,它會返回計算結果,進而將結果賦給全域性變數window.lab49

現在輪到接下來的檔案使用此模式了,它會執行或語句,並發現window.lab49目前已是物件例項——真(物件例項會被判定為真)。此時或語句會走捷徑,並返回這個會立即賦給其自身的值——其實什麼都沒做。

由此導致的結果是,首個被引入的檔案會建立lab49名稱空間(就是個JavaScript物件),而且所有使用這種結構的後續檔案都只是重用此現有例項。

私有狀態(Private State)

正如剛才所說,由於位於函式內部,在其內部宣告的所有內容都處於該函式的作用域內,而非全域性作用域。這對於隔離程式碼真是棒極了,不過它還帶來了一種效果,那就是沒人能呼叫它。真是中看不中用啊!

剛剛還談到,建立window.lab49物件是為了用名稱空間來有效地管理我們的內容。而且由於變數lab49被附加到window物件上,因此它是全域性可用的。為了把其中的內容公佈給模組外部,或許有人會公開聲稱,我們要做的全部就是把一些值附加到那個全域性變數上。正如上例中所寫的add函式一樣。現在,在模組外部就可以通過lab49.add(2, 2)來呼叫add函式了。

在此函式中宣告一些值的另一結果是,要是某個值沒有通過將其附加到全域性名稱空間或者此模組外部的某個物件上的方式來顯示公開,那麼外部程式碼就訪問不到該值。實際上,我們恰好建立了一些私有值。

CommonJS模組(CommonJS Modules)

CommonJS是個社團,主要由伺服器端JavaScript執行庫(server-side JavaScript runtimes)編寫者組成,他們致力於將模組的公開及訪問標準化的工作。值得注意的是,他們提議的模組系統並非標準,因為它不是出自制定JavaScript標準的同一社團,所以它更像是伺服器端JavaScript執行庫編寫者彼此之間的非正式約定。

我通常會支援CommonJS的想法,但要搞清楚的是:它並不是一份崇高而神聖的規範(就像ES5一樣);它只不過是一些人在郵件列表中所討論的想法。而且多數想法都未付諸實現。

——Ryan Dahl, node.js的創造者

這份模組規範的核心相當直截了當。所有模組都要在其自身的上下文中進行計算,並且要有個全域性變數exports供模組使用。而全域性變數exports只是個普通的JavaScript物件,甚至可以自行往上面附加內容,它與上面展示的名稱空間物件(lab49)類似。要想訪問某個模組,需呼叫全域性函式require,並指明所請求的包識別符號。接著會計算此模組,而且無論返回何值都會將其附加到exports上。然後會快取此模組,以便後來的require函式呼叫。

// calculator.js
// 計算器模組——譯註
exports.add = function(n1, n2) {

};

// app.js
// 某個需要呼叫計算器模組的應用程式。
// './calculator'即包識別符號。——譯註
var calculator = require('./calculator');

calculator.add(2, 2);

要是擺弄過Node.js,或許會對以上程式碼有種似曾相識的感覺。這種用Node來實現CommonJS模組的方式真是出奇地簡單,在node-inspector(一款Node偵錯程式)中檢視模組時,會顯示其包裝在函式內部的內容,這些內容正是傳遞給exportsrequire的值。非常類似於上面展示的手卷模組內容。

有幾個node專案(StitchBrowserify),它們將CommonJS模組帶進了瀏覽器。伺服器端元件會把這些彼此獨立的模組js檔案打包到單獨的js檔案中,並把那些程式碼用生成的模組包裝器包起來。

CommonJS主要是為伺服器端JavaScript執行庫設計的,而且由於有幾個屬性使得它們難以在瀏覽器中組織客戶端程式碼。

  • require必須立即返回——要是已經擁有所有內容時這會工作得很好,不過這導致難以使用指令碼載入器(script loader)去非同步下載指令碼。
  • 每個模組佔一個檔案——為了合併為CommonJS模組,必須把它們以某種風格組織起來,幷包裹到一個函式中。要是沒有類似於上面所提及的伺服器元件,那麼就難以使用它們,並且在許多環境(ASP.NET,Java)下這些伺服器元件尚不存在。

非同步模組定義(Asynchronous Module Definition)

非同步模組定義(Asynchronous Module Definition,通常稱為AMD)已被設計為適合於瀏覽器的模組格式。它起初源於CommonJS社團的提案,不過自從遷移到GitHub上以後,現已加入了配套的測試套件,以便模組系統編寫者來驗證其程式碼是否符合AMD的API。

AMD的核心是define函式。呼叫define函式最常見的方式是傳入三個引數——模組名(也就是說不再與檔名繫結)、該模組依賴的模組識別符號陣列、以及將會返回該模組定義的工廠函式。(呼叫define函式的其他方式——詳細資訊請參閱AMD wiki)。

// 定義calculator(計算器)模組。——譯註
define('calculator', ['adder'], function(adder) {
    // 返回具有add方法的匿名物件。——譯註
    return {
        add: function(n1, n2) {
            /*
             * 實際呼叫的是adder(加法器)模組的add方法。
             * 而且adder模組已在前一引數['adder']中指明瞭。——譯註
             */
            return adder.add(n1, n2);
        }
    };
});

由於此模組的定義包在define函式的呼叫中,因此這意味著可以欣然將多個模組都放在單個js檔案中。此外,由於當呼叫模組工廠函式define時,模組載入器已擁有控制權,因此它可以自行安排時間去解決(模組間的)依賴關係——對於那些需要先非同步下載的模組,真可謂得心應手。

為了與原先的CommonJS模組提案保持相容已做出了巨大的努力。有些特殊行為是為了能在模組工廠函式中使用requireexports,這意味著,那些傳統的CommonJS模組可直接拿來用。

看起來AMD正在成為頗受歡迎的組織客戶端JavaScript應用程式的方式。無論是如RequireJScurl.js等模組資源載入器,還是像Dojo等最近已支援AMD的JavaScript應用程式,情況都是如此。

這是否意味著JavaScript很爛?(Does this mean JavaScript sucks?)

缺乏語言級別的結構,而無法將程式碼組織到模組中,這可能會讓來自其他語言的開發者覺得很不爽。然而,正因為此缺陷才迫使JavaScript開發者想出他們自己的模組組織模式,而且我們已經能夠隨著JavaScript應用程式的發展進行迭代並改進。欲深入瞭解此主題請訪問Tagneto的部落格。

想象一下,即便在10年前就已將此類功能引入語言。那麼他們也不可能想到後來的那些需求,例如在伺服器上執行大型JavaScript應用程式、在瀏覽器中非同步載入資源、或者像text templates(文字模板)(就是些文字載入器,其功能類似於RequireJS)那樣引入資源等等。

正在考慮將模組(Modules)作為Harmony/ECMAScript 6的語言級別功能。這多虧了模組系統編寫者們的奇思妙想、以及過去數年中所做的辛勤工作,更有可能的是,我們最終將得到適合於構建現代JavaScript應用程式的語言。

檢視英文原文:JavaScript Modules


關於作者

David Padbury

大家好,我是David Padbury。我在位於紐約的Lab49公司從事為金融行業建立高階應用程式的工作。我不僅把大部分時間花在開發複雜的HMTL5及JavaScript前端系統上,而且還常常會涉獵Java、.NET、以及其他企業型別的內容。

在一些使用者組及會議上,我談到過許多與HTML5及JavaScript有關的內容,偶爾也會提及node.js。目前,我致力於幫助那些熟悉更為傳統的胖客戶端技術(例如WPF、Silverlight、Flex、或Swing)的開發者,以便他們理解如何使用HTML5來構建類似的應用程式。要是您正在圍繞這些主題尋找演講者,那麼請聯絡我

在此釋出內容僅代表個人觀點,與我的老闆無關。

相關文章