寫在前面的話
在閱讀 webpack4.x
原始碼的過程中,參考了《深入淺出webpack》一書和眾多大神的文章,結合自己的一點體會,總結如下。
總述
webpack
就像一條生產線,要經過一系列處理流程後才能將原始檔轉換成輸出結果。 這條生產線上的每個處理流程的職責都是單一的,多個流程之間有存在依賴關係,只有完成當前處理後才能交給下一個流程去處理。 外掛就像是一個插入到生產線中的一個功能,在特定的時機對生產線上的資源做處理。
webpack
通過 Tapable
來組織這條複雜的生產線。 webpack
在執行過程中會廣播事件,外掛只需要監聽它所關心的事件,就能加入到這條生產線中,去改變生產線的運作。 webpack
的事件流機制保證了外掛的有序性,使得整個系統擴充套件性很好。 --吳浩麟《深入淺出webpack》
核心的概念
entry
,loader
,plugin
,module
,chunk
不論文件還是相關的介紹都很多了,不贅述,有疑問的移步文件。
構建流程
webpack
的執行流程是一個序列的過程,從啟動到結束會依次執行以下流程:
- 初始化引數:從配置檔案和
Shell
語句中讀取與合併引數,得出最終的引數; - 開始編譯:用上一步得到的引數初始化
Compiler
物件,載入所有配置的外掛,執行物件的run
方法開始執行編譯; - 確定入口:根據配置中的 entry 找出所有的入口檔案
- 編譯模組:從入口檔案出發,呼叫所有配置的 Loader 對模組進行翻譯,再找出該模組依賴的模組,再遞迴本步驟直到所有入口依賴的檔案都經過了本步驟的處理;
- 完成模組編譯:在經過第4步使用 Loader 翻譯完所有模組後,得到了每個模組被翻譯後的最終內容以及它們之間的依賴關係;
- 輸出資源:根據入口和模組之間的依賴關係,組裝成一個個包含多個模組的
Chunk
,再把每個Chunk
轉換成一個單獨的檔案加入到輸出列表,這步是可以修改輸出內容的最後機會; - 輸出完成:在確定好輸出內容後,根據配置確定輸出的路徑和檔名,把檔案內容寫入到檔案系統。
在以上過程中,
webpack
會在特定的時間點廣播出特定的事件,外掛在監聽到感興趣的事件後會執行特定的邏輯,並且外掛可以呼叫webpack
提供的 API 改變webpack
的執行結果。
webpack 中比較核心的兩個物件
Compile
物件:負責檔案監聽和啟動編譯。Compiler
例項中包含了完整的webpack
配置,全域性只有一個Compiler
例項。compilation
物件:當webpack
以開發模式執行時,每當檢測到檔案變化,一次新的Compilation
將被建立。一個Compilation
物件包含了當前的模組資源、編譯生成資源、變化的檔案等。Compilation
物件也提供了很多事件回撥供外掛做擴充套件。- 這兩個物件都繼承自 Tapable。以
Compile
為例
const {
Tapable,
SyncHook,
SyncBailHook,
AsyncParallelHook,
AsyncSeriesHook
} = require("tapable");
class Compiler extends Tapable {
constructor(context) {
super();
this.hooks = {
/** @type {SyncBailHook<Compilation>} */
//所有需要輸出的檔案已經生成好,詢問外掛哪些檔案需要輸出,哪些不需要。
shouldEmit: new SyncBailHook(["compilation"]),
/** @type {AsyncSeriesHook<Stats>} */
//成功完成一次完成的編譯和輸出流程。
done: new AsyncSeriesHook(["stats"]),
/** @type {AsyncSeriesHook<>} */
additionalPass: new AsyncSeriesHook([]),
/** @type {AsyncSeriesHook<Compiler>} */
beforeRun: new AsyncSeriesHook(["compiler"]),
/** @type {AsyncSeriesHook<Compiler>} */
//啟動一次新的編譯
run: new AsyncSeriesHook(["compiler"]),
/** @type {AsyncSeriesHook<Compilation>} */
// 確定好要輸出哪些檔案後,執行檔案輸出,可以在這裡獲取和修改輸出內容。
emit: new AsyncSeriesHook(["compilation"]),
/** @type {AsyncSeriesHook<Compilation>} */
// 輸出完畢
afterEmit: new AsyncSeriesHook(["compilation"]),
// 以上幾個事件(除了run,beforerun為編譯階段)其餘為輸出階段的事件
/** @type {SyncHook<Compilation, CompilationParams>} */
// compilation 建立之前掛載外掛的過程
thisCompilation: new SyncHook(["compilation", "params"]),
/** @type {SyncHook<Compilation, CompilationParams>} */
// 建立compilation物件
compilation: new SyncHook(["compilation", "params"]),
/** @type {SyncHook<NormalModuleFactory>} */
// 初始化階段:初始化compilation引數
normalModuleFactory: new SyncHook(["normalModuleFactory"]),
/** @type {SyncHook<ContextModuleFactory>} */
// 初始化階段:初始化compilation引數
contextModuleFactory: new SyncHook(["contextModulefactory"]),
/** @type {AsyncSeriesHook<CompilationParams>} */
beforeCompile: new AsyncSeriesHook(["params"]),
/** @type {SyncHook<CompilationParams>} */
// 該事件是為了告訴外掛一次新的編譯將要啟動,同時會給外掛帶上 compiler 物件
compile: new SyncHook(["params"]),
/** @type {AsyncParallelHook<Compilation>} */
//一個新的 Compilation 建立完畢,即將從 Entry 開始讀取檔案,根據檔案型別和配置的 Loader 對檔案進行編譯,編譯完後再找出該檔案依賴的檔案,遞迴的編譯和解析。
make: new AsyncParallelHook(["compilation"]),
/** @type {AsyncSeriesHook<Compilation>} */
// 一次Compilation執行完成
afterCompile: new AsyncSeriesHook(["compilation"]),
/** @type {AsyncSeriesHook<Compiler>} */
//監聽模式下啟動編譯(常用於開發階段)
watchRun: new AsyncSeriesHook(["compiler"]),
/** @type {SyncHook<Error>} */
failed: new SyncHook(["error"]),
/** @type {SyncHook<string, string>} */
invalid: new SyncHook(["filename", "changeTime"]),
/** @type {SyncHook} */
// 如名字所述
watchClose: new SyncHook([]),
// TODO the following hooks are weirdly located here
// TODO move them for webpack 5
/** @type {SyncHook} */
//初始化階段:開始應用 Node.js 風格的檔案系統到compiler 物件,以方便後續的檔案尋找和讀取。
environment: new SyncHook([]),
/** @type {SyncHook} */
// 參照上文
afterEnvironment: new SyncHook([]),
/** @type {SyncHook<Compiler>} */
// 呼叫完內建外掛以及配置引入外掛的apply方法,完成了事件訂閱
afterPlugins: new SyncHook(["compiler"]),
/** @type {SyncHook<Compiler>} */
afterResolvers: new SyncHook(["compiler"]),
/** @type {SyncBailHook<string, EntryOptions>} */
// 讀取配置的 Entrys,為每個 Entry 例項化一個對應的 EntryPlugin,為後面該 Entry 的遞迴解析工作做準備。
entryOption: new SyncBailHook(["context", "entry"])
};
複製程式碼
在 webpack
執行的過程中,會按順序廣播一系列事件--this.hooks
中的一系列事件(類似於我們常用框架中的生命週期),而這些事件的訂閱者該按照怎樣的順序來組織,來執行,來進行引數傳遞... 這就是 Tapable
要做的事情。
關於 Tapable
給大家推薦一篇比較好(但是閱讀量點贊評論都不多2333)的科普文
流程細節
流程細節參照我在引用的Compile
物件中的註釋,有一點需要注意,作者hooks
的書寫順序並不是呼叫順序。
有些沒註釋的有幾種情況:
- 不那麼重要,或參照事件名稱和上下文可知
- 主要是暫時還不知道(2333,後面有新的理解再補充,逃...)
- 當然最重要的事件基本涵蓋到了 這裡補充一個大從參考文章裡面找來的圖
compilation 過程簡介
compilation
實際上就是呼叫相應的 loader
處理檔案生成 chunks
並對這些 chunks
做優化的過程。幾個關鍵的事件(Compilation物件this.hooks中):
buildModule
使用對應的Loader
去轉換一個模組;normalModuleLoader
在用Loader
對一個模組轉換完後,使用acorn
解析轉換後的內容,輸出對應的抽象語法樹(AST),以方便webpack
後面對程式碼的分析。seal
所有模組及其依賴的模組都通過Loader
轉換完成後,根據依賴關係開始生成Chunk
。
最後從參考文章中摘了一張圖片以便於對整個過程有更清晰的認知
參考
- taobaofed.org/blog/2016/0…
- imweb.io/topic/5baca…
- 《深入淺出webpack》
廣而告之
本文釋出於薄荷前端週刊,歡迎Watch & Star ★,轉載請註明出處。