源码分析(十):资源的构建
生成 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,同步及含有 runtime 的 chunk 对应 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();即执行对应 template 的 render。
chunkTemplate
生成主体 chunk 代码
如果是异步 chunk,render 会执行在文件 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里,会根据依赖不同做相应的源码转化的处理。但方法里并没有直接执行源码转化的工作,而是将其转化对象 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": "", // 名称
"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 代码 chunkTemplate 的 fileManifest.render 代码构建完成。
mainTemplate
如果是同步 chunk,render 会执行在文件 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,该 hook 在 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 代码
这里来看 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 代码 mainTemplate 的 fileManifest.render 代码构建完成。
文件名映射资源
无论是同步还是异步,最后都回到 Compilation.js 的 createChunkAssets 里,做了 source 缓存,然后执行:
this.emitAsset(file, source, assetInfo);建立起了文件名与对应源码的联系,将该映射对象挂载到 compilation.assets 下。 然后设置了 alreadyWrittenFiles 这个 Map 对象,防止重复构建代码。到此一个 chunk 的资源构建结束。
chunk 遍历结束后,得到 compilation.assets 和 compilation.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
}
}
}
}本章小结
- 通过
this.emitFile可生成module资源,如果有则直接调用this.emitAsset生成资源; - 生成
chunk资源时,先根据是否含有runtime得到不同的template,包括chunkTemplate和mainTemplate; - 通过不同的
template得到不同的manifest和pathAndInfo,然后调用不同的render渲染代码; - 最后建立文件名与资源之间的映射,最终一起挂载到
compilation.assets即目标资源。