在早期編寫JavaScript時,我們只需在<script>標簽內寫入JavaScript的代碼就可以滿足我們對頁面交互的需要了。但隨著時間的推移,時代的發展,原本的那種簡單粗暴的編寫方式所帶來的諸如邏輯混亂,頁面複雜,可維護性差,全局變數暴露等問題接踵而至,前輩們為瞭解決這些問題提出了很種的解決方 ...
在早期編寫JavaScript時,我們只需在<script>標簽內寫入JavaScript的代碼就可以滿足我們對頁面交互的需要了。但隨著時間的推移,時代的發展,原本的那種簡單粗暴的編寫方式所帶來的諸如邏輯混亂,頁面複雜,可維護性差,全局變數暴露等問題接踵而至,前輩們為瞭解決這些問題提出了很種的解決方案,其中之一就是JavaScript模塊化編程。總的來說,它有以下四種優點:
- 解決項目中的全局變數污染的問題。
- 開發效率高,有利於多人協同開發。
- 職責單一,方便代碼復用和維護 。
- 解決文件依賴問題,無需關註引用文件的順序。
先行者CommonJs
2009年Node.js橫空出世,將JavaScript帶到了伺服器端領域。而對於伺服器端來說,沒有模塊化那可是不行的。因此CommonJs社區的大牛們開始發力了,制定了一個與社區同名的關於模塊化的規範——CommonJs。它的規範主要如下:
- 模塊的標識應遵循的規則(書寫規範)。
- 定義全局函數require,通過傳入模塊標識來引入其他模塊,執行的結果即為別的模塊暴露出來的API。
- 如果被require函數引入的模塊中也包含依賴,那麼依次載入這些依賴。
- 如果引入模塊失敗,那麼require函數應該報一個異常。
- 模塊通過變數exports來向外暴露API,exports只能是一個對象,暴露的API須作為此對象的屬性。
根據CommonJS規範的規定,每個文件就是一個模塊,有自己的作用域,也就是在一個文件裡面定義的變數、函數、類,都是私有的,對其他文件是不可見的。通俗來講,就是說在模塊內定義的變數和函數是無法被其他的模塊所讀取的,除非定義為全局對象的屬性。
1 // addA.js 2 const a = 1; 3 const addA = function(value) { 4 return value + a; 5 }
上面代碼中,變數a和函數addA,是當前文件addA.js私有的,其他文件不可見。如果想在多個文件中分享變數a,必須定義為global對象的屬性:
1 global.a = 1;
這樣我們就能在其他的文件中訪問變數a了,但這種寫法不可取,輸出模塊對象最好的方式是module.exports:
1 // addA.js 2 var a = 1; 3 var addA = function(value) { 4 return value + x; 5 } 6 module.exports.addA = addA;
上面代碼通過module.exports對象輸出了一個函數,該函數就是模塊外部與內部通信的橋梁。載入模塊需要使用require方法,該方法讀取一個文件並執行,最後返迴文件內部的module.exports對象。
1 var example = require('./addA.js'); 2 console.log(example.addA(1)); //2
CommonJs看起來是一個很不錯的選擇,擁有模塊化所需要的嚴格的入口和出口,看起來一切都很美好,但它的一個特性卻決定了它只能在伺服器端大規模使用,而在瀏覽器端發揮不了太大的作用,那就是同步!這在伺服器端不是什麼問題,但放在瀏覽器端就出現問題了,因為文件都放在伺服器上,如果網速不夠快的話,前面的文件如果沒有載入完成,瀏覽器就會失去響應!因此為了在瀏覽器上也實現模塊化得來個非同步的模塊化才行!根據這個需求,我們的下一位主角——AMD就產生了!
AMD 非同步模塊定義
AMD的全名叫做:Asynchronous Module Definition即非同步模塊定義。它採用了非同步的方式來載入模塊,然後在回調函數中執行主邏輯,因此模塊的載入不影響它後面的模塊的運行。它的規範如下:
1 define(id?, dependencies?, factory);
- 用全局函數define來定義模塊;
- id為模塊標識,遵從CommonJS Module Identifiers規範
- dependencies為依賴的模塊數組,在factory中需傳入形參與之一一對應
- 如果dependencies的值中有"require"、"exports"或"module",則與commonjs中的實現保持一致
- 如果dependencies省略不寫,則預設為["require", "exports", "module"],factory中也會預設傳入require,exports,module
- 如果factory為函數,模塊對外暴漏API的方法有三種:return任意類型的數據、exports.xxx=xxx、module.exports=xxx
- 如果factory為對象,則該對象即為模塊的返回值
具體分析AMD我們通過require.js來進行。require.js是一個非常小巧的JavaScript模塊載入框架,是AMD規範最好的實現者之一,require.js的出現主要是來解決兩個問題:
- 實現JavaScript文件的非同步載入,避免網頁失去響應。
- 管理模塊的依賴性,管理模塊的相互獨立性,也就是我們常說的低耦合,這有利於代碼的編寫與維護。
使用require.js我們首先要載入它,為了避免瀏覽器未響應,我們在後面可以加上async,告訴瀏覽器這個文件需要非同步載入(IE不支持該屬性,所以需要把defer也加上):
1 <script src="js/require.js" defer async="true" ></script>
定義模塊時,在require.js中我們可以使用define,但define對於需要定義的模塊是否是獨立的模塊的寫法是不同;所謂的獨立模塊就是指不依賴於其他模塊的模塊,而非獨立模塊就是指不依賴於其他模塊的模塊。
define在定義獨立模塊時有兩種寫法,一種是直接定義對象;另一種是定義一個函數,在函數內的返回值就是輸出的模塊了:
1 define({ 2 method1: function() {}, 3 method2: function() {}, 4 }); 5 //等價於 6 define(function () { 7 return { 8 method1: function() {}, 9 method2: function() {}, 10 } 11 });
如果define定義非獨立模塊,那麼它的語法就規定一定是這樣的:
1 define(['module1', 'module2'], function(m1, m2) { 2 3 return { 4 method: function() { 5 m1.methodA(); 6 m2.methodB(); 7 } 8 } 9 10 });
define在這個時候接受兩個參數,第一個參數是module是一個數組,它的成員是我們當前定義的模塊所依賴的模塊,只有順利載入了這些模塊,我們新定義的模塊才能成功運行。第二個參數是一個函數,當前面數組內的成員全部載入完之後它才運行,它的參數m與前面的module是一一對應的。這個函數必須返回一個對象,以供其他模塊調用,需要註意的是,回調函數必須返回一個對象,這個對象就是你定義的模塊。
在載入模塊方面,AMD和CommonJs都是使用require。require.js也同樣如此,它要求兩個參數:module,callback:
require([module], callback);
第一個參數[module],是一個數組,裡面的成員就是需要載入的模塊;第二個參數callback,則是載入成功之後的回調函數。
require方法本身也是一個對象,它帶有一個config方法,用來配置require.js運行參數。config方法接受一個對象作為參數。
1 //別名配置 2 requirejs.config({ 3 paths: { 4 jquery: [ //如果第一個路徑不能完成載入,就調到第二個路徑繼續進行載入 5 '//cdnjs.cloudflare.com/ajax/libs/jquery/2.0.0/jquery.min.js', 6 'lib/jquery' //本地文件中不需要寫.js 7 ] 8 } 9 }); 10 11 //引入模塊,用變數$表示jquery模塊 12 requirejs(['jquery'], function ($) { 13 $('body').css('background-color','black'); 14 });
雖然require.js實現了非同步的模塊化,但它仍然有一些不足的地方,在使用require.js的時候,我們必須要提前載入所有的依賴,然後才可以使用,而不是需要使用時再載入,使得初次載入其他模塊的速度較慢,提高了開發成本。
CMD 通用模塊定義
CMD的全稱是Common Module Definition,即通用模塊定義。它是由螞蟻金服的前端大佬——玉伯提出來的,實現的JavaScript庫為sea.js。它和AMD的require.js很像,但載入方式不同,它是按需就近載入的,而不是在模塊的開始全部載入完成。它有以下兩大核心特點:
- 簡單友好的模塊定義規範:Sea.js 遵循 CMD 規範,可以像 Node.js 一般書寫模塊代碼。
- 自然直觀的代碼組織方式:依賴的自動載入、配置的簡潔清晰,可以讓我們更多地享受編碼的樂趣。
在CMD規範中,一個文件就是一個模塊,代碼書寫的格式是這樣的:
define(factory);
當factory為函數時,表示模塊的構造方法,執行該方法,可以得到該模塊對外提供的factory介面,factory 方法在執行時,預設會傳入三個參數:require、exports 和 module:
1 // 所有模塊都通過 define 來定義 2 define(function(require, exports, module) { 3 4 // 通過 require 引入依賴 5 var $ = require('jquery'); 6 var Spinning = require('./spinning'); 7 8 // 通過 exports 對外提供介面 9 exports.doSomething = ... 10 11 // 或者通過 module.exports 提供整個介面 12 module.exports = ... 13 14 });
它與AMD的具體區別其實我們也可以通過代碼來表現出來,AMD需要在模塊開始前就將依賴的模塊載入出來,即依賴前置;而CMD則對模塊按需載入,即依賴就近,只有在需要依賴該模塊的時候再require就行了:
1 // AMD規範 2 define(['./a', './b'], function(a, b) { // 依賴必須一開始就寫好 3 a.doSomething() 4 // 此處略去 100 行 5 b.doSomething() 6 ... 7 }); 8 // CMD規範 9 define(function(require, exports, module) { 10 var a = require('./a') 11 a.doSomething() 12 // 此處略去 100 行 13 var b = require('./b') 14 // 依賴可以就近書寫 15 b.doSomething() 16 // ... 17 });
需要註意的是Sea.js的執行模塊順序也是嚴格按照模塊在代碼中出現(require)的順序。
從運行速度的角度來講,AMD雖然在第一次使用時較慢,但在後面再訪問時速度會很快;而CMD第一次載入會相對快點,但後面的載入都是重新載入新的模塊,所以速度會慢點。總的來說,
require.js的做法是並行載入所有依賴的模塊, 等完成解析後, 再開始執行其他代碼, 因此執行結果只會"停頓"1次, 而Sea.js在完成整個過程時則是每次需要相應模塊都需要進行載入,這期間會停頓是多次的,因此require.js從整體而言相對會比Sea.js要快一些。