源码分析(十):资源的构建
生成 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
即目标资源。