Webpack漫談
本文使用「署名 4.0 國際 (CC BY 4.0)」 許可協(xié)議,歡迎轉(zhuǎn)載、或重新修改使用,但需要注明來源。
作者: 百應(yīng)前端團隊 @雙魚
https://juejin.im/user/307518985745895
1. 核心概念
entry:入口。webpack是基于模塊的,使用webpack首先需要指定模塊解析入口(entry),webpack從入口開始根據(jù)模塊間依賴關(guān)系遞歸解析和處理所有資源文件。 output:輸出。源代碼經(jīng)過webpack處理之后的最終產(chǎn)物。 loader:模塊轉(zhuǎn)換器。本質(zhì)就是一個函數(shù),在該函數(shù)中對接收到的內(nèi)容進行轉(zhuǎn)換,返回轉(zhuǎn)換后的結(jié)果。因為 Webpack 只認識 JavaScript,所以 Loader 就成了翻譯官,對其他類型的資源進行轉(zhuǎn)譯的預(yù)處理工作。 plugin:擴展插件。基于事件流框架 Tapable,插件可以擴展 Webpack 的功能,在 Webpack 運行的生命周期中會廣播出許多事件,Plugin 可以監(jiān)聽這些事件,在合適的時機通過 Webpack 提供的 API 改變輸出結(jié)果。module:模塊。除了js范疇內(nèi)的es module、commonJs、AMD等,css @import、url(...)、圖片、字體等在webpack中都被視為模塊。
另外webpack4開始 mode 變成一個重要概念,webpack為不同 mode提供了一些默認值,附上阮一峰老師的吐槽

不同mode的默認配置如下:

2. 打包流程
初始化參數(shù):從配置文件和 Shell 語句中讀取與合并參數(shù),得出最終的參數(shù); 初始化編譯:用上一步得到的參數(shù)初始化 Compiler 對象,注冊插件并傳入 Compiler 實例(掛載了眾多webpack事件api供插件調(diào)用); AST & 依賴圖:從入口文件(entry)出發(fā),調(diào)用AST引擎(acorn)生成抽象語法樹AST,根據(jù)AST構(gòu)建模塊的所有依賴; 遞歸編譯模塊:調(diào)用所有配置的 Loader 對模塊進行編譯; 輸出資源:根據(jù)入口和模塊之間的依賴關(guān)系,組裝成一個個包含多個模塊的 Chunk,再把每個 Chunk 轉(zhuǎn)換成一個單獨的文件加入到輸出列表,這步是可以修改輸出內(nèi)容的最后機會; 輸出完成:在確定好輸出內(nèi)容后,根據(jù)配置確定輸出的路徑和文件名,把文件內(nèi)容寫入到文件系統(tǒng);
在以上過程中,Webpack 會在特定的時間點廣播出特定的事件,插件在監(jiān)聽到相關(guān)事件后會執(zhí)行特定的邏輯,并且插件可以調(diào)用 Webpack 提供的 API 改變 Webpack 的運行結(jié)果
構(gòu)建流程核心概念:
Tapable:一個基于發(fā)布訂閱的事件流工具類,Compiler 和 Compilation 對象都繼承于 Tapable Compiler:webpack編譯貫穿始終的核心對象,在編譯初始化階段被創(chuàng)建的全局單例,包含完整配置信息、loaders、plugins以及各種工具方法 Compilation:代表一次 webpack 構(gòu)建和生成編譯資源的的過程,在watch模式下每一次文件變更觸發(fā)的重新編譯都會生成新的 Compilation 對象,包含了當(dāng)前編譯的模塊 module, 編譯生成的資源,變化的文件, 依賴的狀態(tài)等
更加細化的構(gòu)建流程圖:

看大圖點這里?
流程圖出處:淘系前端團隊-細說 webpack 之流程篇
3. Loader
loader就像一個翻譯官,將源文件經(jīng)過轉(zhuǎn)換后生成目標(biāo)文件并交由下一流程處理
使用方法

每個loader職責(zé)都是單一的,就像流水線上的工人 順序很關(guān)鍵(從右往左)
實現(xiàn)準(zhǔn)則
簡單【Simple】loader只做單一任務(wù),多個loader > 一個多功能loader 鏈?zhǔn)健綜haining】遵循鏈?zhǔn)秸{(diào)用原則 無狀態(tài)【Stateless】即函數(shù)式里的Pure Function,無副作用 使用工具庫【Loader Utilities】充分利用 loader-utils 包
實現(xiàn)一個簡單的loader,功能是替換console.log、去除換行符、在文件結(jié)尾處增加一行自定義內(nèi)容
/**?webpack.config.js?*/??
??
const?path?=?require("path");??
??
module.exports?=?{??
??entry:?{??
????index:?path.resolve(__dirname,?"src/index.js"),??
??},??
??output:?{??
????path:?path.resolve(__dirname,?"dist"),??
??},??
??module:?{??
????rules:?[??
??????{??
????????test:?/\.js$/,??
????????use:?[??
??????????{??
????????????loader:?path.resolve("lib/loader/loader1.js"),??
????????????options:?{??
??????????????message:?"this?is?a?message",??
????????????}??
??????????}??
????????],??
??????},??
????],??
??},??
};??
/**?lib/loader/loader1.js?*/??
??
const?loaderUtils?=?require('loader-utils');??
??
/**?過濾console.log和換行符?*/??
module.exports?=?function?(source)?{??
??
??//?獲取loader配置項??
??const?options?=?loaderUtils.getOptions(this);??
??
??console.log('loader配置項:',?options);??
??
??const?result?=?source??
????.replace(/console.log\(.*\);?/g,?"")??
????.replace(/\n/g,?"")??
????.concat(`console.log("${options.message?||?'沒有配置項'}");`);??
??
??return?result;??
};??
??
異步loader如何編寫
/**?lib/loader/loader1.js?*/??
??
/**?異步loader?*/??
module.exports?=?function?(source)?{??
??
??let?count?=?1;??
??
??//?1.調(diào)用this.async()?告訴webpack這是一個異步loader,需要等待?asyncCallback?回調(diào)之后再進行下一個loader處理??
??//?2.this.async?返回異步回調(diào),調(diào)用表示異步loader處理結(jié)束??
??const?asyncCallback?=?this.async();??
??
??const?timer?=?setInterval(()?=>?{??
????console.log(`時間已經(jīng)過去${count++}秒`);??
??},?1000);??
??
??//?異步操作??
??setTimeout(()?=>?{??
????clearInterval(timer);??
????asyncCallback(null,?source);??
??},?3200);??
??
};??
??
??
4. Plugin
在webpack編譯整個生命周期的特定節(jié)點執(zhí)行特定功能
實現(xiàn)要點:
一個命名JS函數(shù)或者JS類 在prototype上定義一個apply方法(供webpack調(diào)用,并且在調(diào)用時注入 compiler 對象) 在 apply 函數(shù)中需要有通過 compiler 對象掛載的 webpack 事件鉤子(鉤子函數(shù)中能拿到當(dāng)前編譯的 compilation 對象) 處理 webpack 內(nèi)部實例的特定數(shù)據(jù) 功能完成后調(diào)用 webpack 提供的回調(diào)
基本模型:
//?1、Plugin名稱??
const?MY_PLUGIN_NAME?=?"MyBasicPlugin";??
??
class?MyBasicPlugin?{??
??//?2、在構(gòu)造函數(shù)中獲取插件配置項??
??constructor(option)?{??
????this.option?=?option;??
??}??
??
??//?3、在原型對象上定義一個apply函數(shù)供webpack調(diào)用??
??apply(compiler)?{??
????//?4、注冊webpack事件監(jiān)聽函數(shù)??
????compiler.hooks.emit.tapAsync(??
??????MY_PLUGIN_NAME,??
??????(compilation,?asyncCallback)?=>?{??
??
????????//?5、操作Or改變compilation內(nèi)部數(shù)據(jù)??
????????console.log(compilation);????????
??
????????console.log("當(dāng)前階段?======>?編譯完成,即將輸出到output目錄");??
??
????????//?6、如果是異步鉤子,結(jié)束后需要執(zhí)行異步回調(diào)??
????????asyncCallback();??
??????}??
????);??
??}??
}??
??
//?7、模塊導(dǎo)出??
module.exports?=?MyBasicPlugin;??
實現(xiàn)一個plugin,功能是在dist目錄自動生成README文件:
const?MY_PLUGIN_NAME?=?"MyReadMePlugin";??
??
//?插件功能:自動生成README文件,標(biāo)題取自插件option ?
class?MyReadMePlugin?{??
??
??constructor(option)?{??
????this.option?=?option?||?{};??
??}??
??
??apply(compiler)?{??
????compiler.hooks.emit.tapAsync(??
??????MY_PLUGIN_NAME,??
??????(compilation,?asyncCallback)?=>?{??
????????compilation.assets["README.md"]?=?{??
??????????//?文件內(nèi)容??
??????????source:?()?=>?{??
????????????return?`#?${this.option.title?||?'默認標(biāo)題'}`;??
??????????},??
??????????//?文件大小??
??????????size:?()?=>?30,??
????????};??
????????asyncCallback();??
??????}??
????);??
??}??
}??
??
//?7、模塊導(dǎo)出??
module.exports?=?MyReadMePlugin;??
??
compiler.hooks 上掛載了不同時期觸發(fā)的webpack事件函數(shù)(類似于React生命周期),可以在編譯的各個階段執(zhí)行其他邏輯或者改變輸出結(jié)果,具體支持的事件列表可以看這里:compiler-hooks
Tapable:
webpack 的插件架構(gòu)主要基于 Tapable 實現(xiàn)的,Tapable 是 webpack 項目組的一個內(nèi)部庫,主要是抽象了一套插件機制。它類似于 NodeJS 的 EventEmitter 類,專注于自定義事件的觸發(fā)和操作。

Tapable事件類型分為同步和異步,內(nèi)部又以不同的規(guī)則分為不同類型,上述事件的具體區(qū)別可以看 這篇文章,理解這些事件的區(qū)別和應(yīng)用場景有助于我們理解webpack源碼和編寫Plugin
Complier對象:
在webpack啟動時被初始化一次,全局唯一,可以理解為webpack編譯實例,它包含了webpack原始配置、Loader、Plugin引用、各種鉤子

部分源碼:https://github.com/webpack/webpack/blob/10282ea20648b465caec6448849f24fc34e1ba3e/lib/webpack.js
5. 性能優(yōu)化
1. 從何開始?
使用 speed-measure-webpack-plugin 測量打包速度

使用 webpack-bundle-analyzer 進行體積分析

從某項目的分析圖可以看出一個很明顯的優(yōu)化空間就是 BizCharts 沒有按需引入,這時候我們可以import路徑再執(zhí)行一次打包分析看效果。
另外圖中每個模塊都有三種Size,分別是 Stat Size、Parsed Size、Gzipped Size,這三者的分別代表什么含義可以看下插件的github issue
2. 優(yōu)化Loader配置
思路主要是優(yōu)化搜索時間、縮小文件搜索范圍、減少不必要的編譯工作,具體做法可以看以下配置文件
module?.exports?=?{???
??module?:?{???
????rules?:?[{??
??????//?如果項目源碼中只有?文件,就不要寫成/\jsx?$/,以提升正則表達式的性能??
??????test:?/\.js$/,???
??????//?babel-loader?支持緩存轉(zhuǎn)換出的結(jié)果,通過?cacheDirectory?選項開啟??
??????use:?['babel-loader?cacheDirectory']?,???
??????//?只對項目根目錄下?src?目錄中的文件采用?babel-loader??
??????include:?path.resolve(__dirname,'src'),??
??????//?使用resolve.alias把原導(dǎo)入路徑映射成一個新的導(dǎo)入路徑,減少耗時的遞歸解析操作??
??????alias:?{??
????????'react':?path.resolve(?__dirname?,'./node_modules/react/dist/react.min.js'),??
??????},??
??????//?讓?Webpack?忽略對部分沒采用模塊化的文件的遞歸解析處理??
??????noParse:?'/jquery|lodash/',??
????}],??
??}??
}??
3. DLL Plugin Or Externals
合理使用DLLPlugin將更改頻率較低的代碼(三方庫)移到單獨的編譯中,我理解大部分場景下和配置 externals 作用是差不多的(都不用打包三方庫),但是 externals 在某些場景下會存在失效問題,具體可以看 這篇文章,另外 DLLPlugin 具體使用 參考這里

4. 多進程系列
多進程陣營里有幾位知名選手:
thread-loader(v4以后的官方推薦) happypack(不怎么維護了) parallel-webpack(不怎么維護了)
這里只介紹一下 thread-loader ,使用 thread-loader 將開銷較大的 loader(例如babel-loader)放到獨立進程中(官方描述 worker pool)處理,使用上有以下注意事項
將其放在需要單獨加載的loader的前面,順序很關(guān)鍵
module.exports?=?{??
??module:?{??
????rules:?[??
??????{??
????????test:?/\.js$/,??
????????include:?path.resolve("src"),??
????????use:?[??
??????????"thread-loader",??
??????????//?your?expensive?loader?(e.g?babel-loader)??
????????]??
??????}??
????]??
??}??
}??
worker pool中的loader使用上是有限制的,例如無法使用自定義 loader api,無法獲取webpack 配置項
5. 合理利用緩存 縮短非首次構(gòu)建時間
目前項目在用的插件是 hard-source-webpack-plugin,效果較為顯著,不過缺點有3
生成的緩存文件較大,比較占用磁盤空間(之前還出現(xiàn)過發(fā)布的時候誤把緩存文件上傳到服務(wù)器導(dǎo)致發(fā)布特別慢的情況 =。=,所以最好還是指定緩存文件路徑為 node_modules 內(nèi)部) 這個倉庫也很久沒更新了 現(xiàn)有項目偶爾會出現(xiàn)更改代碼不觸發(fā)重新編譯的情況,猜測可能與此插件有關(guān)
另外 webpack5 是否有自帶的緩存策略或者官方維護的緩存插件還需要去了解一下

6. 代碼壓縮 減少產(chǎn)物體積
webpack3配置optimization.minimize = true會默認啟用 UglifyJsPlugin,其多進程版本為 ParallelUglifyPlugin webpack4 中 webpack.optimize.UglifyJsPlugin 已被廢棄,默認內(nèi)置使用 terser-webpack-plugin 插件壓縮優(yōu)化代碼,原生支持多進程(這里想起官方文檔 Build Performance 章節(jié)中列舉的優(yōu)化措施第一點:Stay Up to Date,最香的還是最新的webpack版本)
7. Code Splitting
官方文檔描述的code splitting的3種姿勢:
多entry配置(多entry是天然的code splitting,但是基本沒人會因為性能優(yōu)化的點去把一個單頁應(yīng)用改成多entry)
使用 SplitChunksPlugin 進行重復(fù)數(shù)據(jù)刪除和提取

使用 Dynamic Import 指定模塊拆分,并且可以結(jié)合 preload、prefetch做更多用戶體驗上的優(yōu)化

6. 想的更遠:那些值得深究的問題?
HMR的原理? Tree shaking原理,為什么需要es module的寫法? webpack5的Module Federation有哪些優(yōu)勢,在與http2.0的結(jié)合上有哪些有趣的事情,在微前端上的應(yīng)用? 為什么說rollup比webpack更適合打包組件庫?
