webpack 是一個(gè)現(xiàn)代 JavaScript 應(yīng)用程序的靜態(tài)模塊打包器(module bundler)晨缴。
自從前端模塊化出現(xiàn)译秦,我們可以把代碼拆成一個(gè)個(gè) js 文件,通過(guò) import击碗、require() 去關(guān)聯(lián)依賴文件筑悴,最后再通過(guò)打包工具把這些模塊化的 js 依賴關(guān)系打包成一個(gè)或多個(gè) js 文件在 html 頁(yè)面去引入。webpack 作為一個(gè)模塊化解決方案稍途,把項(xiàng)目中使用的每個(gè)文件都視為 模塊(Modules)雷猪。除了 js,樣式文件中 @import 的 css晰房、stylesheet url(...)
、HTML <img src="...">
中引入的圖片在編譯過(guò)程中都會(huì)被當(dāng)作模塊依賴來(lái)處理射沟。因?yàn)?ES2015+ 殊者、TypeScript 和一些前端框架(如 Vue、 React)的存在验夯,webpack 又擔(dān)負(fù)著將這些瀏覽器不支持的文件轉(zhuǎn)化成可識(shí)別文件的工作猖吴。
除此之外,webpack 還能進(jìn)行 tree-shaking (剔除無(wú)效代碼) 和代碼壓縮挥转,以及抽離出異步加載模塊海蔽、第三方庫(kù)來(lái)實(shí)現(xiàn)最終打包好的主文件只是進(jìn)入首頁(yè)所需要的資源。
webpack 還提供了一系列開(kāi)發(fā)輔助工具绑谣,devserver党窜,HMR 等,幫助我們高效地開(kāi)發(fā)借宵。
webpack 插件架構(gòu)
插件是 webpack 的 支柱 功能幌衣,利用一些插件可以幫助我們提取公共依賴(拆包)、壓縮資源(代碼和圖片)的體積壤玫,大大優(yōu)化我們的構(gòu)建輸出豁护。
webpack 從配置初始化到構(gòu)建完成定義了一個(gè)生命周期。整個(gè)流程是一個(gè)事件驅(qū)動(dòng)架構(gòu)欲间,利用插件系統(tǒng) Tapable楚里,通過(guò)發(fā)布-訂閱事件來(lái)實(shí)現(xiàn)所有擴(kuò)展功能。webpack 在運(yùn)行過(guò)程中會(huì)在特定節(jié)點(diǎn)調(diào)用(廣播)一些 hook猎贴,訂閱了這些 hook 的插件在監(jiān)聽(tīng)到后會(huì)執(zhí)行綁定時(shí)定義好的邏輯班缎。
webpack 核心模塊
webpack 通過(guò) Compiler (主要引擎) 控制構(gòu)建流程蝴光,用 Compilation 對(duì)象存儲(chǔ)過(guò)程中的解析編譯信息。要厘清 webpack 打包原理吝梅,理解它們至關(guān)重要虱疏。關(guān)于這部分我仔細(xì)閱讀源碼寫(xiě)了這篇:webpack 之 Compiler 、Compilation 和 Tapable苏携。還有負(fù)責(zé)生成模塊的 ModuleFactory 生成模塊做瞪,解析源碼 的 Parser ,渲染代碼 的 Template右冻。
webpack 構(gòu)建流程
當(dāng) webpack 處理應(yīng)用程序時(shí)装蓬,它會(huì)從 入口 開(kāi)始,遞歸地構(gòu)建一個(gè)依賴關(guān)系圖 (dependency graph)纱扭,其中包含應(yīng)用程序所需的每個(gè)模塊 ( loader 負(fù)責(zé)將非JavaScript文件轉(zhuǎn)換為依賴圖能直接引用的有效模塊)牍帚,最后將所有這些模塊打包成一個(gè)或多個(gè) bundle。
先放上總的構(gòu)建原理圖乳蛾,后面會(huì)詳細(xì)去闡述暗赶。
再借一張別人畫(huà)的簡(jiǎn)易版流程圖:
幾個(gè)關(guān)鍵階段和結(jié)合資源形態(tài)流轉(zhuǎn)的角度對(duì)過(guò)程的說(shuō)明:
make
后,compilation 會(huì)獲知資源模塊的內(nèi)容與依賴關(guān)系肃叶,也就知道“輸入”是什么蹂随;而經(jīng)過(guò)seal
階段處理后, compilation 則獲知資源輸出的圖譜因惭,也就是知道怎么“輸出”:哪些模塊跟那些模塊“綁定”在一起輸出到哪里岳锁。
compiler.hooks.make 階段:
entry 文件以 dependence 對(duì)象形式加入 compilation 的依賴列表,dependence 對(duì)象記錄有 entry 的類型蹦魔、路徑等信息激率;
根據(jù) dependence 調(diào)用對(duì)應(yīng)的工廠函數(shù)創(chuàng)建 module 對(duì)象,之后讀入 module 對(duì)應(yīng)的文件內(nèi)容勿决,調(diào)用 loader-runner 對(duì)內(nèi)容做轉(zhuǎn)化乒躺,轉(zhuǎn)化結(jié)果若有其它依賴則繼續(xù)讀入依賴資源,重復(fù)此過(guò)程直到所有依賴均被轉(zhuǎn)化為 module低缩。
compilation.seal 階段:
遍歷 module 集合聪蘸,根據(jù) entry 配置及引入資源的方式,將 module 分配到不同的 chunk表制;
遍歷 chunk 集合健爬,調(diào)用 compilation.emitAsset 方法標(biāo)記 chunk 的輸出規(guī)則,即轉(zhuǎn)化為 assets 集合么介。
compiler.emitAssets 階段:
將 assets 寫(xiě)入文件系統(tǒng)娜遵。
webpack 的構(gòu)建從入口文件開(kāi)始,會(huì)找出有哪些模塊是入口起點(diǎn)依賴的壤短。需要 loader 處理的就先轉(zhuǎn)換編譯设拟,之后分析模塊自身是否有依賴慨仿,有依賴就接著處理依賴,流程和剛剛一致纳胧。像這樣遞歸獲取并處理每個(gè)模塊镰吆,同步為dependencies
,異步為block
跑慕,最終存儲(chǔ)到一個(gè) Map 表blockInfoMap
中 (ModuleGraph
)万皿。然后遍歷這些編譯完成的模塊,基于它們進(jìn)行分組 (chunkGroup) 和封包 (chunk) 核行,生成ChunkGraph
并優(yōu)化牢硅。
跟著會(huì)根據(jù)插件配置對(duì) chunk 進(jìn)一步優(yōu)化處理,比如代碼分割芝雪、 treeshaking 或者 代碼壓縮减余,最后生成我們需要的 js。
webpack 的運(yùn)行流程是一個(gè)串行的過(guò)程:
webpack 就像一條生產(chǎn)線惩系,要經(jīng)過(guò)一系列處理流程后才能將源文件轉(zhuǎn)換成輸出結(jié)果位岔。 這條生產(chǎn)線上的每個(gè)處理環(huán)節(jié)的職責(zé)都是單一的,多個(gè)流程之間存在依賴關(guān)系堡牡,只有完成當(dāng)前處理后才能交給下一個(gè)流程去處理抒抬。而插件就像是一個(gè)插入到生產(chǎn)線中的一個(gè)功能,在特定的時(shí)機(jī)對(duì)生產(chǎn)線上的資源做處理悴侵。webpack 通過(guò) Compiler 來(lái)組織這條復(fù)雜的生產(chǎn)線。webpack 在運(yùn)行過(guò)程中會(huì)廣播事件拭嫁,插件只需要監(jiān)聽(tīng)它所關(guān)心的事件可免,就能加入到這條生產(chǎn)線中,去改變生產(chǎn)線的運(yùn)作做粤。webpack 的事件流機(jī)制保證了插件的有序性浇借,使得整個(gè)系統(tǒng)擴(kuò)展性很好。
案例 demo
本系列的項(xiàng)目 demo怕品,后面會(huì)以此為例分析過(guò)程和結(jié)果:【淺析 webpack 打包流程(原理) - 案例 demo】
一妇垢、初始化工作
把 webpack-cli 傳的參數(shù)和項(xiàng)目配置做一個(gè)合并( cli 參數(shù)優(yōu)先級(jí)更高),并處理部分參數(shù) (驗(yàn)證:validateOptions(options)
處理:processOptions(options)
) 肉康,得到最終的配置 options闯估,接著對(duì)配置中的統(tǒng)計(jì)信息(options.stats)進(jìn)行處理。
創(chuàng)建 Compiler 實(shí)例:compiler = new Compiler(options.context)
(options.context 為項(xiàng)目絕對(duì)路徑)吼和,把最終配置 options 掛載到 compiler 對(duì)象下涨薪。
二、編譯前準(zhǔn)備
此階段概述:在 compiler 的各種 hook 上注冊(cè)項(xiàng)目配置的 plugins炫乓、注冊(cè) webpack 默認(rèn)插件 ?? 注冊(cè)
resolverFactory.hooks
為 Factory.createResolver 方法提供參數(shù)對(duì)象刚夺。
webpack 的事件機(jī)制是基于 tapable 庫(kù)做的事件流控制献丑,在整個(gè)編譯過(guò)程中暴露出各種hook,而 plugin 注冊(cè)監(jiān)聽(tīng)了某個(gè)/某些 hook侠姑,在這個(gè) hook 觸發(fā)時(shí)创橄,會(huì)執(zhí)行 plugin 里綁定的方法。
// /lib/Webpack.js
new NodeEnvironmentPlugin({
infrastructureLogging: options.infrastructureLogging
}).apply(compiler);
NodeEnvironmentPlugin
類主要對(duì)文件系統(tǒng)做一些封裝莽红,包括輸入妥畏,輸出,緩存船老,監(jiān)聽(tīng)等等咖熟,這些擴(kuò)展后的方法全部掛載在 compiler 對(duì)象下。
plugin.apply(compiler);
通過(guò)調(diào)用每個(gè)插件實(shí)例的 apply 方法柳畔,并把 complier 實(shí)例作為參數(shù)傳進(jìn)去馍管,在 compiler 生命周期的各種鉤子事件上注冊(cè)配置中的所有 plugins。即插件 apply 方法中訂閱了 compiler 的一些 hook薪韩,后續(xù) compiler 會(huì)根據(jù)運(yùn)行時(shí)各種事件鉤子的觸發(fā)确沸,去執(zhí)行插件注冊(cè)/綁定的函數(shù)。
關(guān)于 Compiler 和 插件機(jī)制我這篇有比較詳細(xì)的說(shuō)明 ?? webpack 之 Compiler 俘陷、Compilation 和 Tapable
// /lib/Webpack.js
compiler.options = new WebpackOptionsApply().process(options, compiler);
WebpackOptionsApply 類的 process 方法把配置里的一些屬性添加到 compiler 上罗捎,更主要的是注冊(cè)激活一些默認(rèn)自帶的插件和 resolverFactory.hooks
。大部分插件的作用是往 compiler 的兩個(gè) hook: compilation, thisCompilation
里注冊(cè)一些事件(此時(shí)這兩個(gè)鉤子已經(jīng)獲取到 normalModuleFactory 等參數(shù))拉盾,舉例:
// /lib/WebpackOptionsApply.js
new JavascriptModulesPlugin().apply(compiler); // 給 normalModuleFactory 的 js 模塊提供 Parser桨菜、JavascriptGenerator 對(duì)象 ,并給 seal 階段的 template 提供 renderManifest 數(shù)組(包含 render 方法)
new EntryOptionPlugin().apply(compiler); // 將插件注冊(cè)在compiler.hooks.entryOption 上
compiler.hooks.entryOption.call(options.context, options.entry); // 激活 entryOption 鉤子事件捉偏,EntryOptionPlugin 實(shí)例里綁定的方法隨即被觸發(fā)
EntryOptionPlugin 插件會(huì)根據(jù)入口配置是單入口或多入口實(shí)例化SingleEntryPlugin / MultiEntryPlugin 插件
倒得,兩者均會(huì)在 apply 方法里注冊(cè) compiler.hooks: compilation, make
。
插件處理完畢夭禽,觸發(fā)compiler.hooks.afterPlugins
鉤子霞掺。
// /lib/WebpackOptionsApply.js
compiler.resolverFactory.hooks.resolveOptions
.for("context")
.tap("WebpackOptionsApply", resolveOptions => {
return Object.assign(
{
fileSystem: compiler.inputFileSystem,
esolveToContext: true
},
cachedCleverMerge(options.resolve, resolveOptions)
);
});
然后依次注冊(cè) compiler.resolverFactory.hooks: resolveOptions.for (normal/context/loader)
,目的是為 Factory.createResolver 提供默認(rèn)參數(shù)對(duì)象 (包含相關(guān)的項(xiàng)目 resolve 配置項(xiàng))讹躯。觸發(fā) compiler.hooks.afterResolvers 鉤子菩彬,至此 compiler 初始化完畢。
三潮梯、開(kāi)始編譯
此階段概述:
compiler.run
??compiler.compile
開(kāi)啟編譯 ?? 實(shí)例化 NormalModuleFactory 類及 ContextModuleFactory 類 ?? 創(chuàng)建Compilation
實(shí)例 ?? 觸發(fā)compiler.hooks.make
鉤子執(zhí)行 compilation.addEntry (處理入口)骗灶,執(zhí)行 moduleFactory.create 開(kāi)始構(gòu)建 module。
compile 是真正進(jìn)行編譯的過(guò)程秉馏,最終會(huì)把所有原始資源編譯為目標(biāo)資源矿卑。
繼續(xù)回到/lib/Webpack.js
,判斷 options 里是否有 watch沃饶,有走 compiler.watch母廷,無(wú)則 compiler.run轻黑,我們執(zhí)行 compiler 的 run 方法,正式啟動(dòng)編譯琴昆。
首先調(diào)用compiler.hooks: beforeRun
鉤子氓鄙,做一些判斷 inputFileSystem 是否配置、讀取之前的 records 等處理业舍,再在回調(diào)里執(zhí)行 Compiler 類的compile
原型方法抖拦。
// /lib/Compiler.js
compile(callback) {
const params = {
normalModuleFactory: this.createNormalModuleFactory(),
contextModuleFactory: this.createContextModuleFactory(),
compilationDependencies: new Set()
};
}
先分別實(shí)例化 NormalModuleFactory 類和 ContextModuleFactory 類 (均擴(kuò)展于 tapable),和觸發(fā) compiler.hooks: normalModuleFactory 舷暮,contextModuleFactory
鉤子态罪。
// /lib/NormalModuleFactory.js
this.hooks.factory.tap("NormalModuleFactory", () => (result, callback) => {
let resolver = this.hooks.resolver.call(null);
resolver(result, (err, data) => {
// ...
});
});
this.hooks.resolver.tap("NormalModuleFactory", () => (data, callback) => {
// ...
});
NormalModuleFactory 負(fù)責(zé)生成各類模塊:從入口點(diǎn)開(kāi)始,分解每個(gè)請(qǐng)求下面,解析文件內(nèi)容以查找進(jìn)一步的請(qǐng)求复颈,然后通過(guò)分解所有請(qǐng)求以及解析新的文件來(lái)爬取全部文件。在最后階段沥割,每個(gè)依賴項(xiàng)都會(huì)成為一個(gè)模塊實(shí)例耗啦。
在實(shí)例化 NormalModuleFactory 執(zhí)行 constructor 的過(guò)程中,注冊(cè)了 normalModuleFactory.hooks: factory
机杜,觸發(fā) factory 鉤子時(shí)會(huì)先觸發(fā) normalModuleFactory.hooks: resolver
帜讲,再執(zhí)行注冊(cè)的回調(diào)函數(shù)。
ContextModuleFactory 從 webpack 獨(dú)特的 require.context API 生成依賴關(guān)系椒拗。它會(huì)解析請(qǐng)求的目錄似将,為每個(gè)文件生成請(qǐng)求,并依據(jù)傳遞來(lái)的 regExp 進(jìn)行過(guò)濾蚀苛。最后匹配成功的依賴關(guān)系將被傳入 NormalModuleFactory在验。
之后觸發(fā)compiler.hooks: beforeCompile、compile
枉阵,然后執(zhí)行:const compilation = this.newCompilation(params)
來(lái)實(shí)例化一個(gè) Compilation 類译红。
newCompilation 方法里還觸發(fā)了 compiler.hooks: thisCompilation预茄、compilation
兴溜,在編譯前注冊(cè)plugins階段WebpackOptionsApply.js
里注冊(cè)了大量這倆 hooks 的事件,此時(shí)拿到 compilation 對(duì)象耻陕,開(kāi)始執(zhí)行這一系列事件拙徽。
-
compiler.hooks.thisCompilation
會(huì)在 compilation 對(duì)象的 hooks 上注冊(cè)一些新事件; -
compiler.hooks.compilation
會(huì)在 compilation诗宣、normalModuleFactory 對(duì)象的 hooks 上注冊(cè)一些新事件膘怕,同時(shí)還會(huì)往 compilation.dependencyFactories (工廠類)、compilation.dependencyTemplates (模板類) 增加依賴模塊召庞。
為什么這里需要 thisCompilation岛心、compilation 兩個(gè)鉤子来破?
Compiler 的 createChildCompiler 方法可以創(chuàng)建子編譯器,過(guò)程中會(huì)復(fù)制 compilation 鉤子(上注入的插件方法)忘古,但不會(huì)復(fù)制thisCompilation
徘禁、make
、compile
等髓堪。子編譯器擁有完整的 module 和 chunk 生成送朱,通過(guò)它可以獨(dú)立于父編譯器執(zhí)行一個(gè)核心構(gòu)建流程,額外生成一些需要的 module 和 chunk干旁。
觸發(fā)compiler.hooks : make
驶沼,執(zhí)行之前在SingleEntryPlugin
| MultiEntryPlugin
注冊(cè)的訂閱事件,執(zhí)行:
// /lib/SingleEntryPlugin.js 或 /lib/MultiEntryPlugin.js
compiler.hooks.make.tapAsync(
"SingleEntryPlugin",
(compilation, callback) => {
const { entry, name, context } = this;
const dep = SingleEntryPlugin.createDependency(entry, name);
compilation.addEntry(context, dep, name, callback);
}
);
再看 compilation 的 addEntry 方法:
// /lib/Compilation.js
_addModuleChain(context, dependency, onModule, callback) {
// ...
const Dep = /** @type {DepConstructor} */ (dependency.constructor);
const moduleFactory = this.dependencyFactories.get(Dep); // moduleFactory 為 normalModuleFactory
this.semaphore.acquire(() => { // 編譯隊(duì)列控制
// 默認(rèn)并發(fā)數(shù)為 100争群,超過(guò)后存入 semaphore.waiters回怜,
// 根據(jù)情況再調(diào)用 semaphore.release 去執(zhí)行存入的事件 semaphore.waiters。
moduleFactory.create({...}, (err, module) => {
//...
});
});
}
addEntry(context, entry, name, callback) {
this.hooks.addEntry.call(entry, name); // 觸發(fā) addEntry 鉤子
// ...
this._addModuleChain( // 調(diào)用上面的_addModuleChain
context,
entry,
module => this.entries.push(module), // 把 module 添加 compilation.entries
(err, module) => {} // _addModuleChain 執(zhí)行完的回調(diào)
)
}
進(jìn)一步分析祭阀,dependency = SingleEntryPlugin.createDependency(entry, name)
鹉戚,即new SingleEntryDependency(entry)
,則 Dep 為 SingleEntryDependency 類专控,而之前compiler.hooks: compilation
的注冊(cè)事件中添加了依賴:
// /lib/SingleEntryPlugin.js 或 /lib/MultiEntryPlugin.js
compilation.dependencyFactories.set(
SingleEntryDependency,
normalModuleFactory
);
所以 moduleFactory 即為 normalModuleFactory抹凳。
this.semaphore
是一個(gè)編譯隊(duì)列控制,對(duì)執(zhí)行進(jìn)行了并發(fā)控制伦腐。moduleFactory.create
開(kāi)始構(gòu)建 module赢底, 遞歸解析依賴的重復(fù)從此處開(kāi)始。
下文:淺析 webpack 打包流程(原理) 二 - 遞歸構(gòu)建 module
webpack 打包流程系列(未完):
淺析 webpack 打包流程(原理) - 案例 demo
淺析 webpack 打包流程(原理) 一 - 準(zhǔn)備工作
淺析 webpack 打包流程(原理) 二 - 遞歸構(gòu)建 module
淺析 webpack 打包流程(原理) 三 - 生成 chunk
淺析 webpack 打包流程(原理) 四 - chunk 優(yōu)化
淺析 webpack 打包流程(原理) 五 - 構(gòu)建資源
淺析 webpack 打包流程(原理) 六 - 生成文件
參考鳴謝:
webpack打包原理 ? 看完這篇你就懂了 !
webpack 透視——提高工程化(原理篇)
webpack 透視——提高工程化(實(shí)踐篇)
webpack 4 源碼主流程分析
[萬(wàn)字總結(jié)] 一文吃透 Webpack 核心原理