Webpack 原理系列八:產(chǎn)物轉(zhuǎn)譯打包邏輯
回顧一下,在之前的文章《有點(diǎn)難的 webpack 知識(shí)點(diǎn):Dependency Graph 深度解析》已經(jīng)聊到,經(jīng)過(guò) 「構(gòu)建(make)階段」 后,Webpack 解析出:
- module 內(nèi)容
- module 與 module 之間的依賴關(guān)系圖
而進(jìn)入 「生成(「「seal」」)階段」 后,Webpack 首先根據(jù)模塊的依賴關(guān)系、模塊特性、entry配置等計(jì)算出 Chunk Graph,確定最終產(chǎn)物的數(shù)量和內(nèi)容,這部分原理在前文《有點(diǎn)難的知識(shí)點(diǎn):Webpack Chunk 分包規(guī)則詳解》中也有較詳細(xì)的描述。
本文繼續(xù)聊聊 Chunk Graph 后面之后,模塊開(kāi)始轉(zhuǎn)譯到模塊合并打包的過(guò)程,大體流程如下:
為了方便理解,我將打包過(guò)程橫向切分為三個(gè)階段:
- 「入口」:指代從 Webpack 啟動(dòng)到調(diào)用 compilation.codeGeneration 之前的所有前置操作
- 「模塊轉(zhuǎn)譯」:遍歷 modules 數(shù)組,完成所有模塊的轉(zhuǎn)譯操作,并將結(jié)果存儲(chǔ)到 compilation.codeGenerationResults 對(duì)象
- 「模塊合并打包」:在特定上下文框架下,組合業(yè)務(wù)模塊、runtime 模塊,合并打包成 bundle ,并調(diào)用 compilation.emitAsset 輸出產(chǎn)物
這里說(shuō)的 「業(yè)務(wù)模塊」 是指開(kāi)發(fā)者所編寫的項(xiàng)目代碼;「runtime 模塊」 是指 Webpack 分析業(yè)務(wù)模塊后,動(dòng)態(tài)注入的用于支撐各項(xiàng)特性的運(yùn)行時(shí)代碼,在上一篇文章 Webpack 原理系列六:徹底理解 Webpack 運(yùn)行時(shí) 已經(jīng)有詳細(xì)講解,這里不贅述。
可以看到,Webpack 先將 modules 逐一轉(zhuǎn)譯為模塊產(chǎn)物 —— 「模塊轉(zhuǎn)譯」,再將模塊產(chǎn)物拼接成 bundle —— 「模塊合并打包」,我們下面會(huì)按照這個(gè)邏輯分開(kāi)討論這兩個(gè)過(guò)程的原理。
一、模塊轉(zhuǎn)譯原理
1.1 簡(jiǎn)介
先回顧一下 Webpack 產(chǎn)物:
上述示例由 index.js / name.js 兩個(gè)業(yè)務(wù)文件組成,對(duì)應(yīng)的 Webpack 配置如上圖左下角所示;Webpack 構(gòu)建產(chǎn)物如右邊 main.js 文件所示,包含三塊內(nèi)容,從上到下分別為:
- name.js 模塊對(duì)應(yīng)的轉(zhuǎn)譯產(chǎn)物,函數(shù)形態(tài)
- Webpack 按需注入的運(yùn)行時(shí)代碼
- index.js 模塊對(duì)應(yīng)的轉(zhuǎn)譯產(chǎn)物,IIFE(立即執(zhí)行函數(shù)) 形態(tài)
其中,運(yùn)行時(shí)代碼的作用與生成邏輯在上篇文章 Webpack 原理系列六:徹底理解 Webpack 運(yùn)行時(shí) 已有詳盡介紹;另外兩塊分別為 name.js 、index.js 構(gòu)建后的產(chǎn)物,可以看到產(chǎn)物與源碼語(yǔ)義、功能均相同,但表現(xiàn)形式發(fā)生了較大變化,例如 index.js 編譯前后的內(nèi)容:
上圖右邊是 Webpack 編譯產(chǎn)物中對(duì)應(yīng)的代碼,相對(duì)于左邊的源碼有如下變化:
- 整個(gè)模塊被包裹進(jìn) IIFE (立即執(zhí)行函數(shù))中
- 添加 __webpack_require__.r(__webpack_exports__); 語(yǔ)句,用于適配 ESM 規(guī)范
- 源碼中的 import 語(yǔ)句被轉(zhuǎn)譯為 __webpack_require__ 函數(shù)調(diào)用
- 源碼 console 語(yǔ)句所使用的 name 變量被轉(zhuǎn)譯為 _name__WEBPACK_IMPORTED_MODULE_0__.default
- 添加注釋
那么 Webpack 中如何執(zhí)行這些轉(zhuǎn)換的呢?
1.2 核心流程
「模塊轉(zhuǎn)譯」操作從 module.codeGeneration 調(diào)用開(kāi)始,對(duì)應(yīng)到上述流程圖的:
總結(jié)一下關(guān)鍵步驟:
1.調(diào)用 JavascriptGenerator 的對(duì)象的 generate 方法,方法內(nèi)部:
- 遍歷模塊的 dependencies 與 presentationalDependencies 數(shù)組
- 執(zhí)行每個(gè)數(shù)組項(xiàng) dependeny 對(duì)象的對(duì)應(yīng)的 template.apply 方法,在 apply 內(nèi)修改模塊代碼,或更新 initFragments 數(shù)組
2.遍歷完畢后,調(diào)用 InitFragment.addToSource 靜態(tài)方法,將上一步操作產(chǎn)生的 source 對(duì)象與 initFragments 數(shù)組合并為模塊產(chǎn)物
簡(jiǎn)單說(shuō)就是遍歷依賴,在依賴對(duì)象中修改 module 代碼,最后再將所有變更合并為最終產(chǎn)物。這里面關(guān)鍵點(diǎn):
- 在 Template.apply 函數(shù)中,如何更新模塊代碼
- 在 InitFragment.addToSource 靜態(tài)方法中,如何將 Template.apply 所產(chǎn)生的 side effect 合并為最終產(chǎn)物
這兩部分邏輯比較復(fù)雜,下面分開(kāi)講解。
1.3 Template.apply 函數(shù)
上述流程中,JavascriptGenerator 類是毋庸置疑的C位角色,但它并不直接修改 module 的內(nèi)容,而是繞了幾層后委托交由 Template 類型實(shí)現(xiàn)。
Webpack 5 源碼中,JavascriptGenerator.generate 函數(shù)會(huì)遍歷模塊的 dependencies 數(shù)組,調(diào)用依賴對(duì)象對(duì)應(yīng)的 Template 子類 apply 方法更新模塊內(nèi)容,說(shuō)起來(lái)有點(diǎn)繞,原始代碼更饒,所以我將重要步驟抽取為如下偽代碼:
- class JavascriptGenerator {
- generate(module, generateContext) {
- // 先取出 module 的原始代碼內(nèi)容
- const source = new ReplaceSource(module.originalSource());
- const { dependencies, presentationalDependencies } = module;
- const initFragments = [];
- for (const dependency of [...dependencies, ...presentationalDependencies]) {
- // 找到 dependency 對(duì)應(yīng)的 template
- const template = generateContext.dependencyTemplates.get(dependency.constructor);
- // 調(diào)用 template.apply,傳入 source、initFragments
- // 在 apply 函數(shù)可以直接修改 source 內(nèi)容,或者更改 initFragments 數(shù)組,影響后續(xù)轉(zhuǎn)譯邏輯
- template.apply(dependency, source, {initFragments})
- }
- // 遍歷完畢后,調(diào)用 InitFragment.addToSource 合并 source 與 initFragments
- return InitFragment.addToSource(source, initFragments, generateContext);
- }
- }
- // Dependency 子類
- class xxxDependency extends Dependency {}
- // Dependency 子類對(duì)應(yīng)的 Template 定義
- const xxxDependency.Template = class xxxDependencyTemplate extends Template {
- apply(dep, source, {initFragments}) {
- // 1. 直接操作 source,更改模塊代碼
- source.replace(dep.range[0], dep.range[1] - 1, 'some thing')
- // 2. 通過(guò)添加 InitFragment 實(shí)例,補(bǔ)充代碼
- initFragments.push(new xxxInitFragment())
- }
- }
從上述偽代碼可以看出,JavascriptGenerator.generate 函數(shù)的邏輯相對(duì)比較固化:
- 初始化一系列變量
- 遍歷 module 對(duì)象的依賴數(shù)組,找到每個(gè) dependency 對(duì)應(yīng)的 template 對(duì)象,調(diào)用 template.apply 函數(shù)修改模塊內(nèi)容
- 調(diào)用 InitFragment.addToSource 方法,合并 source 與 initFragments 數(shù)組,生成最終結(jié)果
這里的重點(diǎn)是 JavascriptGenerator.generate 函數(shù)并不操作 module 源碼,它僅僅提供一個(gè)執(zhí)行框架,真正處理模塊內(nèi)容轉(zhuǎn)譯的邏輯都在 xxxDependencyTemplate 對(duì)象的 apply 函數(shù)實(shí)現(xiàn),如上例偽代碼中 24-28行。
每個(gè) Dependency 子類都會(huì)映射到一個(gè)唯一的 Template 子類,且通常這兩個(gè)類都會(huì)寫在同一個(gè)文件中,例如 ConstDependency 與 ConstDependencyTemplate;NullDependency 與 NullDependencyTemplate。Webpack 構(gòu)建(make)階段,會(huì)通過(guò) Dependency 子類記錄不同情況下模塊之間的依賴關(guān)系;到生成(seal)階段再通過(guò) Template 子類修改 module 代碼。
綜上 Module、JavascriptGenerator、Dependency、Template 四個(gè)類形成如下交互關(guān)系:
Template 對(duì)象可以通過(guò)兩種方法更新 module 的代碼:
- 直接操作 source 對(duì)象,直接修改模塊代碼,該對(duì)象最初的內(nèi)容等于模塊的源碼,經(jīng)過(guò)多個(gè) Template.apply 函數(shù)流轉(zhuǎn)后逐漸被替換成新的代碼形式
- 操作 initFragments 數(shù)組,在模塊源碼之外插入補(bǔ)充代碼片段
這兩種操作所產(chǎn)生的 side effect,最終都會(huì)被傳入 InitFragment.addToSource 函數(shù),合成最終結(jié)果,下面簡(jiǎn)單補(bǔ)充一些細(xì)節(jié)。
1.3.1 使用 Source 更改代碼
Source 是 Webpack 中編輯字符串的一套工具體系,提供了一系列字符串操作方法,包括:
- 字符串合并、替換、插入等
- 模塊代碼緩存、sourcemap 映射、hash 計(jì)算等
Webpack 內(nèi)部以及社區(qū)的很多插件、loader 都會(huì)使用 Source 庫(kù)編輯代碼內(nèi)容,包括上文介紹的 Template.apply 體系中,邏輯上,在啟動(dòng)模塊代碼生成流程時(shí),Webpack 會(huì)先用模塊原本的內(nèi)容初始化 Source 對(duì)象,即:
- const source = new ReplaceSource(module.originalSource());
之后,不同 Dependency 子類按序、按需更改 source 內(nèi)容,例如 ConstDependencyTemplate 中的核心代碼:
- ConstDependency.Template = class ConstDependencyTemplate extends (
- NullDependency.Template
- ) {
- apply(dependency, source, templateContext) {
- // ...
- if (typeof dep.range === "number") {
- source.insert(dep.range, dep.expression);
- return;
- }
- source.replace(dep.range[0], dep.range[1] - 1, dep.expression);
- }
- };
上述 ConstDependencyTemplate 中,apply 函數(shù)根據(jù)參數(shù)條件調(diào)用 source.insert 插入一段代碼,或者調(diào)用 source.replace 替換一段代碼。
1.3.2 使用 InitFragment 更新代碼
除直接操作 source 外,Template.apply 中還可以通過(guò)操作 initFragments 數(shù)組達(dá)成修改模塊產(chǎn)物的效果。initFragments 數(shù)組項(xiàng)通常為 InitFragment 子類實(shí)例,它們通常帶有兩個(gè)函數(shù):getContent、getEndContent,分別用于獲取代碼片段的頭尾部分。
例如 HarmonyImportDependencyTemplate 的 apply 函數(shù)中:
- HarmonyImportDependency.Template = class HarmonyImportDependencyTemplate extends (
- ModuleDependency.Template
- ) {
- apply(dependency, source, templateContext) {
- // ...
- templateContext.initFragments.push(
- new ConditionalInitFragment(
- importStatement[0] + importStatement[1],
- InitFragment.STAGE_HARMONY_IMPORTS,
- dep.sourceOrder,
- key,
- runtimeCondition
- )
- );
- //...
- }
- }
1.4 代碼合并
上述 Template.apply 處理完畢后,產(chǎn)生轉(zhuǎn)譯后的 source 對(duì)象與代碼片段 initFragments 數(shù)組,接著就需要調(diào)用 InitFragment.addToSource 函數(shù)將兩者合并為模塊產(chǎn)物。
addToSource 的核心代碼如下:
- class InitFragment {
- static addToSource(source, initFragments, generateContext) {
- // 先排好順序
- const sortedFragments = initFragments
- .map(extractFragmentIndex)
- .sort(sortFragmentWithIndex);
- // ...
- const concatSource = new ConcatSource();
- const endContents = [];
- for (const fragment of sortedFragments) {
- // 合并 fragment.getContent 取出的片段內(nèi)容
- concatSource.add(fragment.getContent(generateContext));
- const endContent = fragment.getEndContent(generateContext);
- if (endContent) {
- endContents.push(endContent);
- }
- }
- // 合并 source
- concatSource.add(source);
- // 合并 fragment.getEndContent 取出的片段內(nèi)容
- for (const content of endContents.reverse()) {
- concatSource.add(content);
- }
- return concatSource;
- }
- }
可以看到,addToSource 函數(shù)的邏輯:
- 遍歷 initFragments 數(shù)組,按順序合并 fragment.getContent() 的產(chǎn)物
- 合并 source 對(duì)象
- 遍歷 initFragments 數(shù)組,按順序合并 fragment.getEndContent() 的產(chǎn)物
所以,模塊代碼合并操作主要就是用 initFragments 數(shù)組一層一層包裹住模塊代碼 source,而兩者都在 Template.apply 層面維護(hù)。
1.5 示例:自定義 banner 插件
經(jīng)過(guò) Template.apply 轉(zhuǎn)譯與 InitFragment.addToSource 合并之后,模塊就完成了從用戶代碼形態(tài)到產(chǎn)物形態(tài)的轉(zhuǎn)變,為加深對(duì)上述 「模塊轉(zhuǎn)譯」 流程的理解,接下來(lái)我們嘗試開(kāi)發(fā)一個(gè) Banner 插件,實(shí)現(xiàn)在每個(gè)模塊前自動(dòng)插入一段字符串。
實(shí)現(xiàn)上,插件主要涉及 Dependency、Template、hooks 對(duì)象,代碼:
- const { Dependency, Template } = require("webpack");
- class DemoDependency extends Dependency {
- constructor() {
- super();
- }
- }
- DemoDependency.Template = class DemoDependencyTemplate extends Template {
- apply(dependency, source) {
- const today = new Date().toLocaleDateString();
- source.insert(0, `/* Author: Tecvan */
- /* Date: ${today} */
- `);
- }
- };
- module.exports = class DemoPlugin {
- apply(compiler) {
- compiler.hooks.thisCompilation.tap("DemoPlugin", (compilation) => {
- // 調(diào)用 dependencyTemplates ,注冊(cè) Dependency 到 Template 的映射
- compilation.dependencyTemplates.set(
- DemoDependency,
- new DemoDependency.Template()
- );
- compilation.hooks.succeedModule.tap("DemoPlugin", (module) => {
- // 模塊構(gòu)建完畢后,插入 DemoDependency 對(duì)象
- module.addDependency(new DemoDependency());
- });
- });
- }
- };
示例插件的關(guān)鍵步驟:
編寫 DemoDependency 與 DemoDependencyTemplate 類,其中 DemoDependency 僅做示例用,沒(méi)有實(shí)際功能;DemoDependencyTemplate 則在其 apply 中調(diào)用 source.insert 插入字符串,如示例代碼第 10-14 行
- 使用 compilation.dependencyTemplates 注冊(cè) DemoDependency 與 DemoDependencyTemplate 的映射關(guān)系
- 使用 thisCompilation 鉤子取得 compilation 對(duì)象
- 使用 succeedModule 鉤子訂閱 module 構(gòu)建完畢事件,并調(diào)用 module.addDependency 方法添加 DemoDependency 依賴
完成上述操作后,module 對(duì)象的產(chǎn)物在生成過(guò)程就會(huì)調(diào)用到 DemoDependencyTemplate.apply 函數(shù),插入我們定義好的字符串,效果如:
感興趣的讀者也可以直接閱讀 Webpack 5 倉(cāng)庫(kù)的如下文件,學(xué)習(xí)更多用例:
- lib/dependencies/ConstDependency.js,一個(gè)簡(jiǎn)單示例,可學(xué)習(xí) source 的更多操作方法
- lib/dependencies/HarmonyExportSpecifierDependencyTemplate.js,一個(gè)簡(jiǎn)單示例,可學(xué)習(xí) initFragments 數(shù)組的更多用法
- lib/dependencies/HarmonyImportDependencyTemplate.js,一個(gè)較復(fù)雜但使用率極高的示例,可綜合學(xué)習(xí) source、initFragments 數(shù)組的用法
二、模塊合并打包原理
2.1 簡(jiǎn)介
講完單個(gè)模塊的轉(zhuǎn)譯過(guò)程后,我們先回到這個(gè)流程圖:
流程圖中,compilation.codeGeneration 函數(shù)執(zhí)行完畢 —— 也就是模塊轉(zhuǎn)譯階段完成后,模塊的轉(zhuǎn)譯結(jié)果會(huì)一一保存到 compilation.codeGenerationResults 對(duì)象中,之后會(huì)啟動(dòng)一個(gè)新的執(zhí)行流程 —— 「模塊合并打包」。
「模塊合并打包」 過(guò)程會(huì)將 chunk 對(duì)應(yīng)的 module 及 runtimeModule 按規(guī)則塞進(jìn) 「模板框架」 中,最終合并輸出成完整的 bundle 文件,例如上例中:
示例右邊 bundle 文件中,紅框框出來(lái)的部分為用戶代碼文件及運(yùn)行時(shí)模塊生成的產(chǎn)物,其余部分撐起了一個(gè) IIFE 形式的運(yùn)行框架即為 「模板框架」,也就是:
- (() => { // webpackBootstrap
- "use strict";
- var __webpack_modules__ = ({
- "module-a": ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
- // ! module 代碼,
- }),
- "module-b": ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
- // ! module 代碼,
- })
- });
- // The module cache
- var __webpack_module_cache__ = {};
- // The require function
- function __webpack_require__(moduleId) {
- // ! webpack CMD 實(shí)現(xiàn)
- }
- /************************************************************************/
- // ! 各種 runtime
- /************************************************************************/
- var __webpack_exports__ = {};
- // This entry need to be wrapped in an IIFE because it need to be isolated against other modules in the chunk.
- (() => {
- // ! entry 模塊
- })();
- })();
捋一下這里的邏輯,運(yùn)行框架包含如下關(guān)鍵部分:
- 最外層由一個(gè) IIFE 包裹
- 一個(gè)記錄了除 entry 外的其它模塊代碼的 __webpack_modules__ 對(duì)象,對(duì)象的 key 為模塊標(biāo)志符;值為模塊轉(zhuǎn)譯后的代碼
- 一個(gè)極度簡(jiǎn)化的 CMD 實(shí)現(xiàn):__webpack_require__ 函數(shù)
- 最后,一個(gè)包裹了 entry 代碼的 IIFE 函數(shù)
「模塊轉(zhuǎn)譯」 是將 module 轉(zhuǎn)譯為可以在宿主環(huán)境如瀏覽器上運(yùn)行的代碼形式;而 「模塊合并」 操作則串聯(lián)這些 modules ,使之整體符合開(kāi)發(fā)預(yù)期,能夠正常運(yùn)行整個(gè)應(yīng)用邏輯。接下來(lái),我們揭曉這部分代碼的生成原理。
2.2 核心流程
在 compilation.codeGeneration 執(zhí)行完畢,即所有用戶代碼模塊與運(yùn)行時(shí)模塊都執(zhí)行完轉(zhuǎn)譯操作后,seal 函數(shù)調(diào)用 compilation.createChunkAssets 函數(shù),觸發(fā) renderManifest 鉤子,JavascriptModulesPlugin 插件監(jiān)聽(tīng)到這個(gè)鉤子消息后開(kāi)始組裝 bundle,偽代碼:
- // Webpack 5
- // lib/Compilation.js
- class Compilation {
- seal() {
- // 先把所有模塊的代碼都轉(zhuǎn)譯,準(zhǔn)備好
- this.codeGenerationResults = this.codeGeneration(this.modules);
- // 1. 調(diào)用 createChunkAssets
- this.createChunkAssets();
- }
- createChunkAssets() {
- // 遍歷 chunks ,為每個(gè) chunk 執(zhí)行 render 操作
- for (const chunk of this.chunks) {
- // 2. 觸發(fā) renderManifest 鉤子
- const res = this.hooks.renderManifest.call([], {
- chunk,
- codeGenerationResults: this.codeGenerationResults,
- ...others,
- });
- // 提交組裝結(jié)果
- this.emitAsset(res.render(), ...others);
- }
- }
- }
- // lib/javascript/JavascriptModulesPlugin.js
- class JavascriptModulesPlugin {
- apply() {
- compiler.hooks.compilation.tap("JavascriptModulesPlugin", (compilation) => {
- compilation.hooks.renderManifest.tap("JavascriptModulesPlugin", (result, options) => {
- // JavascriptModulesPlugin 插件中通過(guò) renderManifest 鉤子返回組裝函數(shù) render
- const render = () =>
- // render 內(nèi)部根據(jù) chunk 內(nèi)容,選擇使用模板 `renderMain` 或 `renderChunk`
- // 3. 監(jiān)聽(tīng)鉤子,返回打包函數(shù)
- this.renderMain(options);
- result.push({ render /* arguments */ });
- return result;
- }
- );
- });
- }
- renderMain() {/* */}
- renderChunk() {/* */}
- }
這里的核心邏輯是,compilation 以 renderManifest 鉤子方式對(duì)外發(fā)布 bundle 打包需求;JavascriptModulesPlugin 監(jiān)聽(tīng)這個(gè)鉤子,按照 chunk 的內(nèi)容特性,調(diào)用不同的打包函數(shù)。
上述僅針對(duì) Webpack 5。在 Webpack 4 中,打包邏輯集中在 MainTemplate 完成。JavascriptModulesPlugin 內(nèi)置的打包函數(shù)有:
- renderMain:打包主 chunk 時(shí)使用
- renderChunk:打包子 chunk ,如異步模塊 chunk 時(shí)使用
兩個(gè)打包函數(shù)實(shí)現(xiàn)的邏輯接近,都是按順序拼接各個(gè)模塊,下面簡(jiǎn)單介紹下 renderMain 的實(shí)現(xiàn)。
2.3renderMain函數(shù)
renderMain 函數(shù)涉及比較多場(chǎng)景判斷,原始代碼很長(zhǎng)很繞,我摘了幾個(gè)重點(diǎn)步驟:
- class JavascriptModulesPlugin {
- renderMain(renderContext, hooks, compilation) {
- const { chunk, chunkGraph, runtimeTemplate } = renderContext;
- const source = new ConcatSource();
- // ...
- // 1. 先計(jì)算出 bundle CMD 核心代碼,包含:
- // - "var __webpack_module_cache__ = {};" 語(yǔ)句
- // - "__webpack_require__" 函數(shù)
- const bootstrap = this.renderBootstrap(renderContext, hooks);
- // 2. 計(jì)算出當(dāng)前 chunk 下,除 entry 外其它模塊的代碼
- const chunkModules = Template.renderChunkModules(
- renderContext,
- inlinedModules
- ? allModules.filter((m) => !inlinedModules.has(m))
- : allModules,
- (module) =>
- this.renderModule(
- module,
- renderContext,
- hooks,
- allStrict ? "strict" : true
- ),
- prefix
- );
- // 3. 計(jì)算出運(yùn)行時(shí)模塊代碼
- const runtimeModules =
- renderContext.chunkGraph.getChunkRuntimeModulesInOrder(chunk);
- // 4. 重點(diǎn)來(lái)了,開(kāi)始拼接 bundle
- // 4.1 首先,合并核心 CMD 實(shí)現(xiàn),即上述 bootstrap 代碼
- const beforeStartup = Template.asString(bootstrap.beforeStartup) + "\n";
- source.add(
- new PrefixSource(
- prefix,
- useSourceMap
- ? new OriginalSource(beforeStartup, "webpack/before-startup")
- : new RawSource(beforeStartup)
- )
- );
- // 4.2 合并 runtime 模塊代碼
- if (runtimeModules.length > 0) {
- for (const module of runtimeModules) {
- compilation.codeGeneratedModules.add(module);
- }
- }
- // 4.3 合并除 entry 外其它模塊代碼
- for (const m of chunkModules) {
- const renderedModule = this.renderModule(m, renderContext, hooks, false);
- source.add(renderedModule)
- }
- // 4.4 合并 entry 模塊代碼
- if (
- hasEntryModules &&
- runtimeRequirements.has(RuntimeGlobals.returnExportsFromRuntime)
- ) {
- source.add(`${prefix}return __webpack_exports__;\n`);
- }
- return source;
- }
- }
核心邏輯為:
1.先計(jì)算出 bundle CMD 代碼,即 __webpack_require__ 函數(shù)
2.計(jì)算出當(dāng)前 chunk 下,除 entry 外其它模塊代碼 chunkModules計(jì)算出運(yùn)行時(shí)模塊代碼
3.開(kāi)始執(zhí)行合并操作,子步驟有:
- 合并 CMD 代碼
- 合并 runtime 模塊代碼
- 遍歷 chunkModules 變量,合并除 entry 外其它模塊代碼
- 合并 entry 模塊代碼
4.返回結(jié)果
總結(jié):先計(jì)算出不同組成部分的產(chǎn)物形態(tài),之后按順序拼接打包,輸出合并后的版本。
至此,Webpack 完成 bundle 的轉(zhuǎn)譯、打包流程,后續(xù)調(diào)用 compilation.emitAsset ,按上下文環(huán)境將產(chǎn)物輸出到 fs 即可,Webpack 單次編譯打包過(guò)程就結(jié)束了。
三、總結(jié)
本文深入 Webpack 源碼,詳細(xì)討論了打包流程后半截 —— 從 chunk graph 生成一直到最終輸出產(chǎn)物的實(shí)現(xiàn)邏輯,重點(diǎn):
- 首先遍歷 chunk 中的所有模塊,為每個(gè)模塊執(zhí)行轉(zhuǎn)譯操作,產(chǎn)出模塊級(jí)別的產(chǎn)物
- 根據(jù) chunk 的類型,選擇不同結(jié)構(gòu)框架,按序逐次組裝模塊產(chǎn)物,打包成最終 bundle