http://alpha.wallhaven.cc/wallpaper/33246
在這篇文章的第一部分,我談到了什麼是模組,開發人員為什麼使用它們,以及,在你的程式中實現模組的不同方式。
在這第二部分,將會回答捆綁模組到底意味著什麼:為什麼要捆綁,捆綁的不同方法,以及在網頁開發中模組的未來發展。 1. 什麼是模組捆綁
抽象的概括,模組捆綁就是這樣一個簡單的處理:把一組模組以及它們的依賴,按照正確的次序,拼接在一個檔案或一組檔案裡。
但正如網頁開發的其它方方面面,棘手的總是潛藏在具體細節裡。 2. 為什麼一定要捆綁模組?
當你將程式劃分成模組時,一般把它們組織成不同的檔案或檔案夾。很可能你還有一組所用庫的模組,比如 Underscore、React。
結果,這些檔案每一個都得用 <script> 標籤引入你的主 HTML 檔案,當使用者訪問你的首頁時再由瀏覽器載入進來。每個檔案都用單獨的 <script> 標籤引入,意味著瀏覽器不得不分別挨個載入它們。
對網頁載入時間來說這簡直是噩夢。
於是,為瞭解決這個問題,我們把所有檔案捆綁,或“拼接”到一個檔案(有時也是一組檔案)中,正是為了減少請求數。當你聽到開發人員談論“構建步驟”或“構建過程”時,他們談的就是這個。
另一個加速構建操作的常用方法是,“縮減”捆綁後的代碼。縮減,是把原始碼中不需要的字元(如空格、評論、分行符號等等)移除,從而減小了代碼的總體積,卻不改變其功能。
資料更少,瀏覽器處理的時間就更短,便減少了下載檔案花費的時間。如果你見過帶 “min” 副檔名的檔案,比如 “ underscore-min.js ”,可能就已經留意到,相比 完整版 ,縮減版小了好多(不過很難閱讀)。
任務執行工具,如 Gulp、Grunt,讓開發人員操作拼接和縮減更簡單便捷。一邊是展示給開發人員看的人類可讀代碼,另一邊是提供給瀏覽器的捆綁後的電腦可讀代碼。 3. 捆綁模組有哪些不同的方法。
如果你用的是一種標準模組模式(在 第一部分 討論過)來定義模組,那麼拼接和縮減檔案不會出任何岔子,你其實就是把幾堆純 JavaScript 代碼捆成一束。
但如果你用的是非原生模組系統,瀏覽器不能像 CommonJS、AMD、甚至原生 ES6 模組格式那樣解析,你就需要用專門工具先轉化成排列正確、瀏覽器可識別的代碼。這正是 Browserify、RequireJS、Webpack 和其他模組捆綁工具,或模組載入工具粉墨登場的時候。
除了捆綁或載入你的模組,模組捆綁工具還有許多額外的功能,比如,當你修改或為了調試產生源碼映射時,它會自動重編譯你的代碼。
下面就來過一遍常用的模組捆綁方法: 4. 捆綁 CommonJS
從第一部分已經知道,CommonJS 是同步載入模組,雖然沒什麼毛病,但對瀏覽器來說不太現實。我曾提過有方法可以解決——其中之一就是模組捆綁工具 Browserify ,它是為瀏覽器編譯 CommonJS 模組的工具。
舉個例子,假如你有一個個檔案,它引入一個模組以計算一組數值的平均值:
var myDependency = require(‘myDependency’);var myGrades = [93, 95, 88, 0, 91];var myAverageGrade = myDependency.average(myGrades);
在這個情境中,我們只有一個依賴。用下面的這個命令,Browserify 會以這個檔案為入口把所有依賴的模組遞迴地捆綁進一個檔案:
browserify main.js -o bundle.js
Browserify 實際做的是,跳入檔案為每一個依賴分析它的抽象文法樹,從而遍曆出工程的整個依賴關係。一旦它搞懂了你的依賴結構,就把它們按照正確的順序捆綁進一個檔案。那時,你只需用一個 <script> 標籤,引入 bundle.js 到你的 HTML,從而只要一個 http 請求,就把你的所有原始碼下載下來了。哇,還不去捆綁。
與之類似,如果你有多個檔案,每個檔案又有多個依賴,你要做的也很簡單:告訴他你的入口檔案,然後就可以癱坐在椅子上看好戲。
最後產生的捆綁檔案,可以直接導向到一些工具做壓縮處理。 5. 捆綁 AMD
如果你用的是 AMD, 就需要像 RequireJS、Curl 那樣的 AMD 模組載入工具。模組載入工具(與模組捆綁工具不同)會動態載入你的程式所需的模組。
再提醒一下,AMD 跟 CommonJS 最大不同在於,AMD 會非同步載入模組。所以,如果你用 AMD,就無需捆綁模組到一個檔案裡,技術上講也就不需要執行捆綁模組動作的構建過程。因為非同步載入模組意味著在運行過程中逐步下載那些程式所必需的檔案,而不是使用者剛進入頁面就一下把所有檔案都下載下來。
然後實際中,每個使用者動作會額外產生大資料量的請求,這對產品而言很不合理。所以大多數網頁開發人員仍然為了更優的效能,使用構建工具捆綁、縮減他們的 AMD 模組,比如像 RequireJS 最佳化器 r.js 一樣的工具。
概括地說,在捆綁方面 AMD 和 CommonJS 的區別在於:開發時, 用 AMD 的程式可以省去構建過程。直到實際運行時,再讓 r.js 那些最佳化工具介入處理。
要想圍觀 CommonJS 和 AMD 更有意思的“互撕”,看看這篇 Tom Dale 的博文 。 6. Webpack
捆綁工具算有些年頭了,Webpack 則是初來乍到的新人。它的設計基於不知道你所用的模組系統是什麼,從而讓開發人員各依所需選用 CommonJS、AMD 或 ES6。
你是不是納悶,已經有像 Browserify、RequireJS 那樣的捆綁工具各司其職、表現優異,為什麼還需要 Webpack。好吧,至少因為一點,Webpack 額外提供了一些有用的特性,如“代碼分割”——把你的程式碼程式庫分割成按需載入的“塊”。
比如,你有一個網頁應用程式,其中相當一塊代碼只在特定條件下才用到,那麼把整個程式碼程式庫都捆綁進一個大檔案就不是很高效。這時,你可以用代碼分割功能,將那部分代碼抽離、捆綁到另外一塊,在執行時按需載入,從而避免在最開始就遇到大量負載的麻煩。其實大部分使用者只會用到你應用程式的核心代碼。
代碼分割僅僅是 Webpack 提供的眾多亮眼功能之一。關於 Webpack 與 Browserify 哪個更好,網路上充斥著各種觀點,下面的是一些客觀冷靜的討論,助我稍微理清了頭緒: https://gist.github.com/substack/68f8d502be42d5cd4942 http://mattdesl.svbtle.com/browserify-vs-webpack http://blog.namangoel.com/browserify-vs-webpack-js-drama 7. ES6 模組
看完了嗎。好,接下來我想談談 ES6 模組,它在未來會減少捆綁工具的使用。(你很快就會明白我的意思。)先來弄懂 ES6 模組是怎麼被載入的。
ES6 模組同現有的 JS 模組格式最關鍵的區別是,它在設計之初就考慮到了靜態分析。什麼意思呢。當你匯入模組時,匯入的模組會在編譯階段,即代碼開始運行之前,被解析。於是,我們能夠在運行程式之前,把匯出模組中不被其他模組使用的部分移除。這節省了很大的空間,減少了瀏覽器的壓力。
那麼問題來了。你在用一些工具,像 Uglify.js,縮減代碼時,有一個無作用程式碼去除的處理,它和 ES6 移除沒用的模組又有什麼不同呢。只能說“得看情況”。
(註:無作用程式碼去除是可選步驟,即去除未使用的代碼和變數。就把它想成,扔掉捆綁後代碼中不需要啟動並執行冗餘部分,一定在捆綁之後。)
有時,無作用程式碼去除在 Uglify.js 和 ES6 模組中表現完全一樣,而有時也不同。如果你想驗證一下, Rollup’s wiki 裡有個很好的樣本。
ES6 的不同在於,它去除無作用程式碼的方法不同,叫做“tree shaking”,本質上它是無作用程式碼去除的反過程。它只保留你運行所需的代碼,而非排除你運行所不需要的。來看個例子:
我們有一個 util.js 檔案,裡面寫了一些函數,再用 ES6 文法將它們一一匯出(exports):
export function each(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); } }}export function filter(collection, test) { var filtered = []; each(collection, function (item) { if (test(item)) { filtered.push(item); } }); return filtered;}export function map(collection, iterator) { var mapped = []; each(collection, function (value, key, collection) { mapped.push(iterator(value)); }); return mapped;}export function reduce(collection, iterator, accumulator) { var startingValueMissing = accumulator === undefined; each(collection, function (item) { if (startingValueMissing) { accumulator = item; startingValueMissing = false; } else { accumulator = iterator(accumulator, item); } }); return accumulator;}
然後,假設我們不知道自己的程式將要用到哪個功能函數,所以把這些模組全都匯入(import)到 main.js:
import * as Utils from ‘./utils.js’;
但到最後我們只用了 each 函數:
import * as Utils from ‘./utils.js’;Utils.each([1, 2, 3], function (x) { console.log(x) });
經過“tree shaking”之後,把相應的模組載入進來,新的 main.js 會是這個樣子:
function each(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); } }};each([1, 2, 3], function (x) { console.log(x) });
注意到了嗎,匯出模組中只有我們用到的 each 被引入進來了。
另外,如果我們突然決定用 filter 函數而非 each ,結果就會是這樣:
import * as Utils from ‘./utils.js’;Utils.filter([1, 2, 3], function (x) { return x === 2 });