如果你是 JavaScript 的新手,一些像 “module bundlers vs module loaders”、“Webpack vs Browserify” 和 “AMD vs.CommonJS” 這樣的術語,很快讓你不堪重負。
JavaScript 模塊系統可能令人生畏,但理解它對 Web 開發人員至關重要。
在這篇文章中,我將以簡單的言語(以及一些代碼示例)為你解釋這些術語。 希望這對你有會有幫助!
什麼是模塊?
好作者能將他們的書分成章節,優秀的程序員將他們的程序劃分為模塊。
就像書中的章節一樣,模塊只是文字片段(或代碼,視情況而定)的集群。然而,好的模塊是高內聚低松耦的,具有不同的功能,允許在必要時對它們進行替換、刪除或添加,而不會擾亂整體功能。
為什麼使用模塊?
使用模塊有利於擴展、相互依賴的代碼庫,這有很多好處。在我看來,最重要的是:
1)可維護性: 根據定義,模塊是高內聚的。一個設計良好的模塊旨在儘可能減少對代碼庫部分的依賴,這樣它就可以獨立地增強和改進,當模塊與其他代碼片段解耦時,更新單個模塊要容易得多。
回到我們的書的例子,如果你想要更新你書中的一個章節,如果對一個章節的小改動需要你調整每一個章節,那將是一場噩夢。相反,你希望以這樣一種方式編寫每一章,即可以在不影響其他章節的情況下進行改進。
2)命名空間: 在 JavaScript 中,頂級函數範圍之外的變量是全局的(這意味著每個人都可以訪問它們)。因此,“名稱空間汙染”很常見,完全不相關的代碼共享全局變量。
在不相關的代碼之間共享全局變量在開發中是一個大禁忌。正如我們將在本文後面看到的,通過為變量創建私有空間,模塊允許我們避免名稱空間汙染。
3)可重用性:坦白地說:我們將前寫過的代碼複製到新項目中。 例如,假設你從之前項目編寫的一些實用程序方法複製到當前項目中。
這一切都很好,但如果你找到一個更好的方法來編寫代碼的某些部分,那麼你必須記得回去在曾經使用過的其他項目更新它。
這顯然是在浪費時間。如果有一個我們可以一遍又一遍地重複使用的模塊,不是更容易嗎?
如何創建模塊?
有多種方法來創建模塊,來看幾個:
模塊模式
模塊模式用於模擬類的概念(因為 JavaScript 本身不支持類),因此我們可以在單個對象中存儲公共和私有方法和變量——類似於在 Java 或 Python 等其他編程語言中使用類的方式。這允許我們為想要公開的方法創建一個面向公共的 API,同時仍然將私有變量和方法封裝在閉包範圍中。
有幾種方法可以實現模塊模式。在第一個示例中,將使用匿名閉包,將所有代碼放在匿名函數中來幫助我們實現目標。(記住:在 JavaScript 中,函數是創建新作用域的唯一方法。)
例一:匿名閉包
<code>(function () {
// 將這些變量放在閉包範圍內實現私有化
var myGrades = [93, 95, 88, 0, 55, 91];
var average = function() {
var total = myGrades.reduce(function(accumulator, item) {
return accumulator + item}, 0);
return '平均分 ' + total / myGrades.length + '.';
}
var failing = function(){
var failingGrades = myGrades.filter(function(item) {
return item < 70;});
return '掛機科了 ' + failingGrades.length + ' 次。';
}
console.log(failing()); // 掛機科了次
}());
/<code>
使用這個結構,匿名函數就有了自己的執行環境或“閉包”,然後我們立即執行。這讓我們可以從父(全局)命名空間隱藏變量。
這種方法的優點是,你可以在這個函數中使用局部變量,而不會意外地覆蓋現有的全局變量,但仍然可以訪問全局變量,就像這樣:
<code> var global = '你好,我是一個全局變量。)';
(function () {
// 將這些變量放在閉包範圍內實現私有化
var myGrades = [93, 95, 88, 0, 55, 91];
var average = function() {
var total = myGrades.reduce(function(accumulator, item) {
return accumulator + item}, 0);
return '平均分 ' + total / myGrades.length + '.';
}
var failing = function(){
var failingGrades = myGrades.filter(function(item) {
return item < 70;});
return '掛機科了 ' + failingGrades.length + ' 次。';
}
console.log(failing()); // 掛機科了次
onsole.log(global); // 你好,我是一個全局變量。
}());
/<code>
注意,匿名函數的圓括號是必需的,因為以關鍵字 function 開頭的語句通常被認為是函數聲明(請記住,JavaScript 中不能使用未命名的函數聲明)。因此,周圍的括號將創建一個函數表達式,並立即執行這個函數,這還有另一種叫法 立即執行函數(IIFE)。如果你對這感興趣,可以在這裡瞭解到更多。
例二:全局導入
jQuery 等庫使用的另一種流行方法是全局導入。它類似於我們剛才看到的匿名閉包,只是現在我們作為參數傳入全局變量:
<code>(function (globalVariable) {
// 在這個閉包範圍內保持變量的私有化
var privateFunction = function() {
console.log('Shhhh, this is private!');
}
// 通過 globalVariable 接口公開下面的方法
// 同時將方法的實現隱藏在 function() 塊中
globalVariable.each = function(collection, iterator) {
if (Array.isArray(collection)) {
for (var i = 0; i < collection.length; i++) {
iterator(collection[i], i, collection);
}
} else {
for (var key in collection) {
iterator(collection[key], key, collection);
}
}
};
globalVariable.filter = function(collection, test) {
var filtered = [];
globalVariable.each(collection, function(item) {
if (test(item)) {
filtered.push(item);
}
});
return filtered;
};
globalVariable.map = function(collection, iterator) {
var mapped = [];
globalUtils.each(collection, function(value, key, collection) {
mapped.push(iterator(value));
});
return mapped;
};
globalVariable.reduce = function(collection, iterator, accumulator) {
var startingValueMissing = accumulator === undefined;
globalVariable.each(collection, function(item) {
if(startingValueMissing) {
accumulator = item;
startingValueMissing = false;
} else {
accumulator = iterator(accumulator, item);
}
});
return accumulator;
};
}(globalVariable));
/<code>
在這個例子中,globalVariable 是唯一的全局變量。與匿名閉包相比,這種方法的好處是可以預先聲明全局變量,使得別人更容易閱讀代碼。
例三:對象接口
另一種方法是使用立即執行函數接口對象創建模塊,如下所示:
<code>var myGradesCalculate = (function () {
// 將這些變量放在閉包範圍內實現私有化
var myGrades = [93, 95, 88, 0, 55, 91];
// 通過接口公開這些函數,同時將模塊的實現隱藏在function()塊中
return {
average: function() {
var total = myGrades.reduce(function(accumulator, item) {
return accumulator + item;
}, 0);
return'平均分 ' + total / myGrades.length + '.';
},
failing: function() {
var failingGrades = myGrades.filter(function(item) {
return item < 70;
});
return '掛科了' + failingGrades.length + ' 次.';
}
}
})();
myGradesCalculate.failing(); // '掛科了 2 次.'
myGradesCalculate.average(); // '平均分 70.33333333333333.'
/<code>
正如您所看到的,這種方法允許我們通過將它們放在 return 語句中(例如算平均分和掛科數方法)來決定我們想要保留的變量/方法(例如 myGrades)以及我們想要公開的變量/方法。
例四:顯式模塊模式
這與上面的方法非常相似,只是它確保所有方法和變量在顯式公開之前都是私有的:
<code>var myGradesCalculate = (function () {
// 將這些變量放在閉包範圍內實現私有化
var myGrades = [93, 95, 88, 0, 55, 91];
var average = function() {
var total = myGrades.reduce(function(accumulator, item) {
return accumulator + item;
}, 0);
return'平均分 ' + total / myGrades.length + '.';
};
var failing = function() {
var failingGrades = myGrades.filter(function(item) {
return item < 70;
});
return '掛科了' + failingGrades.length + ' 次.';
};
// Explicitly reveal public pointers to the private functions
// that we want to reveal publicly
return {
average: average,
failing: failing
}
})();
myGradesCalculate.failing(); // '掛科了 2 次.'
myGradesCalculate.average(); // '平均分 70.33333333333333.'
/<code>
這可能看起來很多,但它只是模塊模式的冰山一角。 以下是我在自己的探索中發現有用的一些資源:
- Learning JavaScript Design Patterns:作者是 Addy Osmani,一本簡潔又令人印象深刻的書籍,蘊藏著許多寶藏。
- Adequately Good by Ben Cherry:包含模塊模式的高級用法示例。
- Blog of Carl Danley:模塊模式概覽,也是 JavaScript 許多設計模式的資源庫。
CommonJS 和 AMD
所有這些方法都有一個共同點:使用單個全局變量將其代碼包裝在函數中,從而使用閉包作用域為自己創建一個私有名稱空間。
雖然每種方法都有效且都有各自特點,但卻都有缺點。
首先,作為開發人員,你需要知道加載文件的正確依賴順序。例如,假設你在項目中使用 Backbone,因此你可以將 Backbone 的源代碼 以
更加詳細的介紹也可以在 Github 上查看:es-module-loader
此外,如果您想測試這種方法,請查看 SystemJS,它建立在 ES6 Module Loader polyfill 之上。 SystemJS 在瀏覽器和 Node 中動態加載任何模塊格式(ES6模塊,AMD,CommonJS 或 全局腳本)。
它跟蹤“模塊註冊表”中所有已加載的模塊,以避免重新加載先前已加載過的模塊。 更不用說它還會自動轉換ES6模塊(如果只是設置一個選項)並且能夠從任何其他類型加載任何模塊類型!
有了原生的 ES6 模塊後,還需要模塊打包嗎?
對於日益普及的 ES6 模塊,下面有一些有趣的觀點:
HTTP/2 會讓模塊打包過時嗎?
對於 HTTP/1,每個TCP連接只允許一個請求。這就是為什麼加載多個資源需要多個請求。有了 HTTP/2,一切都變了。HTTP/2 是完全多路複用的,這意味著多個請求和響應可以並行發生。因此,我們可以在一個連接上同時處理多個請求。
由於每個 HTTP 請求的成本明顯低於HTTP/1,因此從長遠來看,加載一組模塊不會造成很大的性能問題。一些人認為這意味著模塊打包不再是必要的,這當然有可能,但這要具體情況具體分析了。
例如,模塊打包還有 HTTP/2 沒有好處,比如移除冗餘的導出模塊以節省空間。 如果你正在構建一個性能至關重要的網站,那麼從長遠來看,打包可能會為你帶來增量優勢。 也就是說,如果你的性能需求不是那麼極端,那麼通過完全跳過構建步驟,可以以最小的成本節省時間。
總的來說,絕大多數網站都用上 HTTP/2 的那個時候離我們現在還很遠。我預測構建過程將會保留,至少在近期內。
CommonJS、AMD 與 UMD 會被淘汰嗎?
一旦 ES6 成為模塊標準,我們還需要其他非原生模塊規範嗎?
我覺得還有。
Web 開發遵守一個標準方法進行導入和導出模塊,而不需要中間構建步驟——網頁開發長期受益於此。但 ES6 成為模塊規範需要多長時間呢?
機會是有,但得等一段時間 。
再者,眾口難調,所以“一個標準的方法”可能永遠不會成為現實。
總結
希望這篇文章能幫你理清一些開發者口中的模塊和模塊打包的相關概念,共進步。
閱讀更多 JavaScript前端 的文章