1. 程式人生 > >JavaScript Modules

JavaScript Modules

One of the first challenges developers new to JavaScript who are building large applications will have to face is how to go about organizing their code. Most start by embedding hundreds of lines of code between a<script> tag which works but quickly turns into a mess. The difficultly is that JavaScript doesn’t offer any obvious help with organizing our code. Literally where C# hasusing,

 Java hasimport - JavaScript has nothing. This has forced JavaScript authors to experiment with different conventions and to use the language we do have to create practical ways of organizing large JavaScript applications.

The patterns and tools and practices that will form the foundation of Modern JavaScript are going to have to come from outside implementations of the language itself

The Module Pattern

One of the most widely used approaches to solve this problem is known as the Module Pattern. I’ve attempted to explain a basic example below and talk about some of it’s properties. For a much better description and a fantastic run down of different approaches take a look at Ben Cherry’s post –

JavaScript Module Pattern: In-Depth.

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

In the above example we’ve used a number of basic features from the language to create constructs like what we see in languages like C# and Java.

Isolation

You’ll notice that the code is wrapped inside a function which is invoked immediately (check the last line). By default in the browser JavaScript files are evaluated in the global scope so anything we declared inside our file would be available everywhere. Imagine if in lib1.js we had a var name = '...' statement then in lib2.js we had anothervar name = '...' statement. The second var statement would replace the value of the first – not good. However as JavaScript has function scoping, in the above example everything is declared in it’s own scope away from the global. This means anything in this function will be isolated from whatever else is going on in the system.

Namespacing

In the last line you’ll notice that we’re assigning window.lab49 to either itself or to an empty object literal. It looks a bit odd but let’s walk through an imaginary system where we have a number of js files all using the above function wrapper.

The first file to get included will evaluate that OR statement and find that the left hand side is undefined. This is a falsely value so the OR statement will go ahead and evaluate the right hand side, in this case an empty object literal. The OR statement is actually an expression that will return it’s result and go ahead and assign it to the globalwindow.lab49.

Now the next file to use this pattern will get to the OR statement and find thatwindow.lab49 is now an instance of an object – a truthy value. The OR statement will short circuit and return this value that is immediately assigned to itself – effectively doing nothing.

The result of this is that the first file in will create our lab49 namespace (just a JavaScript object) and every subsequent file using this construct will just reuse the existing instance.

Private State

As we just talked about due to being inside a function, everything declared inside it is in the scope of that function and not the global scope. This is great to isolate our code but it also has the effect that no one could call it. Pretty useless.

As we also just talked about we’re creating a window.lab49 object to effectively namespace our content. This lab49 variable is available globally as it’s attached to the window object. To expose things outside of our module, publically you may say, all we need to do attach values to that global variable. Much like we’re doing with our add function in the above example. Now outside of our module our add function can be called withlab49.add(2, 2).

As another result of declaring our values inside of this function, if a value isn’t explicitly exposed by attaching it to our global namespace or something outside of the module there is no way for external code to reach it. In practice, we’ve just created some private values.

CommonJS Modules

CommonJS is a group primarily made up of authors of server-side JavaScript runtimes who have attempted to standardize exposing and accessing modules. It’s worth noting however that their proposed module system is not a standard from the same group that creates the JavaScript standard so it’s become more of an informal convention between the authors of server-side JavaScript runtimes.

I generally support the CommonJS idea, but let’s be clear: it’s hardly a specification handed down by the gods (like ES5); it’s just some people discussing ideas on a mailing list. Most of these ideas are without actual implementations.

Ryan Dahl, creator of node.js

The core of the Modules specification is relatively straight forward. Modules are evaluated in their own context and have a globalexports variable made available to them. Thisexports variable is just a plain old JavaScript object which you can attach things too, similar to the namespace object we demonstrated above. To access a module you call a globalrequire function and give an identifier for the package you are requesting. This then evaluates the module and returns whatever was attached to theexports. This module will then be cached for subsequent require calls.

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

If you’ve ever played with Node.js you’ll probably find the above familiar. The way that Node implements CommonJS modules is surprisingly easy, looking at a module inside node-inspector (a Node debugger) will show its content wrapped inside a function that is being passed values for exports and require. Very similar to the hand rolled modules we showed above.

There’s a couple of node projects (Stitch and Browserify) which bring CommonJS Modules to the browser. A server-side component will bundle these individual module js files into a single js file with a generated module wrapper around them.

CommonJS was mainly designed for server-side JavaScript runtimes and due to that there’s a couple of properties which can make them difficult for organization of client-side code in the browser.

  • require must return immediately – this works great when you already have all the content but makes it difficult to use a script loader to download the script asynchronously.
  • One module per file – to combine CommonJS modules they need to be wrapped in a function and then organized in some fashion. This makes them difficult to use without some server component like the ones mentioned above and in many environments (ASP.NET, Java) these don’t yet exist.

Asynchronous Module Definition

The Asynchronous Module Definition (commonly known as AMD) has been designed as a module format suitable for the browser. It started life as a proposal from the CommonJS group but has since moved ontoGitHub and is now accompanied by asuite of tests to verify compliance to the AMD API for module system authors.

The core of AMD is the define function. The most common way to call define accepts three parameters – the name of the module (meaning that it’s no longer tied to the name of the file), an array of module identifiers that this module depends on, and a factory function which will return the definition of the module. (There are other ways to call define – check out theAMD wiki for full details).

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

Because of this module definition is wrapped in the define call it means you can happily have multiple modules inside a single js file. Also as the module loader has control over when the define module factory function is invoked it can resolve the dependencies in its own time – handy if those modules have to first be downloaded asynchronously.

A significant effort has been made to remain compatible with the original CommonJS module proposal. There is special behavior for usingrequire andexports within a module factory function meaning that traditional CommonJS modules can be dropped right in.

AMD looks to be becoming a very popular way to organize client-side JavaScript applications. Whether it be through module resource loaders likeRequireJS orcurl.js, or JavaScript applications that have recently embraced AMD likeDojo.

Does this mean JavaScript sucks?

The lack of any language level constructs for organization of code into modules can be quite jarring for developers coming from other languages. However as this deficiency forced JavaScript developers to come up with their own patterns for how modules were structured we’ve been able to iterate and improve as JavaScript applications have evolved. Follow theTagneto blog for some insight into this.

Imagine if this type of functionality had been included in the language 10 years ago. It’s unlikely they would have imagined the requirements for running large JavaScript applications on the server, loading resources asynchronously in the browser, or including resources like text templates that loaders like RequireJS are able to do.

Modules are being considered as a language level feature for Harmony/ECMAScript 6. Thanks to the thought and hard work of authors of module systems over the past few years, it’s much more likely that what we end up getting will be suitable for how modern JavaScript applications are built.

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

形成現代JavaScript基礎的那些模式、工具及實踐必將來自語言本身以外的實現

模組模式(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.jsexports.add = function(n1, n2) {
};
// app.jsvar 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應用程式。