隨着BS架構的發展,網站逐漸變成了互聯網應用程序,嵌入網絡的JavaScript代碼越來越龐大,越來越復雜(業務邏輯處理或用戶交互很多寫在前端)。網頁越來越像桌面程序,需要一個團隊分工協作、進度管理、單元測試等。。開發者不得不使用軟件工程的方法,管理網頁的業務邏輯。因此JavaScript模塊化編程已經成了一個迫切的需求,理想的情況下是開發者只需要實現核心的業務邏輯,其他業務處理都可以加載別人已經寫好的模塊,做到明確分工而不會相互影響。
但是,JavaScript卻不是一種模塊化編程語言,它不支持類(class),更別說模塊(module)了。雖然ECMAScipt正在謀划支持和推廣類和模塊的概念,但要實際投入生產還是遙遙無期,只能自己另外想辦法。為此JavaSript社區做了很多努力,努力在現有的運行環境中,利用現有的資源,實現模塊化的效果。
模塊化的原始寫法
模塊的定義就是實現特定功能的一組方法,只要把不同的函數(以及記錄狀態的變量)簡單地放在一起,就算是一個模塊了。
function f1() { // doSomething } function f2() { // doSomething }
上面的函數f1()和f2()共同組成了一個模塊,使用的時候直接通過函數名調用就行了。這種做法的缺點很明顯,既污染了全局變量(f1和f2處在全局的上下文棧中,屬於window的屬性),也無法保證不與其他模塊發生變量名沖突,而且模塊成員之間看不出直接的關系。
模塊化的對象寫法
為了解決上面的缺點,可以把模塊寫成一個對象,所有的模塊成員都放到這個對象里面。
var module1 = { status: 233, f1: function() { // doSomething }, f2: function() { // doSomething } }
上面的函數f1()和函數f2()都封裝在了module1對象里,使用的時候就是通過訪問module1對象的屬性。
module1.f1();
但是,這樣的寫法會暴露所有的模塊成員,且內部的狀態可以被外部改寫。
module1.status = 666;
模塊化的立即執行函數寫法
使用立即執行函數(Immediately-Invoked Function Expression,IIFE),可以達到不暴露私有成員的目的。
var module1 = (function() { var status = 233; var f1 = function() { // doSomething }; var f2 = function() { // doSomething }; })();
使用這樣的寫法,外部的代碼就無法讀取到內部的變量。
console.log(module1.status); // undefined
這種寫法,就是JavaScript模塊化的基本寫法,后面的實現基本上都是依照這個思路。
模塊化的放大模式
如果一個模塊很大,就會要拆分成幾個小的模塊,或者一個模塊需要繼承另一個模塊,這個時候就要采用放大模式(Augmentation)。
var module1 = (function(mod) { mod.f3 = function() { // doSomething }; return mod; })(module1);
這里為module1模塊添加了一個新函數f3(),然后返回新的module1模塊。也就是把舊的對象傳進來,給這個對象添加屬性,然后返回添加了屬性后的對象,相當於擴展。
模塊化的寬放大模式
在瀏覽器環境中,模塊的各個部分通常都是從網上獲取的,有時無法知道哪個部分會先加載。如果采用上一節的寫法,第一個執行的部分有可能加載一個不存在的空對象,這時就要采用寬放大模式(Loose Augmentation)。
var module1 = (function(mod) { // doSomething return mod; })(window.module1 || {});
與放大模式相比,寬放大模式就是立即執行函數的參數可以是空對象。
模塊化的全局變量輸入
獨立性是模塊化的重要特點,模塊內部最好不與程序的其他部分直接交互。
為了在模塊內部調用全局變量,必須顯式地將其他變量輸入模塊。
var module1 = (function($, YAHOO) { // doSomething })(jQuery, YAHOO);
這里的module1模塊中需要使用jQuery庫和YUI庫,於是就把這兩個庫(其實是兩個模塊)當作參數輸入module1。這樣做除了保證模塊的獨立性,也使得模塊之間的依賴關系變得明顯。
模塊化的幾種規范
模塊化的前提是要遵循同一套規范,否則模塊之間的調用會十分困難。
JavaScript官方沒有模塊化的規范,目前通用的民間規范主要有CommonJS(服務端js模塊化的規范,NodeJS是這種規范的實現)、AMD(Asynchronous Module Definition異步模塊定義,RequireJS遵循此規范)和CMD(Common Module Definition,通用模塊定義,SeaJS遵循此規范)。
模塊化在服務端的規范:CommonJS
2009年,美國程序員Ryan Dahl創造了node.js項目,將JavaScipt語言用於服務器端編程(后端)。這標志着JavaScipt模塊化編程正式誕生。因為老實說,在瀏覽器環境下,沒有模塊也不是特別大的問題,畢竟網頁程序的復雜性有限;但是在服務器端,一定要有模塊,與操作系統和其他應用程序互動,否則根本沒法編程。
node.js的模塊系統,就是參照CommonJS規范實現的。在CommonJS中,有一個全局性的方法require(),用於加載模塊。假定有一個數學模塊math.js,就可以像下面這樣加載並調用模塊中提供的方法:
var math = require('math'); math.add(2, 3); // 5
更多的用法這里就不說了,只需要知道CommonJS是使用require()函數加載模塊就行了。
模塊化規范從服務端到客戶端的發展
有了服務端的模塊化之后,大家就想要客戶端的模塊化了。而且最好兩者能兼容,一個模塊不用修改,在服務器和瀏覽器都可以運行。但是由於一個重大局限,使得CommonJS規范不適用於瀏覽器環境。這是因為,在上面的代碼中,調用math的方法必須要在math.js加載完成。也就是說,如果加載時間很長,整個應用就會停在那里等。
這對於服務器端不是一個問題,因為所有的模塊都存放在本地硬盤,可以同步加載完成,等待時間就是硬盤的讀取時間。但是對於瀏覽器來說,這卻是一個大問題,因為模塊都放在服務器端,等待時間取決於網速的快慢,可能要等很長時間,可能會導致瀏覽器處於假死狀態。
因此瀏覽器端的模塊化不能使用同步加載(Synchronous),只能使用異步加載(Asynchronous)。這就是AMD規范誕生的背景。
模塊化在客戶端的規范:AMD
AMD(Asynchronous Module Definition,異步模塊定義)采用異步方式加載模塊,模塊的加載不影響它后面語句的運行。所有依賴這個模塊的語句,都定義在一個回調函數中,直到加載完成之后,這個回調函數才會運行。
AMD也采用require()語句加載模塊,不同於CommonJS的是,它要求兩個參數:
require([module], callback);
第一個參數[moudle],是一個數組,里面的成員就是要加載的模塊;第二個參數callback,則是加載成功之后的回調函數。如果將前面的代碼改寫成AMD形式,就是這樣:
require(['math'], function(math) { math.add(2, 3); });
這樣,math.add()與math模塊的加載就不是同步的,瀏覽器也不會發生假死的狀況。所以很顯然地是AMD比較適合瀏覽器環境。應用AMD規范的主要有require.js。
模塊化在客戶端的規范:CMD
CMD是SeaJS在推廣過程中對模塊定義的規范化產出。
AMD和CMD的區別:
1.對於依賴的模塊,AMD是提前執行,CMD是延遲執行。CMD推崇的是as lazy as possible,即盡可能得懶加載(延遲加載),即在需要得時候才加載。
2.CMD推崇依賴就近,AMD推崇依賴前置。
// CMD define(function(require, exports, module) { var a = require('./a'); a.doSomething(); var b = require('./b'); // 依賴可以就近書寫 b.doSomething(); }) // AMD 默認推薦的是 define(['./a', './b'], function(a, b) { // 依賴必須一開始就寫好 a.doSomething(); b.doSomething(); })
3.AMD的API默認是一個當多個用,CMD的API則是嚴格區分,推崇職責單一。比如在AMD里,require分全局和局部,而在CMD里則沒有全局require,而是根據模塊系統的完備性,提供seajs.use來實現模塊系統的加載啟動。CMD里,每個API都簡單存粹。
模塊化的優點總結
1.解決了命名的沖突問題。多人開發的場景下,容易出現命名沖突,模塊化通過內部封裝與外部隔離能有效防止命名沖突的問題。
2.解決了文件的依賴問題,使文件易於管理。如果有很多js文件相互依賴,依賴關系和加載順序都是讓人頭冷的問題。使用模塊化就可以很好地實現依賴管理(使用依賴都要提前聲明)。
3.提高代碼的可讀性。各個模塊各自完成自己的功能,專司其職,除了問題也會便於維護。
4.提高代碼的復用性。可以抽提特定的通用功能作為一個通用的模塊。
"可是怎么辦,想起你的時候,心還是會疼。"