我們是袋鼠雲數棧 UED 團隊,致力於打造優秀的一站式資料中臺產品。我們始終保持工匠精神,探索前端道路,為社群積累並傳播經驗價值。
本文作者:霜序
三個階段
初始化階段
- 初始化引數:從配置檔案、配置物件、shell 引數中讀取,與預設的配置引數結合得出最後的引數。
- 建立編譯器物件:透過上一步得到的引數建立 Compiler 物件。
- 初始化編譯器環境:注入內建外掛、各種模組工廠、載入配置等。
- 開始編譯:執行 compiler 物件的 run 方法。
- 確定入口:根據配置中的 entry 找到對應的入口檔案,使用
compilition.addEntry
將入口檔案轉換為 dependence 物件。
構建階段
- 編譯模組(make):根據 entry 對應的 dependence 建立 module 物件,呼叫 loader 將模組轉移成為標準的 JS 內容,在將其轉成 AST 物件,從中找出該模組依賴的模組,再遞迴至所有的入口檔案都經歷了該步驟。
- 完成模組編譯:上一步處理完成之後,得到每一個模組被轉譯之後的內容以及對應的依賴關係圖。
生成階段
- 輸出資源(seal):根據入口檔案和模組之間的依賴關係,組裝成一個個包含多個模組的 chunk,再把每一個 chunk 轉換成為單獨的一個檔案放到輸出列表,這是最後一次可以修改輸出內容的機會。
- 寫入檔案系統(emitAssets):確定好了輸出內容,根據輸出路徑和檔名,把檔案寫入到檔案系統。
初始化階段
new webpack(config, callback)
webpack 支援兩個引數,config 是 webpack.config.js 中的配置,callback 是回撥函式。
webpack 引用於 webpack/lib/webpack.js
上圖是 webpack() 的流程圖,定義了 create 函式
create 函式主要完成
- 定義相關的引數
- 透過 createCompiler 建立 compiler 物件
- 返回 compiler 和其他引數
並會根據 callback 回撥執行不同的操作:
- 如果傳入了 callback 引數,會透過 create 方法拿到對應的 compiler 物件,並執行 compiler.run 方法,返回 compiler 物件
- 在這其中會判斷是否配置 watch 引數,如果有會監聽檔案改變,重新編譯
- 如果沒有傳入 callback 引數,也會透過 create 方法拿到對應的 compiler 物件,直接返回
因此呼叫 webpack() 方法有兩種方式:
// webpack 函式有傳回撥函式
const compiler = webpack(config, (err, stats) => {
if (err) {
console.log(err)
}
})
// 執行 webpack 函式沒有傳回撥函式,手動呼叫一下 compiler.run
const compiler = webpack(config)
compiler.run((err, stats) => {
if (err) {
console.log(err)
}
})
createCompiler
在上一步中,呼叫了 create 方法,compiler 物件實則是透過 createCompiler 函式返回的。
主要邏輯都是在 WebpackOptionsApply.process 中,該方法是將 config 中配置的屬性轉成 plugin 注入到 webpack 中。
透過 Compiler 類建立了 compiler 物件,透過 constructor 初始化一些內容
- 使用 tapable 初始化一系列的 hooks。
- 初始化一些引數。
compiler.run
在第一步的時候,呼叫 webpack 之後,最後都會呼叫 compiler.run 方法。
從程式碼中可以看出來,compiler.run 方法主要做了:
- 定義錯誤處理函式 finalCallback
- 定義 onCompiled 作為 this.compile 的回撥
- 定義 run 方法,執行 run 方法
簡單來說,compiler.run 其實最後呼叫的是 compiler.compile 方法
compiler.compile
compiler.compile 該方法中才開始做 make 處理
從程式碼中可以看出來,compiler.compile 方法主要做了:
- 初始化 compilation 引數,呼叫
new Compilation
建立 compilation 物件 - 執行 make hook,呼叫
compilation.addEntry
方法,進入構建階段 - compilation.seal,執行 seal,對 make 階段處理過的 module 程式碼進行封裝, chunk 輸出最終產物
- afterCompile hook,執行收尾邏輯
呼叫compile
函式觸發make
鉤子後,初始化階段就算是結束了,流程邏輯開始進入「構建階段」
構建階段
構建階段主要使用的 compilation 物件,它和 compiler 是有區別的:
compiler:webpack 剛構建時就會建立 compiler 物件,存在於 webpack 整個生命週期 。
compilation:在準備編譯某一個模組的時候才會建立,主要存在於 compile 到 make 這一段生命週期裡面。
開啟 wacth 對檔案進行監聽時,檔案發生改變需要重新編譯時,只需要重新建立一個 compilation 物件即可,不需要重新建立 compiler 物件做很多第一步初始化操作。如果改變了 config,則需要重新執行 dev/build 命令,建立新的 compiler 物件。
- 當執行初始化階段的時候
WebpackOptionsApply.process
的時候會去初始化 EntryPlugin 呼叫compiler.hooks.make.tapAsync
註冊 compiler 的 make 鉤子,用來開啟編譯。 - 當初始化完成之後呼叫
compiler.compile
方法時,會執行this.hooks.make.callAsync
,從而開始執行compilation.addEntry
新增入口檔案。 - 呼叫
handleModuleCreation
方法,根據檔案型別建立不同的 module。 - 呼叫
module.build
開始構建,透過 loader-runner 轉譯 module 內容,將各種資源轉為 webpack 可以理解的 JavaScript 文字。 - 呼叫 acorn 的
parse
方法將 JS 程式碼解析成為 AST 結構。 - 透過 JavaScriptParser 類中遍歷 AST,觸發各種 hooks 。
- 遇到 import 語句時,觸發
hooks.exportImportSpecifier
。 - 該 hook 在 HarmonyExportDependencyParserPlugin 外掛中被註冊,會將依賴資源新增成為 Dependency 物件。
- 呼叫
module.addDependency
將依賴物件加入到 module 依賴列表中。
- 遇到 import 語句時,觸發
- AST 遍歷完畢後,呼叫
module.handleParseResult
處理模組依賴。 - 對於 module 新增的依賴,呼叫
handleModuleCreate
,控制流回到第一步。 - 所有依賴都解析完畢後,構建階段結束。
在整個過程中資料流 module ⇒ AST ⇒ dependency ⇒ module 的轉變,將原始碼轉為 AST 主要是為了分析模組的 import 語句收集相關依賴陣列,最後遍歷 dependences 陣列將 Dependency 轉換為 Module 物件,之後遞迴處理這些新的 Module,直到所有專案檔案處理完畢。
總結來說就是,從入口檔案開始收集其依賴模組,並對依賴模組再進行相同的模組處理。
構建過程
例如上圖,entry 檔案為 index.js,分別依賴 a.js/b.js,其中 a.js 又依賴 c.js/d.js 。
第一步
根據 webpack 初始化之後,能夠確定入口檔案 index.js,並呼叫compilation.addEntry
函式將之新增為 Module 物件。
第二步
透過 acorn 解析 index 檔案,分析 AST 得到 index 有兩個依賴。
第三步
得到了兩個 dependence 之後,呼叫 module[index] 的 handleParserResult 方法處理 a/b 兩個依賴物件。
第四步
又觸發 module[a/b] 的 handleModuleCreation 方法,從 a 模組中又解析到 c/d 兩個新依賴,於是再繼續呼叫 module[a] 的 handleParseResult,遞迴上述流程。
第五步
最終得到 a/b/c/d 四個 Module 以及其對應的 dependence。
所有的模組構建完畢,沒有新的依賴可以繼續,由此進入生成階段。
生成階段
在構建階段 make 結束之後,就會進入生成階段,呼叫compilation.seal
表明正式進入生成階段。
在 seal 階段主要是是將構建階段生成的 module 拆分組合到 chunk 物件中,再轉譯成為目標環境的產物,並寫出為產物檔案,解決的是資源輸出問題。
- 構建本次編譯的
ChunkGraph
物件 - 透過
hooks.optimizeDependencies
最佳化模組依賴關係 - 迴圈
compilation.entries
入口檔案建立 chunks,呼叫 addChunk 為每一個入口新增 chunk 物件,並且遍歷當前入口的 dependency 物件找到對應 module 物件關聯到該 chunk - 觸發
optimizeModules/optimizeChunks
等鉤子,對 chunk 和 module 進行一系列的最佳化操作,這些最佳化操作都是有外掛去完成的,例如 SplitChunksPlugin - 呼叫
codeGeneration
方法生成 chunk 程式碼,會根據不同的 module 型別生成 template 程式碼 - 呼叫
createChunkAssets
方法為每一個 chunk 生成資產檔案 - compilation.emitAsset 將產物提交到 compilation.assets 中,還尚未寫入磁碟
- 最後執行 callback 回撥回到 compile 的控制流中,執行 onCompiled 方法中的 compiler.emitAsset 輸出資產檔案
流轉過程
在 webpack 執行的三個階段,對應著資源形態扭轉,每一個階段操作的物件都是不一樣的
- compication.make
- 以 entry 檔案為入口,作為 dependency 放入 compilcation 的依賴列表
- 根據 dependences 建立 module 物件,之後讀入 module 對應的檔案內容,呼叫 loader-runner 對內容做轉化,轉化結果若有其它依賴則繼續讀入依賴資源,重複此過程直到所有依賴均被轉化為 module
- compication.seal
- 遍歷所有的 module,根據 entry 的配置以及 module 的型別,分配到不同的 chunk
- 將 chunk 構建成為 chunkGraph
- 遍歷 chunkGraph 呼叫 complication.emitAssets 方法標記 chunk 的輸出規則,即轉化為 assets 集合。
- compiler.emitAssets
- 將 assets 輸出到檔案系統
總結
- 初始化階段:負責構建環境,初始化工廠類,注入內建外掛
- 構建階段:讀入並分析 entry 檔案,查詢其模組依賴,再一次處理模組依賴的依賴,直到所有的依賴都被處理完畢,該過程解決資源輸入問題
- 生成階段:根據 entry 的配置將模組封裝稱為不同的 chunk,經過一系列的最佳化再將模組程式碼編譯成為最終的形態,按 chunk 合併成最後的產物,該過程解決資源輸出問題
最後
歡迎關注【袋鼠雲數棧UED團隊】~
袋鼠雲數棧 UED 團隊持續為廣大開發者分享技術成果,相繼參與開源了歡迎 star
- 大資料分散式任務排程系統——Taier
- 輕量級的 Web IDE UI 框架——Molecule
- 針對大資料領域的 SQL Parser 專案——dt-sql-parser
- 袋鼠雲數棧前端團隊程式碼評審工程實踐文件——code-review-practices
- 一個速度更快、配置更靈活、使用更簡單的模組打包器——ko
- 一個針對 antd 的元件測試工具庫——ant-design-testing