JavaScript模塊化開發一瞥

來源: 圖靈社區  發布時間: 2012-06-29 15:44  閱讀: 7767 次  推薦: 5   原文鏈接   [收藏]  

  英文原文:JavaScript Modules

  對于那些初學JavaScript、同時又正用它構建大型應用程序的開發者而言,必須面對的首要挑戰是,該如何組織代碼。盡管起初通過在<script>標記之間嵌入數百行代碼就能跑起來,不過很快代碼會變得一塌糊涂。其中的難點在于,對于組織我們的代碼,JavaScript并未提供任何明顯幫助。從字面上看,C#有using,Java有import——而JavaScript一無所有。這就迫使JavaScript作者去嘗試各種不同約定(conventions),并用我們擁有的這種語言創建了一些實踐方法來組織大型JavaScript應用程序。

形成現代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)

  你會注意到這段代碼包在一個立即調用的函數里(查看最后一行)。在瀏覽器中,默認情況下會在全局范圍(global scope)級別上對JavaScript文件進行評估(evaluated),因此在我們在文件內聲明的任何內容都是隨處可用的。想象一下,如果在lib1.js中有句var name = '...',而lib2.js中有另一句var name = '...'。那么第二個var語句會替掉第一句的值——這可不太妙。然而,由于JavaScript擁有函數作用域(function scoping)級別,上例中所聲明的一切都在該函數自身作用域中,與全局作用域相脫離。這意味著,無論未來在系統中發生什么,位于該函數中的任何內容都會被隔離開來。

  命名空間(Namespacing)

  在最后一行中,你會發現我們要么把window.lab49賦給其自身,要么把空對象直接量(empty object literal)賦給它。盡管看起來有些奇怪,但是讓我們一起看下某個虛構的系統,在那里我們擁有若干js文件,所有文件都用了上例中的函數包裝器(function wrapper)。

  首個包含進來的文件會評估OR(邏輯或)語句,并發現左側表達式為undefined(未定義)。由于undefined是虛假值(falsely value),因此OR語句會繼續評估右側表達式,本例中是個空對象直接量。此OR語句實際上是個會返回評估結果的表達式,然后將結果賦給全局變量window.lab49

  現在,輪到下個文件來使用此模式了,它會獲得OR語句,并發現window.lab49當前是對象實例 — — 真值(truthy value)。OR語句會短路并返回這個值,并將此值立即賦給其自身 — — 實際上啥也沒做。

  這導致的結果是,首個包含進來的文件會創建我們的lab49命名空間(只是個JavaScript對象),而且每個使用這種結構的后續文件都只不過是重用這個現有實例。

  私有狀態(Private State)

  正如我們剛才所說,由于位于函數內部,在其內部聲明的一切內容都是處于該函數的范圍內,而不是全局范圍。對于隔離我們的代碼這真太棒了,此外,它還有個影響是,沒有人能調用它。中看不中用。

  剛剛我們還談到,我們創建了window.lab49對象來有效管地理我們內容的命名空間。而且這個lab49變量是全局可用的,因為它被附加到window對象上。要想把我們模塊中的內容暴露給外部,你可以公開地說,我們要做的就是把一些值附加到全局變量上。正如我們在上例中對add函數所做的一樣。現在,在我們的模塊外部可以通過lab49.add(2, 2)來調用我們的add函數了。

  在此函數內聲明我們的值的另一結果是,如果某個值不是通過將其附加到我們的全局命名空間或者模塊外部的某物的方法來顯示公開的,那么外部代碼將無法碰到它。事實上,我們剛剛就創建了一些私有值。

  CommonJS模塊(CommonJS Modules)

  CommonJS是一個主要由服務端JavaScript運行庫(server-side JavaScript runtimes)作者組成的小組,他們一直致力于暴露及訪問模塊的標準化工作(standardize exposing and accessing modules)。值得注意的是,盡管他們提議的模塊系統不是來自于創建JavaScript標準同一小組的一個標準,因此它更多地成為JavaScript運行庫作者之間的非正式約定(informal convention)。

我通常支持CommonJS的想法,但要搞清楚的是:它并不是一份崇高而神圣的規范(就像ES5一樣);它不過是某些人在郵件列表中所討論的想法。而且這些想法多數都沒有付諸實現。

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

  該模塊規范(Modules specification)的核心可謂開門見山。模塊(Modules)在它們自己的上下文中進行評估,并且擁有全局變量exports以供模塊使用。變量exports只是個普通的JavaScript對象(plain old JavaScript object),甚至你也可以往它上面附加內容,與我們上面展示的命名空間對象類似。為了訪問某個模塊,你要調用全局函數require,并指明你請求的包的標示符(identifier for the package)。然后評估該模塊,并且無論返回什么都會附加到exports上。此模塊將會緩存起來,以便后來的require函數調用來使用。

// calculator.js
exports.add = function(n1, n2) {
};
// app.js
var calculator = require('./calculator');
calculator.add(2, 2);

  如果你曾經玩過Node.js,那么你會發現上面的代碼很熟悉。這種用Node來實現CommonJS模塊的方式是出奇地簡單,在node-inspector(一款Node調試器)中查看某個模塊時將顯示其包裝在某個函數內部的內容,此函數正是傳遞給exportsrequire的值。非常類似于我們上面展示的手攢模塊。

  有幾個node項目(Stitch和Browserify),它們將CommonJS模塊帶進了瀏覽器。服務器端組件將這些彼此獨立的模塊js文件塞進一個單獨的js文件中,并在那些代碼外面包上生成的模塊包裝器(generated module wrapper)。

  CommonJS主要設計用于服務端JavaScript運行庫,而且由于有幾個屬性使得它們很難在瀏覽器中進行客戶端代碼的組織。

  • require必須立即返回——當你已經擁有所有內容時這會工作得非常好,但是當使用腳本加載器(script loader)異步下載腳本時就會有困難。
  • 每個文件一個模塊——為了合并為CommonJS模塊,必須把它們包裹到一個函數中,然后再組織為某種式樣。如果沒有某些服務器組件,正如上面提到的那些,就會讓它們難以使用,并且在許多環境(ASP.NET,Java)下這些服務器組件尚不存在。

  異步模塊定義(Asynchronous Module Definition)

  異步模塊定義(Asynchronous Module Definition,通常稱為AMD)已設計為適合于瀏覽器的模塊格式。它最初只是一個來自CommonJS小組的提議,但此后移到了GitHub上,而且現在伴有一個適用于模塊系統作者的測試套件,以便驗證對于AMD API的遵從性(compliance)。

  AMD的核心是define函數。調用define函數最常見的方式是接受三個參數——模塊名(也就是說不再與文件名綁定)、該模塊依賴的模塊標識符數組、以及工廠函數,它將返回該模塊的定義。(還有其他的方式調用define函數——詳細信息參閱AMD wiki)。

define('calculator', ['adder'], function(adder) {
    return {
        add: function(n1, n2) {
            return adder.add(n1, n2);
        }
    };
});

  由于此模塊的定義包在define函數的調用中,因此這就意味著,你可以愉快地在單個js文件內擁有多個模塊。此外,由于當調用define模塊工廠函數時,模塊加載器擁有控制權,因此它可在閑暇之余解決(模塊之間的)依賴關系——如果那些模塊必須首先異步下載,那就會很方便了。

  為了與原本的CommonJS模塊提議保持兼容已作出重大努力。當在模塊工廠函數中使用requireexports時會有特殊處理,這意味著,那些傳統的CommonJS模塊可直接拿來用。

  看起來AMD正在成為頗受歡迎的組織客戶端JavaScript應用程序的方式。無論是否通過如RequireJS或curl.js、或是像Dojo等最近已采用AMD的JavaScript應用程序等模塊資源加載器來組織代碼。

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

  缺乏將代碼組織到模塊中的語言級別的結構(language level constructs),這可能會讓來自于其他語言的開發者感覺很不爽。然而,正由于此缺陷才迫使JavaScript開發者想出他們自己的模塊構造模式,我們已經能夠隨著JavaScript應用程序的發展進行迭代和改進。欲深入了解此主題請訪問Tagneto的博客。

  想象一下,即使這種功能類型(即Module)在10年前就已包括在語言中。那么他們也不可能想到在服務器上運行大型JavaScript應用程序、在瀏覽器中異步加載資源、或者像文本模板(text templates)(那些載入器就像RequireJS所做的一樣)那樣包含資源等諸如此類的需求。

  正在考慮將模塊(Modules)作為Harmony/ECMAScript 6的語言級別功能。這多虧了模塊系統作者們的思想和過去幾年的辛勤工作,很可能我們最終得到的語言會適用于構建現代JavaScript應用程序。

5
0
 
 
 

文章列表

arrow
arrow
    全站熱搜
    創作者介紹
    創作者 大師兄 的頭像
    大師兄

    IT工程師數位筆記本

    大師兄 發表在 痞客邦 留言(0) 人氣()