淺析 webpack 打包流程(原理) 五 - 構建資源

接上文:淺析 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ù)存儲在compilationassetsassetsInfo番枚。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
  /***/
});
拿到的 moduleSourcePostRender 值
添加注釋

再觸發(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.assetscompilation.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
      }
    }
  }
}
compilation.assets 和 compilation.assetsInfo
compilation.assets[chunkName]._source

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詳述系列文章 (第三篇)

最后編輯于
?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市怀大,隨后出現(xiàn)的幾起案子纱兑,更是在濱河造成了極大的恐慌,老刑警劉巖化借,帶你破解...
    沈念sama閱讀 211,743評論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件潜慎,死亡現(xiàn)場離奇詭異,居然都是意外死亡蓖康,警方通過查閱死者的電腦和手機铐炫,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,296評論 3 385
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來钓瞭,“玉大人驳遵,你說我怎么就攤上這事淫奔∩轿校” “怎么了?”我有些...
    開封第一講書人閱讀 157,285評論 0 348
  • 文/不壞的土叔 我叫張陵,是天一觀的道長鸭丛。 經(jīng)常有香客問我竞穷,道長,這世上最難降的妖魔是什么鳞溉? 我笑而不...
    開封第一講書人閱讀 56,485評論 1 283
  • 正文 為了忘掉前任瘾带,我火速辦了婚禮,結(jié)果婚禮上熟菲,老公的妹妹穿的比我還像新娘看政。我一直安慰自己,他們只是感情好抄罕,可當我...
    茶點故事閱讀 65,581評論 6 386
  • 文/花漫 我一把揭開白布允蚣。 她就那樣靜靜地躺著,像睡著了一般呆贿。 火紅的嫁衣襯著肌膚如雪嚷兔。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,821評論 1 290
  • 那天做入,我揣著相機與錄音冒晰,去河邊找鬼。 笑死竟块,一個胖子當著我的面吹牛壶运,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播浪秘,決...
    沈念sama閱讀 38,960評論 3 408
  • 文/蒼蘭香墨 我猛地睜開眼前弯,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了秫逝?” 一聲冷哼從身側(cè)響起恕出,我...
    開封第一講書人閱讀 37,719評論 0 266
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎违帆,沒想到半個月后浙巫,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,186評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡刷后,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,516評論 2 327
  • 正文 我和宋清朗相戀三年的畴,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片尝胆。...
    茶點故事閱讀 38,650評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡丧裁,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出含衔,到底是詐尸還是另有隱情煎娇,我是刑警寧澤二庵,帶...
    沈念sama閱讀 34,329評論 4 330
  • 正文 年R本政府宣布,位于F島的核電站缓呛,受9級特大地震影響催享,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜哟绊,卻給世界環(huán)境...
    茶點故事閱讀 39,936評論 3 313
  • 文/蒙蒙 一因妙、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧票髓,春花似錦攀涵、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,757評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至玲躯,卻和暖如春据德,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背跷车。 一陣腳步聲響...
    開封第一講書人閱讀 31,991評論 1 266
  • 我被黑心中介騙來泰國打工棘利, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人朽缴。 一個月前我還...
    沈念sama閱讀 46,370評論 2 360
  • 正文 我出身青樓善玫,卻偏偏與公主長得像,于是被迫代替她去往敵國和親密强。 傳聞我的和親對象是個殘疾皇子茅郎,可洞房花燭夜當晚...
    茶點故事閱讀 43,527評論 2 349

推薦閱讀更多精彩內(nèi)容