自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

Webpack 原理系列八:產(chǎn)物轉(zhuǎn)譯打包邏輯

開發(fā) 前端
本文深入 Webpack 源碼,詳細討論了打包流程后半截 —— 從 chunk graph 生成一直到最終輸出產(chǎn)物的實現(xiàn)邏輯,首先遍歷 chunk 中的所有模塊,為每個模塊執(zhí)行轉(zhuǎn)譯操作,產(chǎn)出模塊級別的產(chǎn)物。

 回顧一下,在之前的文章《有點難的 webpack 知識點:Dependency Graph 深度解析》已經(jīng)聊到,經(jīng)過 「構(gòu)建(make)階段」 后,Webpack 解析出:

  • module 內(nèi)容
  • module 與 module 之間的依賴關(guān)系圖

而進入 「生成(「「seal」」)階段」 后,Webpack 首先根據(jù)模塊的依賴關(guān)系、模塊特性、entry配置等計算出 Chunk Graph,確定最終產(chǎn)物的數(shù)量和內(nèi)容,這部分原理在前文《有點難的知識點:Webpack Chunk 分包規(guī)則詳解》中也有較詳細的描述。

本文繼續(xù)聊聊 Chunk Graph 后面之后,模塊開始轉(zhuǎn)譯到模塊合并打包的過程,大體流程如下:

為了方便理解,我將打包過程橫向切分為三個階段:

  • 「入口」:指代從 Webpack 啟動到調(diào)用 compilation.codeGeneration 之前的所有前置操作
  • 「模塊轉(zhuǎn)譯」:遍歷 modules 數(shù)組,完成所有模塊的轉(zhuǎn)譯操作,并將結(jié)果存儲到 compilation.codeGenerationResults 對象
  • 「模塊合并打包」:在特定上下文框架下,組合業(yè)務模塊、runtime 模塊,合并打包成 bundle ,并調(diào)用 compilation.emitAsset 輸出產(chǎn)物

這里說的 「業(yè)務模塊」 是指開發(fā)者所編寫的項目代碼;「runtime 模塊」 是指 Webpack 分析業(yè)務模塊后,動態(tài)注入的用于支撐各項特性的運行時代碼,在上一篇文章 Webpack 原理系列六:徹底理解 Webpack 運行時 已經(jīng)有詳細講解,這里不贅述。

可以看到,Webpack 先將 modules 逐一轉(zhuǎn)譯為模塊產(chǎn)物 —— 「模塊轉(zhuǎn)譯」,再將模塊產(chǎn)物拼接成 bundle —— 「模塊合并打包」,我們下面會按照這個邏輯分開討論這兩個過程的原理。

一、模塊轉(zhuǎn)譯原理

1.1 簡介

先回顧一下 Webpack 產(chǎn)物:

上述示例由 index.js / name.js 兩個業(yè)務文件組成,對應的 Webpack 配置如上圖左下角所示;Webpack 構(gòu)建產(chǎn)物如右邊 main.js 文件所示,包含三塊內(nèi)容,從上到下分別為:

  • name.js 模塊對應的轉(zhuǎn)譯產(chǎn)物,函數(shù)形態(tài)
  • Webpack 按需注入的運行時代碼
  • index.js 模塊對應的轉(zhuǎn)譯產(chǎn)物,IIFE(立即執(zhí)行函數(shù)) 形態(tài)

其中,運行時代碼的作用與生成邏輯在上篇文章 Webpack 原理系列六:徹底理解 Webpack 運行時 已有詳盡介紹;另外兩塊分別為 name.js 、index.js 構(gòu)建后的產(chǎn)物,可以看到產(chǎn)物與源碼語義、功能均相同,但表現(xiàn)形式發(fā)生了較大變化,例如 index.js 編譯前后的內(nèi)容:

上圖右邊是 Webpack 編譯產(chǎn)物中對應的代碼,相對于左邊的源碼有如下變化:

  • 整個模塊被包裹進 IIFE (立即執(zhí)行函數(shù))中
  • 添加 __webpack_require__.r(__webpack_exports__); 語句,用于適配 ESM 規(guī)范
  • 源碼中的 import 語句被轉(zhuǎn)譯為 __webpack_require__ 函數(shù)調(diào)用
  • 源碼 console 語句所使用的 name 變量被轉(zhuǎn)譯為 _name__WEBPACK_IMPORTED_MODULE_0__.default
  • 添加注釋

那么 Webpack 中如何執(zhí)行這些轉(zhuǎn)換的呢?

1.2 核心流程

「模塊轉(zhuǎn)譯」操作從 module.codeGeneration 調(diào)用開始,對應到上述流程圖的:

總結(jié)一下關(guān)鍵步驟:

1.調(diào)用 JavascriptGenerator 的對象的 generate 方法,方法內(nèi)部:

  • 遍歷模塊的 dependencies 與 presentationalDependencies 數(shù)組
  • 執(zhí)行每個數(shù)組項 dependeny 對象的對應的 template.apply 方法,在 apply 內(nèi)修改模塊代碼,或更新 initFragments 數(shù)組

2.遍歷完畢后,調(diào)用 InitFragment.addToSource 靜態(tài)方法,將上一步操作產(chǎn)生的 source 對象與 initFragments 數(shù)組合并為模塊產(chǎn)物

簡單說就是遍歷依賴,在依賴對象中修改 module 代碼,最后再將所有變更合并為最終產(chǎn)物。這里面關(guān)鍵點:

  • 在 Template.apply 函數(shù)中,如何更新模塊代碼
  • 在 InitFragment.addToSource 靜態(tài)方法中,如何將 Template.apply 所產(chǎn)生的 side effect 合并為最終產(chǎn)物

這兩部分邏輯比較復雜,下面分開講解。

1.3 Template.apply 函數(shù)

上述流程中,JavascriptGenerator 類是毋庸置疑的C位角色,但它并不直接修改 module 的內(nèi)容,而是繞了幾層后委托交由 Template 類型實現(xiàn)。

Webpack 5 源碼中,JavascriptGenerator.generate 函數(shù)會遍歷模塊的 dependencies 數(shù)組,調(diào)用依賴對象對應的 Template 子類 apply 方法更新模塊內(nèi)容,說起來有點繞,原始代碼更饒,所以我將重要步驟抽取為如下偽代碼:

  1. class JavascriptGenerator { 
  2.     generate(module, generateContext) { 
  3.         // 先取出 module 的原始代碼內(nèi)容 
  4.         const source = new ReplaceSource(module.originalSource()); 
  5.         const { dependencies, presentationalDependencies } = module; 
  6.         const initFragments = []; 
  7.         for (const dependency of [...dependencies, ...presentationalDependencies]) { 
  8.             // 找到 dependency 對應的 template 
  9.             const template = generateContext.dependencyTemplates.get(dependency.constructor); 
  10.             // 調(diào)用 template.apply,傳入 source、initFragments 
  11.             // 在 apply 函數(shù)可以直接修改 source 內(nèi)容,或者更改 initFragments 數(shù)組,影響后續(xù)轉(zhuǎn)譯邏輯 
  12.             template.apply(dependency, source, {initFragments}) 
  13.         } 
  14.         // 遍歷完畢后,調(diào)用 InitFragment.addToSource 合并 source 與 initFragments 
  15.         return InitFragment.addToSource(source, initFragments, generateContext); 
  16.     } 
  17.  
  18. // Dependency 子類 
  19. class xxxDependency extends Dependency {} 
  20.  
  21. // Dependency 子類對應的 Template 定義 
  22. const xxxDependency.Template = class xxxDependencyTemplate extends Template { 
  23.     apply(dep, source, {initFragments}) { 
  24.         // 1. 直接操作 source,更改模塊代碼 
  25.         source.replace(dep.range[0], dep.range[1] - 1, 'some thing'
  26.         // 2. 通過添加 InitFragment 實例,補充代碼 
  27.         initFragments.push(new xxxInitFragment()) 
  28.     } 

從上述偽代碼可以看出,JavascriptGenerator.generate 函數(shù)的邏輯相對比較固化:

  1. 初始化一系列變量
  2. 遍歷 module 對象的依賴數(shù)組,找到每個 dependency 對應的 template 對象,調(diào)用 template.apply 函數(shù)修改模塊內(nèi)容
  3. 調(diào)用 InitFragment.addToSource 方法,合并 source 與 initFragments 數(shù)組,生成最終結(jié)果

這里的重點是 JavascriptGenerator.generate 函數(shù)并不操作 module 源碼,它僅僅提供一個執(zhí)行框架,真正處理模塊內(nèi)容轉(zhuǎn)譯的邏輯都在 xxxDependencyTemplate 對象的 apply 函數(shù)實現(xiàn),如上例偽代碼中 24-28行。

每個 Dependency 子類都會映射到一個唯一的 Template 子類,且通常這兩個類都會寫在同一個文件中,例如 ConstDependency 與 ConstDependencyTemplate;NullDependency 與 NullDependencyTemplate。Webpack 構(gòu)建(make)階段,會通過 Dependency 子類記錄不同情況下模塊之間的依賴關(guān)系;到生成(seal)階段再通過 Template 子類修改 module 代碼。

綜上 Module、JavascriptGenerator、Dependency、Template 四個類形成如下交互關(guān)系:

Template 對象可以通過兩種方法更新 module 的代碼:

  • 直接操作 source 對象,直接修改模塊代碼,該對象最初的內(nèi)容等于模塊的源碼,經(jīng)過多個 Template.apply 函數(shù)流轉(zhuǎn)后逐漸被替換成新的代碼形式
  • 操作 initFragments 數(shù)組,在模塊源碼之外插入補充代碼片段

這兩種操作所產(chǎn)生的 side effect,最終都會被傳入 InitFragment.addToSource 函數(shù),合成最終結(jié)果,下面簡單補充一些細節(jié)。

1.3.1 使用 Source 更改代碼

Source 是 Webpack 中編輯字符串的一套工具體系,提供了一系列字符串操作方法,包括:

  • 字符串合并、替換、插入等
  • 模塊代碼緩存、sourcemap 映射、hash 計算等

Webpack 內(nèi)部以及社區(qū)的很多插件、loader 都會使用 Source 庫編輯代碼內(nèi)容,包括上文介紹的 Template.apply 體系中,邏輯上,在啟動模塊代碼生成流程時,Webpack 會先用模塊原本的內(nèi)容初始化 Source 對象,即:

  1. const source = new ReplaceSource(module.originalSource()); 

之后,不同 Dependency 子類按序、按需更改 source 內(nèi)容,例如 ConstDependencyTemplate 中的核心代碼:

  1. ConstDependency.Template = class ConstDependencyTemplate extends ( 
  2.   NullDependency.Template 
  3. ) { 
  4.   apply(dependency, source, templateContext) { 
  5.     // ... 
  6.     if (typeof dep.range === "number") { 
  7.       source.insert(dep.range, dep.expression); 
  8.       return
  9.     } 
  10.  
  11.     source.replace(dep.range[0], dep.range[1] - 1, dep.expression); 
  12.   } 
  13. }; 

上述 ConstDependencyTemplate 中,apply 函數(shù)根據(jù)參數(shù)條件調(diào)用 source.insert 插入一段代碼,或者調(diào)用 source.replace 替換一段代碼。

1.3.2 使用 InitFragment 更新代碼

除直接操作 source 外,Template.apply 中還可以通過操作 initFragments 數(shù)組達成修改模塊產(chǎn)物的效果。initFragments 數(shù)組項通常為 InitFragment 子類實例,它們通常帶有兩個函數(shù):getContent、getEndContent,分別用于獲取代碼片段的頭尾部分。

例如 HarmonyImportDependencyTemplate 的 apply 函數(shù)中:

  1. HarmonyImportDependency.Template = class HarmonyImportDependencyTemplate extends ( 
  2.   ModuleDependency.Template 
  3. ) { 
  4.   apply(dependency, source, templateContext) { 
  5.     // ... 
  6.     templateContext.initFragments.push( 
  7.         new ConditionalInitFragment( 
  8.           importStatement[0] + importStatement[1], 
  9.           InitFragment.STAGE_HARMONY_IMPORTS, 
  10.           dep.sourceOrder, 
  11.           key
  12.           runtimeCondition 
  13.         ) 
  14.       ); 
  15.     //... 
  16.   } 
  17.  } 

1.4 代碼合并

上述 Template.apply 處理完畢后,產(chǎn)生轉(zhuǎn)譯后的 source 對象與代碼片段 initFragments 數(shù)組,接著就需要調(diào)用 InitFragment.addToSource 函數(shù)將兩者合并為模塊產(chǎn)物。

addToSource 的核心代碼如下:

  1. class InitFragment { 
  2.   static addToSource(source, initFragments, generateContext) { 
  3.     // 先排好順序 
  4.     const sortedFragments = initFragments 
  5.       .map(extractFragmentIndex) 
  6.       .sort(sortFragmentWithIndex); 
  7.     // ... 
  8.  
  9.     const concatSource = new ConcatSource(); 
  10.     const endContents = []; 
  11.     for (const fragment of sortedFragments) { 
  12.         // 合并 fragment.getContent 取出的片段內(nèi)容 
  13.       concatSource.add(fragment.getContent(generateContext)); 
  14.       const endContent = fragment.getEndContent(generateContext); 
  15.       if (endContent) { 
  16.         endContents.push(endContent); 
  17.       } 
  18.     } 
  19.  
  20.     // 合并 source 
  21.     concatSource.add(source); 
  22.     // 合并 fragment.getEndContent 取出的片段內(nèi)容 
  23.     for (const content of endContents.reverse()) { 
  24.       concatSource.add(content); 
  25.     } 
  26.     return concatSource; 
  27.   } 

可以看到,addToSource 函數(shù)的邏輯:

  • 遍歷 initFragments 數(shù)組,按順序合并 fragment.getContent() 的產(chǎn)物
  • 合并 source 對象
  • 遍歷 initFragments 數(shù)組,按順序合并 fragment.getEndContent() 的產(chǎn)物

所以,模塊代碼合并操作主要就是用 initFragments 數(shù)組一層一層包裹住模塊代碼 source,而兩者都在 Template.apply 層面維護。

1.5 示例:自定義 banner 插件

經(jīng)過 Template.apply 轉(zhuǎn)譯與 InitFragment.addToSource 合并之后,模塊就完成了從用戶代碼形態(tài)到產(chǎn)物形態(tài)的轉(zhuǎn)變,為加深對上述 「模塊轉(zhuǎn)譯」 流程的理解,接下來我們嘗試開發(fā)一個 Banner 插件,實現(xiàn)在每個模塊前自動插入一段字符串。

實現(xiàn)上,插件主要涉及 Dependency、Template、hooks 對象,代碼:

  1. const { Dependency, Template } = require("webpack"); 
  2.  
  3. class DemoDependency extends Dependency { 
  4.   constructor() { 
  5.     super(); 
  6.   } 
  7.  
  8. DemoDependency.Template = class DemoDependencyTemplate extends Template { 
  9.   apply(dependency, source) { 
  10.     const today = new Date().toLocaleDateString(); 
  11.     source.insert(0, `/* Author: Tecvan */ 
  12. /* Date: ${today} */ 
  13. `); 
  14.   } 
  15. }; 
  16.  
  17. module.exports = class DemoPlugin { 
  18.   apply(compiler) { 
  19.     compiler.hooks.thisCompilation.tap("DemoPlugin", (compilation) => { 
  20.       // 調(diào)用 dependencyTemplates ,注冊 Dependency 到 Template 的映射 
  21.       compilation.dependencyTemplates.set
  22.         DemoDependency, 
  23.         new DemoDependency.Template() 
  24.       ); 
  25.       compilation.hooks.succeedModule.tap("DemoPlugin", (module) => { 
  26.         // 模塊構(gòu)建完畢后,插入 DemoDependency 對象 
  27.         module.addDependency(new DemoDependency()); 
  28.       }); 
  29.     }); 
  30.   } 
  31. }; 

示例插件的關(guān)鍵步驟:

編寫 DemoDependency 與 DemoDependencyTemplate 類,其中 DemoDependency 僅做示例用,沒有實際功能;DemoDependencyTemplate 則在其 apply 中調(diào)用 source.insert 插入字符串,如示例代碼第 10-14 行

  • 使用 compilation.dependencyTemplates 注冊 DemoDependency 與 DemoDependencyTemplate 的映射關(guān)系
  • 使用 thisCompilation 鉤子取得 compilation 對象
  • 使用 succeedModule 鉤子訂閱 module 構(gòu)建完畢事件,并調(diào)用 module.addDependency 方法添加 DemoDependency 依賴

完成上述操作后,module 對象的產(chǎn)物在生成過程就會調(diào)用到 DemoDependencyTemplate.apply 函數(shù),插入我們定義好的字符串,效果如:

感興趣的讀者也可以直接閱讀 Webpack 5 倉庫的如下文件,學習更多用例:

  • lib/dependencies/ConstDependency.js,一個簡單示例,可學習 source 的更多操作方法
  • lib/dependencies/HarmonyExportSpecifierDependencyTemplate.js,一個簡單示例,可學習 initFragments 數(shù)組的更多用法
  • lib/dependencies/HarmonyImportDependencyTemplate.js,一個較復雜但使用率極高的示例,可綜合學習 source、initFragments 數(shù)組的用法

二、模塊合并打包原理

2.1 簡介

講完單個模塊的轉(zhuǎn)譯過程后,我們先回到這個流程圖:

流程圖中,compilation.codeGeneration 函數(shù)執(zhí)行完畢 —— 也就是模塊轉(zhuǎn)譯階段完成后,模塊的轉(zhuǎn)譯結(jié)果會一一保存到 compilation.codeGenerationResults 對象中,之后會啟動一個新的執(zhí)行流程 —— 「模塊合并打包」。

「模塊合并打包」 過程會將 chunk 對應的 module 及 runtimeModule 按規(guī)則塞進 「模板框架」 中,最終合并輸出成完整的 bundle 文件,例如上例中:

示例右邊 bundle 文件中,紅框框出來的部分為用戶代碼文件及運行時模塊生成的產(chǎn)物,其余部分撐起了一個 IIFE 形式的運行框架即為 「模板框架」,也就是:

  1. (() => { // webpackBootstrap 
  2.     "use strict"
  3.     var __webpack_modules__ = ({ 
  4.         "module-a": ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => { 
  5.             // ! module 代碼, 
  6.         }), 
  7.         "module-b": ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => { 
  8.             // ! module 代碼, 
  9.         }) 
  10.     }); 
  11.     // The module cache 
  12.     var __webpack_module_cache__ = {}; 
  13.     // The require function 
  14.     function __webpack_require__(moduleId) { 
  15.         // ! webpack CMD 實現(xiàn) 
  16.     } 
  17.     /************************************************************************/ 
  18.     // ! 各種 runtime 
  19.     /************************************************************************/ 
  20.     var __webpack_exports__ = {}; 
  21.     // This entry need to be wrapped in an IIFE because it need to be isolated against other modules in the chunk. 
  22.     (() => { 
  23.         // ! entry 模塊 
  24.     })(); 
  25. })(); 

捋一下這里的邏輯,運行框架包含如下關(guān)鍵部分:

  • 最外層由一個 IIFE 包裹
  • 一個記錄了除 entry 外的其它模塊代碼的 __webpack_modules__ 對象,對象的 key 為模塊標志符;值為模塊轉(zhuǎn)譯后的代碼
  • 一個極度簡化的 CMD 實現(xiàn):__webpack_require__ 函數(shù)
  • 最后,一個包裹了 entry 代碼的 IIFE 函數(shù)

「模塊轉(zhuǎn)譯」 是將 module 轉(zhuǎn)譯為可以在宿主環(huán)境如瀏覽器上運行的代碼形式;而 「模塊合并」 操作則串聯(lián)這些 modules ,使之整體符合開發(fā)預期,能夠正常運行整個應用邏輯。接下來,我們揭曉這部分代碼的生成原理。

2.2 核心流程

在 compilation.codeGeneration 執(zhí)行完畢,即所有用戶代碼模塊與運行時模塊都執(zhí)行完轉(zhuǎn)譯操作后,seal 函數(shù)調(diào)用 compilation.createChunkAssets 函數(shù),觸發(fā) renderManifest 鉤子,JavascriptModulesPlugin 插件監(jiān)聽到這個鉤子消息后開始組裝 bundle,偽代碼:

  1. // Webpack 5 
  2. // lib/Compilation.js 
  3. class Compilation { 
  4.   seal() { 
  5.     // 先把所有模塊的代碼都轉(zhuǎn)譯,準備好 
  6.     this.codeGenerationResults = this.codeGeneration(this.modules); 
  7.     // 1. 調(diào)用 createChunkAssets 
  8.     this.createChunkAssets(); 
  9.   } 
  10.  
  11.   createChunkAssets() { 
  12.     // 遍歷 chunks ,為每個 chunk 執(zhí)行 render 操作 
  13.     for (const chunk of this.chunks) { 
  14.       // 2. 觸發(fā) renderManifest 鉤子 
  15.       const res = this.hooks.renderManifest.call([], { 
  16.         chunk, 
  17.         codeGenerationResults: this.codeGenerationResults, 
  18.         ...others, 
  19.       }); 
  20.       // 提交組裝結(jié)果 
  21.       this.emitAsset(res.render(), ...others); 
  22.     } 
  23.   } 
  24.  
  25. // lib/javascript/JavascriptModulesPlugin.js 
  26. class JavascriptModulesPlugin { 
  27.   apply() { 
  28.     compiler.hooks.compilation.tap("JavascriptModulesPlugin", (compilation) => { 
  29.       compilation.hooks.renderManifest.tap("JavascriptModulesPlugin", (result, options) => { 
  30.           // JavascriptModulesPlugin 插件中通過 renderManifest 鉤子返回組裝函數(shù) render 
  31.           const render = () => 
  32.             // render 內(nèi)部根據(jù) chunk 內(nèi)容,選擇使用模板 `renderMain` 或 `renderChunk` 
  33.             // 3. 監(jiān)聽鉤子,返回打包函數(shù) 
  34.             this.renderMain(options); 
  35.  
  36.           result.push({ render /* arguments */ }); 
  37.           return result; 
  38.         } 
  39.       ); 
  40.     }); 
  41.   } 
  42.  
  43.   renderMain() {/*  */} 
  44.  
  45.   renderChunk() {/*  */} 

這里的核心邏輯是,compilation 以 renderManifest 鉤子方式對外發(fā)布 bundle 打包需求;JavascriptModulesPlugin 監(jiān)聽這個鉤子,按照 chunk 的內(nèi)容特性,調(diào)用不同的打包函數(shù)。

上述僅針對 Webpack 5。在 Webpack 4 中,打包邏輯集中在 MainTemplate 完成。JavascriptModulesPlugin 內(nèi)置的打包函數(shù)有:

  • renderMain:打包主 chunk 時使用
  • renderChunk:打包子 chunk ,如異步模塊 chunk 時使用

兩個打包函數(shù)實現(xiàn)的邏輯接近,都是按順序拼接各個模塊,下面簡單介紹下 renderMain 的實現(xiàn)。

2.3renderMain函數(shù)

renderMain 函數(shù)涉及比較多場景判斷,原始代碼很長很繞,我摘了幾個重點步驟:

  1. class JavascriptModulesPlugin { 
  2.   renderMain(renderContext, hooks, compilation) { 
  3.     const { chunk, chunkGraph, runtimeTemplate } = renderContext; 
  4.  
  5.     const source = new ConcatSource(); 
  6.     // ... 
  7.     // 1. 先計算出 bundle CMD 核心代碼,包含: 
  8.     //      - "var __webpack_module_cache__ = {};" 語句 
  9.     //      - "__webpack_require__" 函數(shù) 
  10.     const bootstrap = this.renderBootstrap(renderContext, hooks); 
  11.  
  12.     // 2. 計算出當前 chunk 下,除 entry 外其它模塊的代碼 
  13.     const chunkModules = Template.renderChunkModules( 
  14.       renderContext, 
  15.       inlinedModules 
  16.         ? allModules.filter((m) => !inlinedModules.has(m)) 
  17.         : allModules, 
  18.       (module) => 
  19.         this.renderModule( 
  20.           module, 
  21.           renderContext, 
  22.           hooks, 
  23.           allStrict ? "strict" : true 
  24.         ), 
  25.       prefix 
  26.     ); 
  27.  
  28.     // 3. 計算出運行時模塊代碼 
  29.     const runtimeModules = 
  30.       renderContext.chunkGraph.getChunkRuntimeModulesInOrder(chunk); 
  31.  
  32.     // 4. 重點來了,開始拼接 bundle 
  33.     // 4.1 首先,合并核心 CMD 實現(xiàn),即上述 bootstrap 代碼 
  34.     const beforeStartup = Template.asString(bootstrap.beforeStartup) + "\n"
  35.     source.add
  36.       new PrefixSource( 
  37.         prefix, 
  38.         useSourceMap 
  39.           ? new OriginalSource(beforeStartup, "webpack/before-startup"
  40.           : new RawSource(beforeStartup) 
  41.       ) 
  42.     ); 
  43.  
  44.     // 4.2 合并 runtime 模塊代碼 
  45.     if (runtimeModules.length > 0) { 
  46.       for (const module of runtimeModules) { 
  47.         compilation.codeGeneratedModules.add(module); 
  48.       } 
  49.     } 
  50.     // 4.3 合并除 entry 外其它模塊代碼 
  51.     for (const m of chunkModules) { 
  52.       const renderedModule = this.renderModule(m, renderContext, hooks, false); 
  53.       source.add(renderedModule) 
  54.     } 
  55.  
  56.     // 4.4 合并 entry 模塊代碼 
  57.     if ( 
  58.       hasEntryModules && 
  59.       runtimeRequirements.has(RuntimeGlobals.returnExportsFromRuntime) 
  60.     ) { 
  61.       source.add(`${prefix}return __webpack_exports__;\n`); 
  62.     } 
  63.  
  64.     return source; 
  65.   } 

核心邏輯為:

1.先計算出 bundle CMD 代碼,即 __webpack_require__ 函數(shù)

2.計算出當前 chunk 下,除 entry 外其它模塊代碼 chunkModules計算出運行時模塊代碼

3.開始執(zhí)行合并操作,子步驟有:

  • 合并 CMD 代碼
  • 合并 runtime 模塊代碼
  • 遍歷 chunkModules 變量,合并除 entry 外其它模塊代碼
  • 合并 entry 模塊代碼

4.返回結(jié)果

總結(jié):先計算出不同組成部分的產(chǎn)物形態(tài),之后按順序拼接打包,輸出合并后的版本。

至此,Webpack 完成 bundle 的轉(zhuǎn)譯、打包流程,后續(xù)調(diào)用 compilation.emitAsset ,按上下文環(huán)境將產(chǎn)物輸出到 fs 即可,Webpack 單次編譯打包過程就結(jié)束了。

三、總結(jié)

本文深入 Webpack 源碼,詳細討論了打包流程后半截 —— 從 chunk graph 生成一直到最終輸出產(chǎn)物的實現(xiàn)邏輯,重點:

  • 首先遍歷 chunk 中的所有模塊,為每個模塊執(zhí)行轉(zhuǎn)譯操作,產(chǎn)出模塊級別的產(chǎn)物
  • 根據(jù) chunk 的類型,選擇不同結(jié)構(gòu)框架,按序逐次組裝模塊產(chǎn)物,打包成最終 bundle

 

責任編輯:姜華 來源: Tecvan
相關(guān)推薦

2022-05-03 20:48:17

Webpackcommonjsesmodule

2021-05-31 05:36:43

WebpackJavaScript 前端

2021-09-13 09:40:35

Webpack 前端HMR 原理

2021-08-26 10:30:29

WebpackTree-Shakin前端

2021-12-16 22:02:28

webpack原理模塊化

2021-12-24 08:01:44

Webpack優(yōu)化打包

2021-12-25 22:29:04

WebpackRollup 前端

2020-08-05 08:21:41

Webpack

2020-07-10 12:06:28

WebpackBundleless瀏覽器

2021-10-12 09:52:30

Webpack 前端多進程打包

2024-05-27 00:00:01

2021-12-20 00:03:38

Webpack運行機制

2021-02-26 22:34:28

Webpack 前端項目

2022-02-10 14:23:16

WebpackJavaScript

2021-11-09 09:57:46

Webpack 前端分包優(yōu)化

2021-04-19 10:45:52

Webpack熱更新前端

2021-11-15 09:44:49

Webpack 前端 Scope Hois

2021-12-15 09:21:59

Webpack 前端Sourcemap

2021-10-25 10:23:49

Webpack 前端Tree shakin

2021-12-15 23:42:56

Webpack原理實踐
點贊
收藏

51CTO技術(shù)棧公眾號