接上文:淺析 webpack 打包流程(原理) 四 - chunk 優(yōu)化
七铸题、構建資源
本階段概述:
1.獲取 compilation 每個 module 的 buildInfo.assets,然后調(diào)用 this.emitAsset 生成 module 資源铁材;
2.遍歷compilation.chunks
生成 chunk 資源時宇攻,先根據(jù)是否含有 runtime (webpackBootstrap 代碼) 選擇不同的 template (有則 mainTemplate惫叛,不然一概 chunkTemplate),得到各自的 manifest 數(shù)據(jù) 和 pathAndInfo逞刷,然后調(diào)用不同的 render 渲染代碼嘉涌;
3.最后建立文件名與資源之間的映射,并將得到的所有目標資源信息掛載到compilation.assets
上亲桥。
4.如果有配置諸如terser-webpack-plugin
的代碼壓縮插件(一般都有)洛心,在optimizeChunkAssets
鉤子觸發(fā)后,對生成的資源根據(jù)seal
階段(生成chunk之前)做的標記進行 treeshaking题篷。
7.1 生成 module 資源
繼續(xù)執(zhí)行:
// /lib/Compilation.js
this.hooks.beforeModuleAssets.call();
this.createModuleAssets();
createModuleAssets 方法獲取每個 module 的 buildInfo.assets词身,然后觸發(fā)compilation.emitAsset
生成 module 資源,得到的相關數(shù)據(jù)存儲在compilation
的assets
和assetsInfo
番枚。buildInfo.assets 相關數(shù)據(jù)可在 loader 里調(diào)用 Api: this.emitFile 來生成 (loaderContext.emitFile 方法法严,詳見/lib/NormalModule.js
)。
7.2 生成 chunk 資源
7.2.1 生成前的準備
得到 manifest 數(shù)據(jù)對象
當 compiler 處理葫笼、解析和映射應用代碼時深啤,manifest 會記錄每個模塊的詳細要點(如 module identifier、路徑等)路星。程序運行時溯街,runtime 會根據(jù) manifest 中的數(shù)據(jù)來解析和加載模塊:
__webpack_require__
方法接收模塊標識符(identifier)作為參數(shù),簡稱 moduleId洋丐,通過這個標識符可以檢索出 manifest 集合中對應的模塊呈昔。
// /lib/Compilation.js
this.hooks.beforeChunkAssets.call();
this.createChunkAssets();
createChunkAssets 方法循環(huán)對每個 chunk 執(zhí)行:
// /lib/Compilation.js
createChunkAssets() {
// ...
for (let i = 0; i < this.chunks.length; i++) {
const chunk = this.chunks[i];
// 判斷 chunk 是否包含 runtime 代碼,獲取到對應的 template
const template = chunk.hasRuntime() ? this.mainTemplate : this.chunkTemplate;
// 得到相應的 manifest 數(shù)據(jù)對象
const manifest = template.getRenderManifest({
chunk,
hash: this.hash,
fullHash: this.fullHash,
outputOptions,
moduleTemplates: this.moduleTemplates,
dependencyTemplates: this.dependencyTemplates,
}); // manifest 為 `render` 所需要的全部信息:`[{ render(), filenameTemplate, pathOptions, identifier, hash }]`
// ...
}
}
判斷 chunk 是否含有 runtime 代碼 (它所屬的 chunkGroup 是否是初始的那個 EntryPoint友绝,chunkGroup.runtimeChunk 是否就是當前 chunk)堤尾,從而獲取到對應的 template:默認情況下包含 runtime 和同步依賴的 入口 chunk 對應 mainTemplate;異步 chunk 對應 chunkTemplate迁客。
默認配置下郭宝,即未手動抽離 runtime 和 配置 splitChunks,同步依賴都會被合并在入口 chunk 中掷漱,并且從屬于一個 EntryPoint(繼承自chunkGroup)粘室。而每個異步模塊都會單獨生成一個 chunk 和 chunkGroup。
一旦從入口 chunk 單獨抽出 runtime chunk卜范,則只有 runtime chunk 由mainTemplate
渲染衔统,其余都屬于由chunkTemplate
渲染的普通 chunk 了(無論同步異步)。
然后執(zhí)行對應的 getRenderManifest,觸發(fā)template.hooks:renderManifest
缰冤,執(zhí)行插件 JavascriptModulesPlugin 相關事件:
(如果有配置 MiniCssExtractPlugin 插件犬缨,也會在此時執(zhí)行從當前 chunk 分離出所有 CssModule [thisCompilation
時生成],并在當前 chunk 清單文件中添加一個單獨的 css 文件棉浸,即抽離 css 樣式到單獨的*.css
)
// /lib/JavascriptModulesPlugin.js
// 運行時 chunk (默認是入口 chunk) 相關的插件事件
compilation.mainTemplate.hooks.renderManifest.tap(...)
// 普通 chunk 相關的插件事件
compilation.chunkTemplate.hooks.renderManifest.tap(
"JavascriptModulesPlugin",
(result, options) => {
// ...
result.push({
render: () =>
this.renderJavascript(
compilation.chunkTemplate,
chunk,
moduleTemplates.javascript,
dependencyTemplates
),
filenameTemplate,
pathOptions: {
chunk,
contentHashType: "javascript"
},
identifier: `chunk${chunk.id}`,
hash: chunk.hash
});
return result;
}
);
得到 manifest 即 render 所需要的全部信息:[{ render(), filenameTemplate, pathOptions, identifier, hash }]
怀薛。
如果是 chunkTemplate 還會觸發(fā)插件 WebAssemblyModulesPlugin 去處理 WebAssembly 相關。
得到 pathAndInfo
然后遍歷 manifest 數(shù)組迷郑,在里面執(zhí)行:
// /lib/Compilation.js
const pathAndInfo = this.getPathWithInfo(
filenameTemplate,
fileManifest.pathOptions
);
getPathWithInfo 用于得到路徑和相關信息枝恋,會觸發(fā)mainTemplate.hooks: assetPath
,去執(zhí)行插件 TemplatedPathPlugin 相關事件嗡害,使用若干 replace 將如[name].[chunkhash:8].js
替換為0.c7687fbe.js
焚碌。
7.2.2 構建資源
然后判斷有無 source 緩存,若無則執(zhí)行:source = fileManifest.render();
即執(zhí)行 manifest 每一項里的 render 函數(shù)霸妹。
(1) chunkTemplate
生成主體 chunk 代碼
如果是普通 chunk十电,render 會調(diào)用 JavascriptModulesPlugin.js 插件里的renderJavascript
方法:先執(zhí)行 Template.renderChunkModules 靜態(tài)方法:
// /lib/JavascriptModulesPlugin.js
renderJavascript(chunkTemplate, chunk, moduleTemplate, dependencyTemplates) {
// 生成每個 module 代碼
const moduleSources = Template.renderChunkModules(
chunk,
m => typeof m.source === "function",
moduleTemplate,
dependencyTemplates
);
}
renderChunkModules 生成每個 module 代碼
// /lib/Template.js
static renderChunkModules(chunk, filterFn, moduleTemplate, dependencyTemplates, prefix = "" ) {
const allModules = modules.map((module) => {
return {
id: module.id,
// ModuleTemplate.js 的 render 方法,循環(huán)對每一個 module 執(zhí)行 render
source: moduleTemplate.render(module, dependencyTemplates, { chunk }),
};
});
}
// /lib/ModuleTemplate.js 的 render 方法
const moduleSource = module.source(
dependencyTemplates,
this.runtimeTemplate,
this.type
);
module.source 即/lib/NormalModule.js
的 source 方法叹螟,內(nèi)部執(zhí)行:const source = this.generator.generate(this, dependencyTemplates, runtimeTemplate, type);
這個 generator 就是在 reslove 流程 ?? getGenerator 所獲得鹃骂,即在/lib/JavascriptGenerator.js
執(zhí)行:
this.sourceBlock(module, module, [], dependencyTemplates, source, runtimeTemplate);
這里循環(huán)處理 module 的每個依賴(module.dependencies),獲得依賴所對應的 template 模板類罢绽,然后執(zhí)行該類的 apply:
// /lib/JavascriptGenerator.js
const template = dependencyTemplates.get(dependency.constructor);
template.apply(dependency, source, runtimeTemplate, dependencyTemplates);
這里的 dependencyTemplates 就是在【淺析 webpack 打包流程(原理) 一 - 準備工作】 ?? 實例化 compilation 時添加的依賴模板模塊畏线。
template.apply
會根據(jù)依賴的不同做相應的源碼轉(zhuǎn)化處理。但方法里并沒有直接執(zhí)行源碼轉(zhuǎn)化良价,而是將其轉(zhuǎn)化對象 push 到ReplaceSource.replacements
里寝殴,轉(zhuǎn)化對象的格式為:
注:webpack-sources 提供若干類型的 source 類,如 CachedSource明垢、PrefixSource蚣常、ConcatSource、ReplaceSource 等袖外。它們可以組合使用史隆,方便對代碼進行添加魂务、替換曼验、連接等操作。同時又含有一些 source-map 相關粘姜、updateHash 等 Api 供 webpack 內(nèi)部調(diào)用鬓照。
// Replacement
{
"content": "__webpack_require__.r(__webpack_exports__);\n", // 替換的內(nèi)容
"end": -11, // 替換源碼的終止位置
"insertIndex": 0, // 優(yōu)先級
"name": undefined, // 名稱
"start": -10 // 替換源碼的起始位置
}
各模版的轉(zhuǎn)化處理見【淺析 webpack 打包流程(原理) 二 - 遞歸構建 module】 最末:各依賴作用簡單解釋。
包裹代碼
收集完依賴相關的轉(zhuǎn)化對象 Replacement 后孤紧,對得到的結(jié)果進行cachedSource
緩存包裝豺裆,回到 ModuleTemplate.js 的 render 方法得到 moduleSource。
然后觸發(fā)ModuleTemplate.hooks:content、module臭猜、render躺酒、package
,content蔑歌、module 鉤子主要是可以讓我們完成對 module 源碼的再次處理羹应;然后在 render 鉤子里執(zhí)行插件 FunctionModuleTemplatePlugin 的訂閱事件:對處理后的 module 源碼進行包裹,即生成代碼:
/***/
(function (module, __webpack_exports__, __webpack_require__) {
'use strict';
// children 數(shù)組中的 CachedSource 即為`module`源碼次屠,里面包含 replacements
/***/
});
添加注釋
再觸發(fā) package 鉤子執(zhí)行插件 FunctionModuleTemplatePlugin 的訂閱事件园匹,主要是添加相關注釋,即生成代碼:
/*!***************************************************************!*\
!*** ./src/c.js ***!
\***************************************************************/
/*! exports provided: sub */
/***/
(function (module, __webpack_exports__, __webpack_require__) {
'use strict';
// children 數(shù)組中的 CachedSource 即為`module`源碼劫灶,里面包含 replacements
/***/
});
將所有的 module 都處理完畢后裸违,回到 Template.js 的 renderChunkModules 繼續(xù)處理生成代碼,最終將每個 module 生成的代碼串起來回到 JavascriptModulesPlugin.js 的 renderJavascript 方法里得到了 moduleSources本昏。
生成 jsonp 包裹代碼
接著觸發(fā)chunkTemplate.hooks: modules
供汛,為修改生成的 chunk 代碼提供鉤子。得到 core 后涌穆,觸發(fā)chunkTemplate.hooks: render
執(zhí)行插件 JsonpChunkTemplatePlugin.js 訂閱事件紊馏,主要是添加 jsonp 包裹代碼,得到:
美化一下就和我們常見的打包文件無異了:
(window['webpackJsonp'] = window['webpackJsonp'] || []).push([
[0],
{
// 前面生成的 chunk 代碼
/***/ "./src/c.js":
/*!******************!*\
!*** ./src/c.js ***!
\******************/
/*! exports provided: sub */
/***/ (function(module, __webpack_exports__, __webpack_require__) {
/***/ })
}
]);
最后 return 一個new ConcatSource(source, ";")
蒲犬。至此普通 (非初始) chunk 代碼 chunkTemplate 的fileManifest.render
(/lib/Compilation.js 中) 構建完成朱监。
注意:非初始不代表異步,當入口 chunk 被拆成多個同步 chunk原叮,初始 chunk 就指代包含 runtime 的那個 chunk赫编。
可以配合【webpack 模塊加載原理及打包文件分析 (一)】服用,便能清晰知曉window['webpackJsonp'].push
不光用來加載異步 chunk奋隶。
(2) mainTemplate
如果是初始 chunk擂送,render 會執(zhí)行 JavascriptModulesPlugin.js 里的 compilation.mainTemplate.render 即/lib/MainTemplate.js
的 render 方法。
生成 runtime 代碼
內(nèi)部執(zhí)行:const buf = this.renderBootstrap(hash, chunk, moduleTemplate, dependencyTemplates);
得到 webpack runtime bootstrap 代碼數(shù)組唯欣,過程中會判斷 chunks 中是否存在異步 chunk嘹吨,如果有,則代碼里還會包含異步相關的 runtime 代碼境氢。如果還有延遲加載的同步 chunk蟀拷,都會在這里處理為相應的 runtime。
包裹 runtime 與 chunk 代碼
然后執(zhí)行:
let source = this.hooks.render.call(
new OriginalSource(
Template.prefix(buf, " \t") + "\n",
"webpack/bootstrap"
),
chunk,
hash,
moduleTemplate,
dependencyTemplates
);
先通過 Template.js 的 prefix 方法合并 runtime 代碼字符串萍聊,得到 OriginalSource 實例问芬,然后將其作為參數(shù)執(zhí)行MainTemplate.hooks: render
,該 hook 事件注冊在 MainTemplate 自身的 constructor 中寿桨,代碼如下:
const source = new ConcatSource();
source.add('/******/ (function(modules) { // webpackBootstrap\n');
source.add(new PrefixSource('/******/', bootstrapSource));
source.add('/******/ })\n');
source.add('/************************************************************************/\n');
source.add('/******/ (');
source.add(this.hooks.modules.call(new RawSource(''), chunk, hash, moduleTemplate, dependencyTemplates));
source.add(')');
return source;
對 runtime bootstrap 代碼進行了包裝 (bootstrapSource 即前面生成的 runtime 代碼)此衅,過程中觸發(fā)MainTemplate.hooks: modules
得到 chunk 的生成代碼,即最終返回一個包含 runtime 代碼和 chunk 代碼的 ConcatSource 實例。
生成 chunk 代碼
這里來看通過this.hooks.modules.call()
鉤子得到 chunk 生成代碼的實現(xiàn):
觸發(fā)插件 JavascriptModulesPlugin 的注冊事件挡鞍,即執(zhí)行 Template 類的靜態(tài)方法renderChunkModules
骑歹。與前文 chunkTemplate ?? 生成主體 chunk 代碼的實現(xiàn)一致。
最終經(jīng)過包裹后得到的代碼大致如下:
"/******/ (function(modules) { // webpackBootstrap
// runtime 代碼的 PrefixSource 實例
/******/ })
/************************************************************************/
/******/ ({
/***/ "./src/a.js":
/*!******************!*\
!*** ./src/a.js ***!
\******************/
/*! no exports provided */
/***/ (function(module, __webpack_exports__, __webpack_require__) {
"use strict";
// module a 的 CachedSource 實例
/***/ }),
/***/ "./src/b.js":
/*!******************!*\
!*** ./src/b.js ***!
\******************/
/*! exports provided: add, unusedAdd */
/***/ (function(module, __webpack_exports__, __webpack_require__) {
"use strict";
// module b 的 CachedSource 實例
/***/ }),
/***/ "./src/d.js":
/*!******************!*\
!*** ./src/d.js ***!
\******************/
/*! exports provided: default */
/***/ (function(module, __webpack_exports__, __webpack_require__) {
"use strict";
// module d 的 CachedSource 實例
/***/ })
/******/ })"
最后返回一個new ConcatSource(source, ";")
墨微。至此入口 chunk (包含 runtime 的 chunk) 代碼 mainTemplate 的fileManifest.render
(/lib/Compilation.js 中) 構建完成陵刹。
7.2.3 文件名映射資源
接下來,無論是包含 runtime 的主 chunk 還是普通 chunk欢嘿,都回到 Compilation.js 的 createChunkAssets 方法衰琐,在compilation.cache[cacheName]
做了 source 緩存,然后執(zhí)行:
this.emitAsset(file, source, assetInfo);
chunk.files.push(file);
建立起文件名與對應源碼的聯(lián)系炼蹦,以this.assets[file] = source
的形式將該映射對象掛載到 compilation.assets 下羡宙。 把文件名稱存入對應的 chunk.files 數(shù)組中,即compilation.chunks
下掐隐。然后設置了 alreadyWrittenFiles (Map 對象)狗热,以防重復構建代碼。至此一個 chunk 的資源構建結(jié)束虑省。
所有 chunk 遍歷結(jié)束后匿刮,得到的compilation.assets
和 compilation.assetsInfo
:
// compilation
{
//...
"assets": {
"0.92bfd615.js": CachedSource, // CachedSource 里包含 chunk 資源
"index.1d678a.js": CachedSource
},
"assetsInfo": { // Map結(jié)構
0: {
"key": '0.92bfd615.js',
"value": {
immutable:true
}
},
1: {
"key": 'index.1d678a.js',
"value": {
immutable:true
}
}
}
}
7.3 對生成資源進行 TreeShaking (需配置相應插件)
在compilation.hooks.additionalAssets
鉤子觸發(fā)后,如果有配置進一步處理生成資源的插件探颈,則會對資源再度優(yōu)化熟丸。
比如compilation.hooks.optimizeChunkAssets
會觸發(fā)terser-webpack-plugin
代碼壓縮插件(一般都會配置,webpack 5 內(nèi)置)伪节,對生成的 chunk 資源根據(jù)(seal
階段compilation.hooks.optimizeDependencies
)生成的unused harmony export
標記等信息進行 treeshaking光羞。
下文:淺析 webpack 打包流程(原理) 六 - 生成文件
webpack 打包流程系列(未完):
淺析 webpack 打包流程(原理) - 案例 demo
淺析 webpack 打包流程(原理) 一 - 準備工作
淺析 webpack 打包流程(原理) 二 - 遞歸構建 module
淺析 webpack 打包流程(原理) 三 - 生成 chunk
淺析 webpack 打包流程(原理) 四 - chunk 優(yōu)化
淺析 webpack 打包流程(原理) 五 - 構建資源
淺析 webpack 打包流程(原理) 六 - 生成文件
參考鳴謝:
webpack打包原理 ? 看完這篇你就懂了 !
webpack 透視——提高工程化(原理篇)
webpack 透視——提高工程化(實踐篇)
webpack 4 源碼主流程分析
[萬字總結(jié)] 一文吃透 Webpack 核心原理
關于Webpack詳述系列文章 (第三篇)