浅析 webpack 打包流程(原理) 五 - 构建资源

接上文:浅析 webpack 打包流程(原理) 四 - chunk 优化

七、构建资源

本阶段概述:
1.获取 compilation 每个 module 的 buildInfo.assets,然后调用 this.emitAsset 生成 module 资源;
2.遍历compilation.chunks生成 chunk 资源时,先根据是否含有 runtime (webpackBootstrap 代码) 选择不同的 template (有则 mainTemplate,不然一概 chunkTemplate),得到各自的 manifest 数据pathAndInfo,然后调用不同的 render 渲染代码;
3.最后建立文件名与资源之间的映射,并将得到的所有目标资源信息挂载到compilation.assets上。
4.如果有配置诸如terser-webpack-plugin的代码压缩插件(一般都有),在optimizeChunkAssets钩子触发后,对生成的资源根据seal阶段(生成chunk之前)做的标记进行 treeshaking。

7.1 生成 module 资源

继续执行:

// /lib/Compilation.js
this.hooks.beforeModuleAssets.call();
this.createModuleAssets();

createModuleAssets 方法获取每个 module 的 buildInfo.assets,然后触发compilation.emitAsset生成 module 资源,得到的相关数据存储在compilationassetsassetsInfo。buildInfo.assets 相关数据可在 loader 里调用 Api: this.emitFile 来生成 (loaderContext.emitFile 方法,详见/lib/NormalModule.js)。

7.2 生成 chunk 资源

7.2.1 生成前的准备

得到 manifest 数据对象

当 compiler 处理、解析和映射应用代码时,manifest 会记录每个模块的详细要点(如 module identifier、路径等)。程序运行时,runtime 会根据 manifest 中的数据来解析和加载模块:__webpack_require__方法接收模块标识符(identifier)作为参数,简称 moduleId,通过这个标识符可以检索出 manifest 集合中对应的模块。

// /lib/Compilation.js
this.hooks.beforeChunkAssets.call();
this.createChunkAssets();

createChunkAssets 方法循环对每个 chunk 执行:

// /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 数据对象
    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 了(无论同步异步)。

然后执行对应的 getRenderManifest,触发template.hooks:renderManifest,执行插件 JavascriptModulesPlugin 相关事件:
(如果有配置 MiniCssExtractPlugin 插件,也会在此时执行从当前 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 还会触发插件 WebAssemblyModulesPlugin 去处理 WebAssembly 相关。

得到 pathAndInfo

然后遍历 manifest 数组,在里面执行:

// /lib/Compilation.js
const pathAndInfo = this.getPathWithInfo(
  filenameTemplate,
  fileManifest.pathOptions
);

getPathWithInfo 用于得到路径和相关信息,会触发mainTemplate.hooks: assetPath,去执行插件 TemplatedPathPlugin 相关事件,使用若干 replace 将如[name].[chunkhash:8].js替换为0.c7687fbe.js

7.2.2 构建资源

然后判断有无 source 缓存,若无则执行:source = fileManifest.render();
即执行 manifest 每一项里的 render 函数。

(1) chunkTemplate
生成主体 chunk 代码

如果是普通 chunk,render 会调用 JavascriptModulesPlugin.js 插件里的renderJavascript方法:先执行 Template.renderChunkModules 静态方法:

// /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 方法,循环对每一个 module 执行 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 方法,内部执行:const source = this.generator.generate(this, dependencyTemplates, runtimeTemplate, type);
这个 generator 就是在 reslove 流程 ➡️ getGenerator 所获得,即在/lib/JavascriptGenerator.js执行:
this.sourceBlock(module, module, [], dependencyTemplates, source, runtimeTemplate);

这里循环处理 module 的每个依赖(module.dependencies),获得依赖所对应的 template 模板类,然后执行该类的 apply:

// /lib/JavascriptGenerator.js
const template = dependencyTemplates.get(dependency.constructor);
template.apply(dependency, source, runtimeTemplate, dependencyTemplates);

这里的 dependencyTemplates 就是在【浅析 webpack 打包流程(原理) 一 - 准备工作】 ➡️ 实例化 compilation 时添加的依赖模板模块。
template.apply会根据依赖的不同做相应的源码转化处理。但方法里并没有直接执行源码转化,而是将其转化对象 push 到ReplaceSource.replacements里,转化对象的格式为:

注:webpack-sources 提供若干类型的 source 类,如 CachedSource、PrefixSource、ConcatSource、ReplaceSource 等。它们可以组合使用,方便对代码进行添加、替换、连接等操作。同时又含有一些 source-map 相关、updateHash 等 Api 供 webpack 内部调用。

// Replacement
{
  "content": "__webpack_require__.r(__webpack_exports__);\n", // 替换的内容
  "end": -11, // 替换源码的终止位置
  "insertIndex": 0, // 优先级
  "name": undefined, // 名称
  "start": -10 // 替换源码的起始位置
}

各模版的转化处理见【浅析 webpack 打包流程(原理) 二 - 递归构建 module】 最末:各依赖作用简单解释。

包裹代码

收集完依赖相关的转化对象 Replacement 后,对得到的结果进行cachedSource缓存包装,回到 ModuleTemplate.js 的 render 方法得到 moduleSource。
然后触发ModuleTemplate.hooks:content、module、render、package,content、module 钩子主要是可以让我们完成对 module 源码的再次处理;然后在 render 钩子里执行插件 FunctionModuleTemplatePlugin 的订阅事件:对处理后的 module 源码进行包裹,即生成代码:

/***/
(function (module, __webpack_exports__, __webpack_require__) {
  'use strict';
  // children 数组中的 CachedSource 即为`module`源码,里面包含 replacements
  /***/
});
拿到的 moduleSourcePostRender 值
添加注释

再触发 package 钩子执行插件 FunctionModuleTemplatePlugin 的订阅事件,主要是添加相关注释,即生成代码:

/*!***************************************************************!*\
  !*** ./src/c.js ***!
  \***************************************************************/
/*! exports provided: sub */
/***/
(function (module, __webpack_exports__, __webpack_require__) {
  'use strict';
  // children 数组中的 CachedSource 即为`module`源码,里面包含 replacements
  /***/
});

将所有的 module 都处理完毕后,回到 Template.js 的 renderChunkModules 继续处理生成代码,最终将每个 module 生成的代码串起来回到 JavascriptModulesPlugin.js 的 renderJavascript 方法里得到了 moduleSources。

生成 jsonp 包裹代码

接着触发chunkTemplate.hooks: modules,为修改生成的 chunk 代码提供钩子。得到 core 后,触发chunkTemplate.hooks: render执行插件 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 会执行 JavascriptModulesPlugin.js 里的 compilation.mainTemplate.render 即/lib/MainTemplate.js的 render 方法。

生成 runtime 代码

内部执行:const buf = this.renderBootstrap(hash, chunk, moduleTemplate, dependencyTemplates);
得到 webpack runtime bootstrap 代码数组,过程中会判断 chunks 中是否存在异步 chunk,如果有,则代码里还会包含异步相关的 runtime 代码。如果还有延迟加载的同步 chunk,都会在这里处理为相应的 runtime。

包裹 runtime 与 chunk 代码

然后执行:

let source = this.hooks.render.call(
  new OriginalSource(
    Template.prefix(buf, " \t") + "\n",
    "webpack/bootstrap"
  ),
  chunk,
  hash,
  moduleTemplate,
  dependencyTemplates
);

先通过 Template.js 的 prefix 方法合并 runtime 代码字符串,得到 OriginalSource 实例,然后将其作为参数执行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 代码),过程中触发MainTemplate.hooks: modules得到 chunk 的生成代码,即最终返回一个包含 runtime 代码和 chunk 代码的 ConcatSource 实例。

生成 chunk 代码

这里来看通过this.hooks.modules.call()钩子得到 chunk 生成代码的实现:
触发插件 JavascriptModulesPlugin 的注册事件,即执行 Template 类的静态方法renderChunkModules。与前文 chunkTemplate ➡️ 生成主体 chunk 代码的实现一致。

最终经过包裹后得到的代码大致如下:

"/******/ (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 缓存,然后执行:

this.emitAsset(file, source, assetInfo);
chunk.files.push(file);

建立起文件名与对应源码的联系,以this.assets[file] = source的形式将该映射对象挂载到 compilation.assets 下。 把文件名称存入对应的 chunk.files 数组中,即compilation.chunks下。然后设置了 alreadyWrittenFiles (Map 对象),以防重复构建代码。至此一个 chunk 的资源构建结束。

所有 chunk 遍历结束后,得到的compilation.assetscompilation.assetsInfo:

// compilation
{
  //...
  "assets": {
    "0.92bfd615.js": CachedSource, // CachedSource 里包含 chunk 资源
    "index.1d678a.js": CachedSource
  },
  "assetsInfo": { // Map结构
    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钩子触发后,如果有配置进一步处理生成资源的插件,则会对资源再度优化。
比如compilation.hooks.optimizeChunkAssets会触发terser-webpack-plugin代码压缩插件(一般都会配置,webpack 5 内置),对生成的 chunk 资源根据(seal阶段compilation.hooks.optimizeDependencies)生成的unused harmony export标记等信息进行 treeshaking

下文:浅析 webpack 打包流程(原理) 六 - 生成文件

webpack 打包流程系列(未完):
浅析 webpack 打包流程(原理) - 案例 demo
浅析 webpack 打包流程(原理) 一 - 准备工作
浅析 webpack 打包流程(原理) 二 - 递归构建 module
浅析 webpack 打包流程(原理) 三 - 生成 chunk
浅析 webpack 打包流程(原理) 四 - chunk 优化
浅析 webpack 打包流程(原理) 五 - 构建资源
浅析 webpack 打包流程(原理) 六 - 生成文件

参考鸣谢:
webpack打包原理 ? 看完这篇你就懂了 !
webpack 透视——提高工程化(原理篇)
webpack 透视——提高工程化(实践篇)
webpack 4 源码主流程分析
[万字总结] 一文吃透 Webpack 核心原理
关于Webpack详述系列文章 (第三篇)

你可能感兴趣的:(浅析 webpack 打包流程(原理) 五 - 构建资源)