Webpack 核心流程

袋鼠云数栈前端發表於2024-08-22

我們是袋鼠雲數棧 UED 團隊,致力於打造優秀的一站式資料中臺產品。我們始終保持工匠精神,探索前端道路,為社群積累並傳播經驗價值。

本文作者:霜序

三個階段

初始化階段

  1. 初始化引數:從配置檔案、配置物件、shell 引數中讀取,與預設的配置引數結合得出最後的引數。
  2. 建立編譯器物件:透過上一步得到的引數建立 Compiler 物件。
  3. 初始化編譯器環境:注入內建外掛、各種模組工廠、載入配置等。
  4. 開始編譯:執行 compiler 物件的 run 方法。
  5. 確定入口:根據配置中的 entry 找到對應的入口檔案,使用compilition.addEntry將入口檔案轉換為 dependence 物件。

構建階段

  1. 編譯模組(make):根據 entry 對應的 dependence 建立 module 物件,呼叫 loader 將模組轉移成為標準的 JS 內容,在將其轉成 AST 物件,從中找出該模組依賴的模組,再遞迴至所有的入口檔案都經歷了該步驟。
  2. 完成模組編譯:上一步處理完成之後,得到每一個模組被轉譯之後的內容以及對應的依賴關係圖。

生成階段

  1. 輸出資源(seal):根據入口檔案和模組之間的依賴關係,組裝成一個個包含多個模組的 chunk,再把每一個 chunk 轉換成為單獨的一個檔案放到輸出列表,這是最後一次可以修改輸出內容的機會。
  2. 寫入檔案系統(emitAssets):確定好了輸出內容,根據輸出路徑和檔名,把檔案寫入到檔案系統。

初始化階段

file

new webpack(config, callback)

webpack 支援兩個引數,config 是 webpack.config.js 中的配置,callback 是回撥函式。
webpack 引用於 webpack/lib/webpack.js
file
上圖是 webpack() 的流程圖,定義了 create 函式
create 函式主要完成

  1. 定義相關的引數
  2. 透過 createCompiler 建立 compiler 物件
  3. 返回 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

file
在上一步中,呼叫了 create 方法,compiler 物件實則是透過 createCompiler 函式返回的。
主要邏輯都是在 WebpackOptionsApply.process 中,該方法是將 config 中配置的屬性轉成 plugin 注入到 webpack 中。
透過 Compiler 類建立了 compiler 物件,透過 constructor 初始化一些內容

  • 使用 tapable 初始化一系列的 hooks。
  • 初始化一些引數。

compiler.run

在第一步的時候,呼叫 webpack 之後,最後都會呼叫 compiler.run 方法。
從程式碼中可以看出來,compiler.run 方法主要做了:

  1. 定義錯誤處理函式 finalCallback
  2. 定義 onCompiled 作為 this.compile 的回撥
  3. 定義 run 方法,執行 run 方法

簡單來說,compiler.run 其實最後呼叫的是 compiler.compile 方法

compiler.compile

compiler.compile 該方法中才開始做 make 處理
從程式碼中可以看出來,compiler.compile 方法主要做了:

  1. 初始化 compilation 引數,呼叫new Compilation建立 compilation 物件
  2. 執行 make hook,呼叫compilation.addEntry方法,進入構建階段
  3. compilation.seal,執行 seal,對 make 階段處理過的 module 程式碼進行封裝, chunk 輸出最終產物
  4. afterCompile hook,執行收尾邏輯

呼叫compile函式觸發make鉤子後,初始化階段就算是結束了,流程邏輯開始進入「構建階段

構建階段

構建階段主要使用的 compilation 物件,它和 compiler 是有區別的:

compiler:webpack 剛構建時就會建立 compiler 物件,存在於 webpack 整個生命週期 。
compilation:在準備編譯某一個模組的時候才會建立,主要存在於 compile 到 make 這一段生命週期裡面。
開啟 wacth 對檔案進行監聽時,檔案發生改變需要重新編譯時,只需要重新建立一個 compilation 物件即可,不需要重新建立 compiler 物件做很多第一步初始化操作。如果改變了 config,則需要重新執行 dev/build 命令,建立新的 compiler 物件。

file

  1. 當執行初始化階段的時候WebpackOptionsApply.process的時候會去初始化 EntryPlugin 呼叫compiler.hooks.make.tapAsync註冊 compiler 的 make 鉤子,用來開啟編譯。
  2. 當初始化完成之後呼叫compiler.compile方法時,會執行this.hooks.make.callAsync,從而開始執行compilation.addEntry新增入口檔案。
  3. 呼叫handleModuleCreation方法,根據檔案型別建立不同的 module。
  4. 呼叫module.build開始構建,透過 loader-runner 轉譯 module 內容,將各種資源轉為 webpack 可以理解的 JavaScript 文字。
  5. 呼叫 acorn 的parse方法將 JS 程式碼解析成為 AST 結構。
  6. 透過 JavaScriptParser 類中遍歷 AST,觸發各種 hooks 。
    • 遇到 import 語句時,觸發hooks.exportImportSpecifier
    • 該 hook 在 HarmonyExportDependencyParserPlugin 外掛中被註冊,會將依賴資源新增成為 Dependency 物件。
    • 呼叫module.addDependency將依賴物件加入到 module 依賴列表中。
  7. AST 遍歷完畢後,呼叫module.handleParseResult處理模組依賴。
  8. 對於 module 新增的依賴,呼叫handleModuleCreate,控制流回到第一步。
  9. 所有依賴都解析完畢後,構建階段結束。

在整個過程中資料流 module ⇒ AST ⇒ dependency ⇒ module 的轉變,將原始碼轉為 AST 主要是為了分析模組的 import 語句收集相關依賴陣列,最後遍歷 dependences 陣列將 Dependency 轉換為 Module 物件,之後遞迴處理這些新的 Module,直到所有專案檔案處理完畢。

總結來說就是,從入口檔案開始收集其依賴模組,並對依賴模組再進行相同的模組處理。

構建過程
file

例如上圖,entry 檔案為 index.js,分別依賴 a.js/b.js,其中 a.js 又依賴 c.js/d.js 。
第一步
根據 webpack 初始化之後,能夠確定入口檔案 index.js,並呼叫compilation.addEntry函式將之新增為 Module 物件。
file

第二步
透過 acorn 解析 index 檔案,分析 AST 得到 index 有兩個依賴。
file

第三步
得到了兩個 dependence 之後,呼叫 module[index] 的 handleParserResult 方法處理 a/b 兩個依賴物件。
file

第四步
又觸發 module[a/b] 的 handleModuleCreation 方法,從 a 模組中又解析到 c/d 兩個新依賴,於是再繼續呼叫 module[a] 的 handleParseResult,遞迴上述流程。
file

第五步
最終得到 a/b/c/d 四個 Module 以及其對應的 dependence。
file
所有的模組構建完畢,沒有新的依賴可以繼續,由此進入生成階段。

生成階段

在構建階段 make 結束之後,就會進入生成階段,呼叫compilation.seal表明正式進入生成階段。
在 seal 階段主要是是將構建階段生成的 module 拆分組合到 chunk 物件中,再轉譯成為目標環境的產物,並寫出為產物檔案,解決的是資源輸出問題。
file

  1. 構建本次編譯的ChunkGraph物件
  2. 透過hooks.optimizeDependencies最佳化模組依賴關係
  3. 迴圈compilation.entries入口檔案建立 chunks,呼叫 addChunk 為每一個入口新增 chunk 物件,並且遍歷當前入口的 dependency 物件找到對應 module 物件關聯到該 chunk
  4. 觸發optimizeModules/optimizeChunks等鉤子,對 chunk 和 module 進行一系列的最佳化操作,這些最佳化操作都是有外掛去完成的,例如 SplitChunksPlugin
  5. 呼叫codeGeneration方法生成 chunk 程式碼,會根據不同的 module 型別生成 template 程式碼
  6. 呼叫createChunkAssets方法為每一個 chunk 生成資產檔案
  7. compilation.emitAsset 將產物提交到 compilation.assets 中,還尚未寫入磁碟
  8. 最後執行 callback 回撥回到 compile 的控制流中,執行 onCompiled 方法中的 compiler.emitAsset 輸出資產檔案

流轉過程

在 webpack 執行的三個階段,對應著資源形態扭轉,每一個階段操作的物件都是不一樣的
file

  • 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

相關文章