了解 webpack 整個基礎工作流程,有助于我們解決日常使用 webpack 時遇到的一些問題,也有助于我們更好地理解 webpack loader 和 plugin 的使用。
拋開復雜的 loader 和 plugin 機制,webpack 本質上就是一個 JS Module Bundler,用于將多個代碼模塊進行打包,所以我們先撇開 webpack 錯綜復雜的整體實現,來看一下一個相對簡單的 JS Module Bunlder 的基礎工作流程是怎么樣的,在了解了 bundler 如何工作的基礎上,再進一步去整理 webpack 整個流程,將 loader 和 plugin 的機制弄明白。
> 以下內容將 module bundler 簡稱為 bundler。
## bundler 的基礎流程
首先,bundler 從一個構建入口出發,解析代碼,分析出代碼模塊依賴關系,然后將依賴的代碼模塊組合在一起,在 JavaScript bundler 中,還需要提供一些膠水代碼讓多個代碼模塊可以協同工作,相互引用。下邊會舉一些簡單的例子來說明一下這幾個關鍵的部分是怎么工作的。
首先是解析代碼,分析依賴關系,對于 [ES6 Module](http://es6.ruanyifeng.com/#docs/module) 以及 [CommonJS Modules](http://www.commonjs.org/specs/modules/1.0/) 語法定義的模塊,例如這樣的代碼:
```
// entry.js
import { bar } from './bar.js'; // 依賴 ./bar.js 模塊
// bar.js
const foo = require('./foo.js'); // 依賴 ./foo.js 模塊
```
bundler 需要從這個入口代碼(第一段)中解析出依賴 bar.js,然后再讀取 bar.js 這個代碼文件,解析出依賴 foo.js 代碼文件,繼續解析其依賴,遞歸下去,直至沒有更多的依賴模塊,最終形成一顆模塊依賴樹。
> 至于如何從 JavaScript 代碼中解析出這些依賴,作者寫過一篇文章,可以參考下:[使用 Acorn 來解析 JavaScript](https://juejin.im/post/582425402e958a129926fcb4)。
如果 foo.js 文件沒有依賴其他的模塊的話,那么這個簡單例子的依賴樹也就相對簡單:`entry.js -> bar.js -> foo.js`,當然,日常開發中遇見的一般都是相當復雜的代碼模塊依賴關系。
分析出依賴關系后,bunlder 需要將依賴關系中涉及的所有文件組合到一起,但由于依賴代碼的執行是有先后順序以及會引用模塊內部不同的內容,不能簡單地將代碼拼接到一起。webpack 會利用 JavaScript Function 的特性提供一些代碼來將各個模塊整合到一起,即是將每一個模塊包裝成一個 JS Function,提供一個引用依賴模塊的方法,如下面例子中的 `__webpack__require__`,這樣做,既可以避免變量相互干擾,又能夠有效控制執行順序,簡單的代碼例子如下:
```
// 分別將各個依賴模塊的代碼用 modules 的方式組織起來打包成一個文件
// entry.js
modules['./entry.js'] = function() {
const { bar } = __webpack__require__('./bar.js')
}
// bar.js
modules['./bar.js'] = function() {
const foo = __webpack__require__('./foo.js')
};
// foo.js
modules['./foo.js'] = function() {
// ...
}
// 已經執行的代碼模塊結果會保存在這里
const installedModules = {}
function __webpack__require__(id) {
// ...
// 如果 installedModules 中有就直接獲取
// 沒有的話從 modules 中獲取 function 然后執行,將結果緩存在 installedModules 中然后返回結果
}
```
這只是 webpack 的實現方式的簡單例子,[rollup](https://rollupjs.org/guide/en) 有另外的實現方式,并且筆者個人覺得 rollup 的實現方式比 webpack 要更加優秀一些,rollup 可以讓你構建出來的代碼量更少一點,有興趣的同學可以看看這個文章:[Webpack and Rollup: the same but different](https://medium.com/webpack/webpack-and-rollup-the-same-but-different-a41ad427058c),也可以使用 [rollup](https://rollupjs.org/guide/en) 來構建一個簡單的例子,看看結果是什么樣子的。
我們在介紹 bundler 的基礎流程時,把各個部分的實現細節簡化了,這有利于我們從整體的角度去看清楚整個輪廓,至于某一部分的具體實現,例如解析代碼依賴,模塊依賴關系管理,膠水代碼的生成等,深入細節的話會比較復雜,這里不再作相關的展開。
## webpack 的結構
webpack 需要強大的擴展性,尤其是插件實現這一塊,webpack 利用了 [tapable](https://github.com/webpack/tapable) 這個庫(其實也是 webpack 作者開發的庫)來協助實現對于整個構建流程各個步驟的控制。
關于這個庫更多的使用內容可以去查看官方的文檔:[tapable](https://github.com/webpack/tapable),使用上并不算十分復雜,最主要的功能就是用來添加各種各樣的鉤子方法(即 Hook)。
webpack 基于 tapable 定義了主要構建流程后,使用 tapable 這個庫添加了各種各樣的鉤子方法來將 webpack 擴展至功能十分豐富,同時對外提供了相對強大的擴展性,即 plugin 的機制。
在這個基礎上,我們來了解一下 webpack 工作的主要流程和其中幾個重要的概念。
* Compiler,webpack 的運行入口,實例化時定義 webpack 構建主要流程,同時創建構建時使用的核心對象 compilation
* Compilation,由 Compiler 實例化,存儲構建過程中各流程使用到的數據,用于控制這些數據的變化
* Chunk,即用于表示 chunk 的類,對于構建時需要的 chunk 對象由 Compilation 創建后保存管理
* Module,用于表示代碼模塊的類,衍生出很多子類用于處理不同的情況,關于代碼模塊的所有信息都會存在 Module 實例中,例如 `dependencies` 記錄代碼模塊的依賴等
* Parser,其中相對復雜的一個部分,基于 [acorn](https://github.com/acornjs/acorn) 來分析 AST 語法樹,解析出代碼模塊的依賴
* Dependency,解析時用于保存代碼模塊對應的依賴使用的對象
* Template,生成最終代碼要使用到的代碼模板,像上述提到的膠水代碼就是用對應的 Template 來生成
> 官方對于 Compiler 和 Compilation 的定義是:
>
> **compiler** 對象代表了完整的 webpack 環境配置。這個對象在啟動 webpack 時被一次性建立,并配置好所有可操作的設置,包括 options,loader 和 plugin。當在 webpack 環境中應用一個插件時,插件將收到此 compiler 對象的引用。可以使用它來訪問 webpack 的主環境。
>
> **compilation** 對象代表了一次資源版本構建。當運行 webpack 開發環境中間件時,每當檢測到一個文件變化,就會創建一個新的 compilation,從而生成一組新的編譯資源。一個 compilation 對象表現了當前的模塊資源、編譯生成資源、變化的文件、以及被跟蹤依賴的狀態信息。compilation 對象也提供了很多關鍵步驟的回調,以供插件做自定義處理時選擇使用。
上述是 webpack 源碼實現中比較重要的幾個部分,webpack 運行的大概工作流程是這樣的:
```
創建 Compiler ->
調用 compiler.run 開始構建 ->
創建 Compilation ->
基于配置開始創建 Chunk ->
使用 Parser 從 Chunk 開始解析依賴 ->
使用 Module 和 Dependency 管理代碼模塊相互關系 ->
使用 Template 基于 Compilation 的數據生成結果代碼
```
上述只是筆者理解中的大概流程,細節相對復雜,一方面是技術實現的細節有一定復雜度,另一方面是實現的功能邏輯上也有一定復雜度,深入介紹的話,篇幅會很長,并且可能效果不理想,當我們還沒到了要去實現具體功能的時候,無須關注那么具體的實現細節,只需要站在更高的層面去分析整體的流程。
有興趣探究某一部分實現細節的同學,可以查閱 webpack 源碼,從 webpack 基礎流程入手:[Compiler Hooks](https://github.com/webpack/webpack/blob/master/lib/Compiler.js#L29)。
> 這里提供的是 4.x 版本的源碼 master 分支的鏈接地址,webpack 的源碼相對難懂,如果是想要學習 bundler 的整個工作流程,可以考慮看閱讀 [rollup](https://github.com/rollup/rollup) 的源碼,可讀性相對會好很多。
## 從源碼中探索 webpack
webpack 主要的構建處理方法都在 `Compilation` 中,我們要了解 loader 和 plugin 的機制,就要深入 `Compilation` 這一部分的內容。
Compilation 的實現也是比較復雜的,`lib/Compilation.js` 單個文件代碼就有近 2000 行之多,我們挑關鍵的幾個部分來介紹一下。
### addEntry 和 \_addModuleChain
`addEntry` 這個方法顧名思義,用于把配置的入口加入到構建的任務中去,當解析好 webpack 配置,準備好開始構建時,便會執行 `addEntry` 方法,而 `addEntry` 會調用 `_addModuleChain` 來為入口文件(入口文件這個時候等同于第一個依賴)創建一個對應的 `Module` 實例。
`_addModuleChain` 方法會根據入口文件這第一個依賴的類型創建一個 `moduleFactory`,然后再使用這個 `moduleFactory` 給入口文件創建一個 `Module` 實例,這個 `Module` 實例用來管理后續這個入口構建的相關數據信息,關于 `Module` 類的具體實現可以參考這個源碼:[lib/Module.js](https://github.com/webpack/webpack/blob/master/lib/Module.js),這個是個基礎類,大部分我們構建時使用的代碼模塊的 `Module` 實例是 [lib/NormalModule.js](https://github.com/webpack/webpack/blob/master/lib/NormalModule.js) 這個類創建的。
我們介紹 `addEntry` 主要是為了尋找整個構建的起點,讓這一切有跡可循,后續的深入可以從這個點出發。
### buildModule
當一個 `Module` 實例被創建后,比較重要的一步是執行 `compilation.buildModule` 這個方法,這個方法主要會調用 `Module` 實例的 `build` 方法,這個方法主要就是創建 `Module` 實例需要的一些東西,對我們梳理流程來說,這里邊最重要的部分就是調用自身的 [runLoaders](https://github.com/webpack/webpack/blob/master/lib/NormalModule.js#L218) 方法。
`runLoaders` 這個方法是 webpack 依賴的這個類庫實現的:[loader-runner](https://github.com/webpack/loader-runner),這個方法也比較容易理解,就是執行對應的 loaders,將代碼源碼內容一一交由配置中指定的 loader 處理后,再把處理的結果保存起來。
我們之前介紹過,webpack 的 loader 就是轉換器,loader 就是在這個時候發揮作用的,至于 loader 執行的細節,有興趣深入的同學可以去了解 [loader-runner](https://github.com/webpack/loader-runner) 的實現。
上述提到的 `Module` 實例的 `build` 方法在執行完對應的 loader,處理完模塊代碼自身的轉換后,還有相當重要的一步是調用 [Parser](https://github.com/webpack/webpack/blob/master/lib/Parser.js) 的實例來解析自身依賴的模塊,解析后的結果存放在 `module.dependencies` 中,首先保存的是依賴的路徑,后續會經由 `compilation.processModuleDependencies` 方法,再來處理各個依賴模塊,遞歸地去建立整個依賴關系樹。
### Compilation 的鉤子
我們前邊提到了 webpack 會使用 [tapable](https://github.com/webpack/tapable) 給整個構建流程中的各個步驟定義鉤子,用于注冊事件,然后在特定的步驟執行時觸發相應的事件,注冊的事件函數便可以調整構建時的上下文數據,或者做額外的處理工作,這就是 webpack 的 plugin 機制。
在 webpack 執行入口處 [lib/webpack.js](https://github.com/webpack/webpack/blob/master/lib/webpack.js#L35) 有這么一段代碼:
```
if (options.plugins && Array.isArray(options.plugins)) {
for (const plugin of options.plugins) {
plugin.apply(compiler); // 調用每一個 plugin 的 apply 方法,把 compiler 實例傳遞過去
}
}
```
這個 plugin 的 `apply` 方法就是用來給 `compiler` 實例注冊事件鉤子函數的,而 `compiler` 的一些事件鉤子中可以獲得 `compilation` 實例的引用,通過引用又可以給 `compilation` 實例注冊事件函數,以此類推,便可以將 plugin 的能力覆蓋到整個 webpack 構建過程。
而關于這些事件函數的名稱和定義可以查看官方的文檔:[compiler 的事件鉤子](https://doc.webpack-china.org/api/compiler/#%E4%BA%8B%E4%BB%B6%E9%92%A9%E5%AD%90) 和 [compilation 的事件鉤子](https://doc.webpack-china.org/api/compilation/)。
后續的 15 小節會介紹如何編寫 webpack plugin,可以將兩部分的內容結合一下,來幫助理解 webpack plugin 的執行機制。
### 產出構建結果
最后還有一個部分,即用 `Template` 產出最終構建結果的代碼內容,這一部分不作詳細介紹了,僅留下一些線索,供有興趣繼續深入的同學使用:
* `Template` 基礎類:[lib/Template.js](https://github.com/webpack/webpack/blob/master/lib/Template.js)
* 常用的主要 `Template` 類:[lib/MainTemplate.js](https://github.com/webpack/webpack/blob/master/lib/MainTemplate.js)
* Compilation 中產出構建結果的代碼:[compilation.createChunkAssets](https://github.com/webpack/webpack/blob/master/lib/Compilation.js#L1722)
這一部分內容的介紹就到這里了,對此部分內容有興趣繼續深入探索的同學,建議使用斷點調試的方式,結合筆者介紹的這些內容,大致走一遍 webpack 的構建流程,會對這一部分的內容印象更加深刻,同時也可以通過斷點更有針對性地了解某一部分的細節處理。
## 小結
本小節介紹了一個 bundler 的基礎流程應該是怎么樣的,以及 webpack 在 bundler 的基礎上如何去增強自己的擴展性,同時我們介紹了 webpack 主要構建流程中比較重要的幾個概念,并且從 webpack 這些概念的關鍵部分的源碼來探索 webpack 的主要執行流程,希望這些內容可以幫助你更好地理解 webpack。