阅读 116

webpack 4 源码主流程分析(十):资源的构建

原文首发于 blog.flqin.com。如有错误,请联系笔者。分析码字不易,转载请表明出处,谢谢!

生成 module 资源

接上文,执行:

this.hooks.beforeModuleAssets.call();
this.createModuleAssets();
复制代码

这一步用于生成 module 资源。在 createModuleAssets 里,获取每个 module 属性上的 buildInfo.assets,然后触发 this.emitAsset 生成资源。buildInfo.assets 相关数据可以在 loader 里调用 api: this.emitFile 生成。

生成 chunk 资源

这一步用于创建 chunk 资源。

生成前的准备

manifest

执行:

this.hooks.beforeChunkAssets.call();
this.createChunkAssets();
复制代码

createChunkAssets 里循环对每个 chunk 执行:

//...
const template = chunk.hasRuntime() ? this.mainTemplate : this.chunkTemplate;
const manifest = template.getRenderManifest({
  chunk,
  hash: this.hash,
  fullHash: this.fullHash,
  outputOptions,
  moduleTemplates: this.moduleTemplates,
  dependencyTemplates: this.dependencyTemplates
}); //得到 `render` 所需要的全部信息:`[{ render(), filenameTemplate, pathOptions, identifier, hash }]`
//...
复制代码

在判断 chunk 是否含有 runtime 代码后即同步异步后,获取到对应的 template。异步 chunk 对应 chunkTemplate,同步及含有 runtimechunk 对应 mainTemplate

然后执行对应的 getRenderManifest,触发 template.hooks:renderManifest 执行插件 JavascriptModulesPlugin 相关事件得到 render 所需要的全部信息:[{ render(), filenameTemplate, pathOptions, identifier, hash }]

如果是 chunkTemplate 还会触发插件 WebAssemblyModulesPlugin 的相关事件处理 WebAssembly 相关。

pathAndInfo

然后遍历 manifest 对象执行:

const pathAndInfo = this.getPathWithInfo(filenameTemplate, fileManifest.pathOptions);
复制代码

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

构建资源

然后判断有无 source 缓存后,若无则执行:

source = fileManifest.render();
复制代码

即执行对应 templaterender

chunkTemplate

生成主体 chunk 代码

如果是异步 chunkrender 会执行在文件 JavascriptModulesPlugin.js 里的 renderJavascript。方法里先执行 Template.renderChunkModules 静态方法:

const moduleSources = Template.renderChunkModules(chunk, m => typeof m.source === 'function', moduleTemplate, dependencyTemplates);
复制代码
生成每个 module 代码

方法里执行:

const allModules = modules.map(module => {
  return {
    id: module.id,
    source: moduleTemplate.render(module, dependencyTemplates, {
      chunk
    })
  };
});
复制代码

这里循环对每一个 module 执行 render,方法里执行:

const moduleSource = module.source(dependencyTemplates, this.runtimeTemplate, this.type);
//...
复制代码

module.source里执行:

const source = this.generator.generate(this, dependencyTemplates, runtimeTemplate, type);
复制代码

这个 generator 就是在 reslove 流程 -> getGenerator 所获得,即执行:

this.sourceBlock(module, module, [], dependencyTemplates, source, runtimeTemplate);
复制代码

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

const template = dependencyTemplates.get(dependency.constructor);
//...
template.apply(dependency, source, runtimeTemplate, dependencyTemplates);
复制代码

这里的 dependencyTemplates 就是在 reslove 流程前的准备 ->Compiler.compile ->实例化 compilation 里添加的依赖模板模块。

apply里,会根据依赖不同做相应的源码转化的处理。但方法里并没有直接执行源码转化的工作,而是将其转化对象 pushReplaceSource.replacements 里,转化对象的格式为:

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

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

各模板的具体处理转化见 构建 module(下) -> parse 源码 -> 各依赖作用解释

包裹代码

收集完依赖相关的转化对象 Replacement 之后,回到 module.source 进行 cachedSource 缓存包装后,回到 moduleTemplate.render 方法得到 moduleSource

然后触发相关 ModuleTemplate.hooks:content,module,render,package,前两个钩子主要是可以让我们完成对 module 源码的再次处理,然后在 render 钩子里执行插件 FunctionModuleTemplatePlugin 的相关事件,主要是给处理后的 module 源码进行包裹,即生成代码:

/***/
(function(module, __webpack_exports__, __webpack_require__) {
  'use strict';
  //CachedSource 即为module源码,里面包含 replacements
  /***/
});
复制代码
添加注释

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

/*!***************************************************************!*\
  !*** ./src/c.js ***!
  \***************************************************************/
/*! exports provided: sub */
/***/
(function(module, __webpack_exports__, __webpack_require__) {
  'use strict';
  //CachedSource 即为module源码,里面包含 replacements
  /***/
});
复制代码

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

生成异步包裹代码

方法里先触发 chunkTemplate.hooks : modules 为修改生成的 chunk 代码提供钩子,得到 core 后,触发 chunkTemplate.hooks:render 执行插件 JsonpChunkTemplatePlugin 相关事件,该事件主要是添加 jsonp 异步包裹代码,得到:

(window['webpackJsonp'] = window['webpackJsonp'] || []).push([
  [0]
  // 前面生成的 chunk 代码
]);
复制代码

完成后,最后返回一个 new ConcatSource(source, ";")。到此普通的异步 chunk 代码 chunkTemplatefileManifest.render 代码构建完成。

mainTemplate

如果是同步 chunkrender 会执行在文件 JavascriptModulesPlugin.js 里的 compilation.mainTemplate.render 即文件 MainTemplate.js 里的 render

生成 runtime 代码

方法里执行:

const buf = this.renderBootstrap(hash, chunk, moduleTemplate, dependencyTemplates);
复制代码

该方法得到 webpack runtime bootstrap 代码数组,从中会判断是否有异步 chunk,如果有,则代码里还会包含异步相关的 runtime 代码,如果还有其他什么延迟加载的模块,都会在这里处理为相应是 runtime

包裹 runtime 与 chunk 代码

然后执行:

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

先通过 Template.prefix 合并 runtime 代码字符串,得到 OriginalSource 的实例,然后将其作为参数执行 MainTemplate.hooks : render,该 hookconstructor 里已注册,代码如下:

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 代码

这里来看 chunk 代码的实现,如上文代码中:

this.hooks.modules.call(new RawSource(''), chunk, hash, moduleTemplate, dependencyTemplates);
复制代码

这里 mainTemplate.hooks: modules 触发插件 JavascriptModulesPlugin 的相关事件,即执行 Template 类的静态方法 renderChunkModules。与前文 chunkTemplate -> 生成主体 chunk 代码 的实现一致。

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

"/******/ (function(modules) { // webpackBootstrap
// runtime 代码的 PrefixSource 实例
/******/ })
/************************************************************************/
/******/ ({

/***/ "../github/test-loader/loader.js?number=20!./src/d.js":
/*!************************************************************!*\
  !*** ../github/test-loader/loader.js?number=20!./src/d.js ***!
  \************************************************************/
/*! exports provided: mul */
/***/ (function(module, __webpack_exports__, __webpack_require__) {

"use strict";
// module d 的 CachedSource 实例

/***/ }),

/***/ "./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, addddd */
/***/ (function(module, __webpack_exports__, __webpack_require__) {

"use strict";
// module b 的 CachedSource 实例

/***/ })

/******/ })"
复制代码

完成后,最后返回一个 new ConcatSource(source, ";")。到此普通的同步 chunk 代码 mainTemplatefileManifest.render 代码构建完成。

文件名映射资源

无论是同步还是一部,最后都回到 Compilation.jscreateChunkAssets 里,做了 source 缓存,然后执行:

this.emitAsset(file, source, assetInfo);
复制代码

建立起了文件名与对应源码的联系,将该映射对象挂载到 compilation.assets 下。 然后设置了 alreadyWrittenFiles 这个 Map 对象,防止重复构建代码。到此一个 chunk 的资源构建结束。

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

//compilation
{
  //...
  "assets": {
    "0.3e.js": CachedSource, // CachedSource 里包含资源
    "bundle.bf23.js": CachedSource
  },
  //...map结构
  "assetsInfo": {
    0: {
      "key": '0.3e.js',
      "value": {
        immutable:true
      }
    },
    1: {
      "key": 'bundle.bf23.js',
      "value": {
        immutable:true
      }
    }
  }
}
复制代码

本章小结

  1. 通过 this.emitFile 可生成 module 资源,如果有则直接调用 this.emitAsset 生成资源;
  2. 生成 chunk 资源时,先根据是否含有 runtime 得到不同的 template,包括 chunkTemplatemainTemplate;
  3. 通过不同的 template 得到不同的 manifestpathAndInfo,然后调用不同的 render 渲染代码;
  4. 最后建立文件名与资源之间的映射,最终一起挂载到 compilation.assets 即目标资源。