從源碼出發(fā)探索理解 webpack 核心特性
webpack作為模塊打包工具,在前端界可以說是惡棍天使一般的存在,前端人對它大多數(shù)對它都是咬牙切齒的,以至于后面出現(xiàn)vite,以及剛剛出爐號稱比webpack快700倍的Turbopack,日薄西山的webpacK儼然已經(jīng)成為過氣網(wǎng)紅,但是在目前來說,很多公司老項(xiàng)目都還是用webpack體系進(jìn)行打包,面試的時候,往往也是會提一嘴,你做過什么優(yōu)化,webpack的一些原理和知識點(diǎn)好像還是必備, 早在兩年前我就寫過一篇文章《前端開發(fā)中常用的webpack優(yōu)化和相關(guān)原理》,那會還是webpack4,后來2020年10月10號左右,webpack5橫空出世,截止到今天2022年10月,webpack已經(jīng)更新到V5.75,再次重新去“順藤摸瓜”式的去粗讀源碼,溫故知新,當(dāng)然大家也可以帶著下面的問題思考:
- 1、webpack的構(gòu)建流程是怎么樣的?
- 2、webpack-cli對于文件打包是必須的嗎?
- 3、webpack中l(wèi)oader先執(zhí)行還是plugin先執(zhí)行?
- 4、webpack中 loader 的鏈?zhǔn)秸{(diào)用與執(zhí)行順序是怎么樣的?
- 5、 module.rules的loader的加載和傳遞流程是怎么樣的呢?
- 5、 webpack優(yōu)化新的思考
1.1 梳理前置知識
從webpack4(2018年2月)開始它的CLI部分單獨(dú)抽離在?webpack-cli?模塊中,接下來我們就帶著源碼一點(diǎn)點(diǎn)來探索,webpack的核心工作流程。
注:本文下載的
webpack源碼版本為
"version": "5.75.0"
webpack-cli源碼版本為"version": "4.10.0"
通過 上篇文章《前端工程化基建探索(3)定制腳手架模板——前端新建項(xiàng)目的“反卷利器”》我們對cli腳手架有個基礎(chǔ)知識儲備,現(xiàn)在閱讀起源碼來,也稍微更有方向感,我們從github下載webpack源碼到本地,直奔主題。
1.2 啟動webpack都做了什么
查看webpack啟動文件,我們可以看到它首先做了一些判斷是否安裝了webpack-cli,如果不安裝webpack-cli,程序運(yùn)行就結(jié)束。
這里我們重點(diǎn)關(guān)注?runCli?函數(shù),調(diào)用它會執(zhí)行?webpack-cli?中bin目錄下的cli.js?文件,后面的工作就交給webpack-cli?處理了。
接下來我們關(guān)注點(diǎn)轉(zhuǎn)移到webpack-cli?源碼,webpack-cli的啟動文件,打開webpack-cli/bin/cli.js
#!/usr/bin/env?node
"use?strict";
const?importLocal?=?require("import-local");
const?runCLI?=?require("../lib/bootstrap");
if?(!process.env.WEBPACK_CLI_SKIP_IMPORT_LOCAL)?{
??//?Prefer?the?local?installation?of?`webpack-cli`
??if?(importLocal(__filename))?{
????return;
??}
}
process.title?=?"webpack";
runCLI(process.argv);
最終它去執(zhí)行await cli.run(args),用來處理命令行參數(shù)
image.png然后我們?nèi)タ纯?require("./webpack-cli")?里面的?run()?函數(shù),這個run函數(shù),足足有700多行代碼(看完燒腦),總的來說是處理命令行,并通過調(diào)用webpack的核心函數(shù),構(gòu)建compiler 對象,最后再執(zhí)行整個構(gòu)建流程。
image.png結(jié)論:webpack-cli對于文件打包不是必需的,因?yàn)樗皇翘幚砻钚袇?shù),我們也可以自行設(shè)計(jì)cli來處理參數(shù),比如我們常見的 Vue/React框架也是沒有使用webpack-cli
啟動流程總結(jié)
webpack啟動流程.png內(nèi)部的運(yùn)行機(jī)制

(圖片來源:點(diǎn)擊圖片即可直達(dá))
1.3 整理一下Webpack 核心工作過程中的關(guān)鍵環(huán)節(jié)
一、初始化參數(shù)
(1)從配置文件解析配置項(xiàng),開始載入?Webpack?核心模塊,傳入配置選項(xiàng),這部分工作由?webpack-cli?處理
二、 開始編譯
(1)?run():編譯的入口方法
(2)?run觸發(fā)compile,接下來就是開始構(gòu)建options中模塊
(3) 構(gòu)建compilation對象。該對象負(fù)責(zé)組織整個編譯過程,包含了每個構(gòu)建環(huán)節(jié)所對應(yīng)的方法對象內(nèi)部保留了對compile對象的引用,并且存放所有modules, chunks,生成的assets以及最后用來生成最后JS的template。(4) compile中觸發(fā)make事件并調(diào)用addEntry
(5) 找到入口js文件,進(jìn)行下一步的模塊綁定
三、 構(gòu)建模塊
(1) 解析入口js文件,通過對應(yīng)的工廠方法創(chuàng)建模塊,保存到compilation對象上(通過單例模式保證同樣的模塊只有一個實(shí)例)
(2) 對module進(jìn)行build了。包括調(diào)用loader處理源文件,使用?acorn生成AST并且遍歷AST,遇到requirt等依賴時,創(chuàng)建依賴?Dependency加入依賴數(shù)組。
(3)?module已經(jīng)build完畢,此時開始處理依賴的module?異步的對依賴的module進(jìn)行build,如果依賴中仍有依賴,則循環(huán)處理其依賴
四、 完成模塊編譯
(1) 調(diào)用seal方法封裝,逐次對每個module和?chunk進(jìn)行整理,生成編譯后的源碼,合并,拆分。每一個chunk對應(yīng)一個入口文件。
(2) 開始處理最后生成的js
五、 輸出資源
根據(jù)入口和模塊之間的依賴關(guān)系,組裝成一個個包含多個模塊的?Chunk,再把每個?Chunk?轉(zhuǎn)換成一個單獨(dú)的文件加入到輸出列表
(1)所有的module,chunk仍然保存的是通過一個個require()聚合起來的代碼,需要通過Template產(chǎn)生最后帶有_webpack_require()的格式
(2)MainTemplate:處理入口文件的module,?ChunkTemplate:處理非首屏,需異步加載的module
(3)注意這里開始輸出生產(chǎn)的?assets,插件有機(jī)會最后修改assets
六、 輸出完成
在確定好輸出內(nèi)容后,根據(jù)配置確定輸出的路徑和文件名,把文件內(nèi)容寫入到文件系統(tǒng)。(1)不同的dependencyTemplates,如CommonJs,AMD... (2)生成好的js保存在compilation.assets中 (3)通過emitAssets將最終的js輸出到output的path中
2.1 webpack中l(wèi)oader先執(zhí)行還是plugin先執(zhí)行?
其實(shí)這個是一個偽命題,,單從字面上是不能判斷執(zhí)行順序的,大家都知道plugin?負(fù)責(zé)webpack除了模塊化打包外其他多樣性的構(gòu)建任務(wù)處理。
,我們從源碼上可以看到,當(dāng)創(chuàng)建了?Compiler?對象過后,Webpack?就開始注冊我們配置中的每一個插件,這樣webpack在往后的生命周期里,都可以觸發(fā)對應(yīng)的plugin?插件鉤子。
loader用于處理不同的文件類型,在編譯靜態(tài)資源個環(huán)節(jié)。我們繼續(xù)查閱源碼,創(chuàng)建完Compilation?對象過后,觸發(fā)了一個叫作?make?的鉤子,make的工作就是根據(jù)entry配置找到入口模塊,開始依次遞歸出所有依賴,形成依賴關(guān)系樹,buildModule?方法進(jìn)行模塊構(gòu)建,這里執(zhí)行具體的?Loader,處理特殊資源加載。
image.png2.2 webpack中 loader 的鏈?zhǔn)秸{(diào)用與執(zhí)行順序是怎么樣的?
在webpack官網(wǎng)上有一句話:
“ loader 總是從右到左被調(diào)用。有些情況下,loader 只關(guān)心 request 后面的?元數(shù)據(jù)(metadata),并且忽略前一個 loader 的結(jié)果。在實(shí)際(從右到左)執(zhí)行 loader 之前,會先?從左到右?調(diào)用 loader 上的?pitch?方法”
對于以下?use?配置:
module.exports?=?{
??//...
??module:?{
????rules:?[
??????{
????????//...
????????use:?['a-loader',?'b-loader',?'c-loader'],
??????},
????],
??},
};
將會發(fā)生這些步驟:
|-?a-loader?`pitch`
??|-?b-loader?`pitch`
????|-?c-loader?`pitch`
??????|-?requested?module?is?picked?up?as?a?dependency
????|-?c-loader?normal?execution
??|-?b-loader?normal?execution
|-?a-loader?normal?execution
module.rules的loader的加載和傳遞流程是怎么樣的呢?這里我們可以查閱源碼lib/rules/RuleSetCompiler.js
這里列舉了一兩個通過源碼查閱,來理解技術(shù)的方式,如果你有更多的疑問也可以一一通過源碼去查閱理解。
三、簡談項(xiàng)目中webpack優(yōu)化webpack優(yōu)化,一提到這個問題,大家用心玩過webpack的人都能道個一二三,但是怎么樣系統(tǒng)的思考優(yōu)化,或許很少人會去總結(jié)。
3.1 怎么去量化優(yōu)化指標(biāo)?
一切以結(jié)果為導(dǎo)向,優(yōu)化到底優(yōu)化了什么,怎么樣量化(可視化、數(shù)據(jù)化),這個作為職場人重要的職業(yè)技能,優(yōu)化,通常來說我們可以從時間和空間兩個維度去考量:
(1)時間:比如編譯和打包過程中的耗時情況,你從3分鐘提速到1分鐘,這是一個很好的時間參數(shù)優(yōu)化,所以我們可以選擇一些基于時間的分析工具或插件,來幫我們統(tǒng)耗時情況,當(dāng)然你也可以自己卷一個工具,NPM上成熟的工具已經(jīng)有很多例如:?speed-measure-webpack-plugin
(2) 空間:這里通常指的就是輸出的代碼體積,你從10M壓縮到1M,這是一個很巨大體積優(yōu)化,我們可以通過產(chǎn)物內(nèi)容分析工具webpack-bundle-analyzer
image.png可以直觀分析打包出的文件包含哪些,大小占比如何,壓縮后的大小。找到那些冗余的、可以被優(yōu)化的依賴項(xiàng)。
3.2 定位優(yōu)化方向
一、編譯提效
我們可以從開發(fā)場景,體驗(yàn)了Vite的絲滑后,webpack開發(fā)編譯等待是個難受的,多的時候得3-5分鐘,才能看到編譯成功,要提升這一階段的構(gòu)建效率,大致可以分為三個方向
- 減少執(zhí)行編譯的模塊。
- 提升單個模塊構(gòu)建的速度。
- 并行構(gòu)建以提升總體效率。
二、打包提效
通常我們發(fā)布項(xiàng)目的時候打包也是也超級長的等待時間,我們可以回憶上面webpack的構(gòu)建流程中的環(huán)節(jié),大概可以分為兩個方向:
-
以提升當(dāng)前任務(wù)工作效率為目標(biāo),壓縮代碼,壓縮
JS、css、圖片等靜態(tài)資源 -
以提升后續(xù)環(huán)節(jié)工作效率為目標(biāo),比如分模塊打包(
Split Chunks),搖樹(Tree Shaking)
以上都有很成熟的插件和文章去執(zhí)行webpack的優(yōu)化了,這里就不再細(xì)做班門弄斧了。
本文主要是簡單從幾個小角度探索源碼出發(fā)理解webpack核心特性,和簡談項(xiàng)目中webpack優(yōu)化,如果你對webpack一些特性有想了解的,也可以通過帶著問題和目標(biāo),去“順藤摸瓜”式的去粗讀源碼,了解更多相關(guān)知識點(diǎn),百尺竿頭更進(jìn)一步!
