做者:崔静、肖磊javascript
通过前几篇文章咱们介绍了 webpack 如何从配置文件的入口开始,将每个文件转变为内部的 module,而后再由 module 整合成一个一个的 chunk。这篇文章咱们来看一下最后一步 —— chunk 如何转变为最终的 js 文件。html
上篇文章主要是梳理了在 seal 阶段的开始, webpack 内部是如何将有依赖关系的 module 统一组织到一个 chunk 当中的。如今继续来看 seal 阶段,chunk 生成以后的部分,咱们从 optimizeTree.callAsync
看起java
seal(callback) {
// 优化 dependence 的 hook
// 生成 chunk
// 优化 modules 的 hook,提供给插件修改 modules 的能力
// 优化 chunk 的 hook,提供给插件修改 chunk 的能力
this.hooks.optimizeTree.callAsync(this.chunks, this.modules, err => {
//... 优化 chunk 和 module
//... record 为记录相关的,不是主流程,这里先忽略
//... 优化顺序
// 生成 module id
this.hooks.beforeModuleIds.call(this.modules);
this.hooks.moduleIds.call(this.modules);
this.applyModuleIds();
//... optimize
// 排序
this.sortItemsWithModuleIds();
// 生成 chunk id
//...
this.hooks.optimizeChunkOrder.call(this.chunks);
this.hooks.beforeChunkIds.call(this.chunks);
this.applyChunkIds();
//... optimize
// 排序
this.sortItemsWithChunkIds();
//...省略 recode 相关代码
// 生成 hash
this.hooks.beforeHash.call();
this.createHash();
this.hooks.afterHash.call();
//...
// 生成最终输出静态文件的内容
this.hooks.beforeModuleAssets.call();
this.createModuleAssets();
if (this.hooks.shouldGenerateChunkAssets.call() !== false) {
this.hooks.beforeChunkAssets.call();
this.createChunkAssets();
}
this.hooks.additionalChunkAssets.call(this.chunks);
this.summarizeDependencies();
//...
// 增长 webpack 须要的额外代码
this.hooks.additionalAssets.callAsync(err => {
//...
});
});
}
复制代码
上面代码中,按照从上往下的顺序依次看,经历的主流程以下:node
主要步骤为:生成 moduleId,生成 chunkId,生成 hash,而后生成最终输出文件的内容,同时每一步之间都会暴露 hook , 提供给插件修改的机会。接下来咱们一一看一下核心逻辑:id 生成,hash 生成,文件内容生成webpack
webpack 会对 module 和 chunk 分别生成id,这两者在逻辑上基本相同。咱们先以 module id 为例来看 id 生成的过程(在 webpack 为 module 生成 id 的逻辑位于 applyModuleIds
方法中),代码以下git
applyModuleIds() {
const unusedIds = [];
let nextFreeModuleId = 0;
const usedIds = new Set();
if (this.usedModuleIds) {
for (const id of this.usedModuleIds) {
usedIds.add(id);
}
}
const modules1 = this.modules;
for (let indexModule1 = 0; indexModule1 < modules1.length; indexModule1++) {
const module1 = modules1[indexModule1];
if (module1.id !== null) {
usedIds.add(module1.id);
}
}
if (usedIds.size > 0) {
let usedIdMax = -1;
for (const usedIdKey of usedIds) {
if (typeof usedIdKey !== "number") {
continue;
}
usedIdMax = Math.max(usedIdMax, usedIdKey);
}
let lengthFreeModules = (nextFreeModuleId = usedIdMax + 1);
while (lengthFreeModules--) {
if (!usedIds.has(lengthFreeModules)) {
unusedIds.push(lengthFreeModules);
}
}
}
// 为 module 设置 id
const modules2 = this.modules;
for (let indexModule2 = 0; indexModule2 < modules2.length; indexModule2++) {
const module2 = modules2[indexModule2];
if (module2.id === null) {
if (unusedIds.length > 0) module2.id = unusedIds.pop();
else module2.id = nextFreeModuleId++;
}
}
}
复制代码
能够看到设置 id 的流程主要分两步:github
找到当前未使用的 id 和 已经使用的最大的 id。举个例子:若是已经使用的 id 是 [3, 6, 7 ,8]
,那么通过第一步处理后,nextFreeModuleId = 9
, unusedIds = [0, 1, 2, 4, 5]
。web
给没有 id 的 module 设置 id。设置 id 时,优先使用 unusedIds 中的值。json
在设置 id 的时候,有一个判断 module2.id === null
,也就是说若在这一步以前,已经被设置过 id 值,那么这里便直接忽略。在设置 id 以前,会触发两个钩子:bootstrap
this.hooks.beforeModuleIds.call(this.modules);
this.hooks.moduleIds.call(this.modules);
复制代码
咱们可在这两个钩子中,操做 module,设置本身的 id。webpack 内部 NamedModulesPlugin 就是注册在 beforeModuleIds
钩子上,将 module 的相对路径设置为 id。在开发环境下,便于咱们调试和分析代码,webpack 默认会使用这个插件。
设置完 id 以后,会对 this.modules 中的 module 和 chunks 中的 module 按照 id 来排序。同时还会对 module 中的 reason 和 usedExports 排序。
chunk id 的生成逻辑与 module id 相似,一样的,在设置完 id 后,按照 id 进行排序。
在 webpack 生成最后文件的时候,咱们常常会设置文件名称为 [name].[hash].js
的模式,给文件名称增长一个 hash 值。凭着直觉,这里的 hash 值和文件内容相关,可是具体是怎么来的呢?答案就位于 Compilation.js 的 createHash
方法中:
createHash() {
const outputOptions = this.outputOptions;
const hashFunction = outputOptions.hashFunction;
const hashDigest = outputOptions.hashDigest;
const hashDigestLength = outputOptions.hashDigestLength;
const hash = createHash(hashFunction);
//... update hash
// module hash
const modules = this.modules;
for (let i = 0; i < modules.length; i++) {
const module = modules[i];
const moduleHash = createHash(hashFunction);
module.updateHash(moduleHash);
module.hash = moduleHash.digest(hashDigest);
module.renderedHash = module.hash.substr(0, hashDigestLength);
}
// clone needed as sort below is inplace mutation
const chunks = this.chunks.slice();
/** * sort here will bring all "falsy" values to the beginning * this is needed as the "hasRuntime()" chunks are dependent on the * hashes of the non-runtime chunks. */
chunks.sort((a, b) => {
const aEntry = a.hasRuntime();
const bEntry = b.hasRuntime();
if (aEntry && !bEntry) return 1;
if (!aEntry && bEntry) return -1;
return byId(a, b);
});
// chunck hash
for (let i = 0; i < chunks.length; i++) {
const chunk = chunks[i];
const chunkHash = createHash(hashFunction);
if (outputOptions.hashSalt) chunkHash.update(outputOptions.hashSalt);
chunk.updateHash(chunkHash);
const template = chunk.hasRuntime()
? this.mainTemplate
: this.chunkTemplate;
template.updateHashForChunk(chunkHash, chunk);
this.hooks.chunkHash.call(chunk, chunkHash);
chunk.hash = chunkHash.digest(hashDigest);
hash.update(chunk.hash);
chunk.renderedHash = chunk.hash.substr(0, hashDigestLength);
this.hooks.contentHash.call(chunk);
}
this.fullHash = hash.digest(hashDigest);
this.hash = this.fullHash.substr(0, hashDigestLength);
}
复制代码
主结构其实就是两部分:
webpack 中计算 hash 值底层所使用的是 Node.js 中的 crypto, 主要用到了两个方法:
hash.update
能够简单认为是增长用于生成 hash 的原始内容(如下统一简称为 hash 源)digest
方法用来获得最终 hash 值。下面咱们先看 module hash 生成过程。
module hash 生成的代码逻辑以下:
createHash() {
//...省略其余逻辑
const modules = this.modules;
for (let i = 0; i < modules.length; i++) {
const module = modules[i];
const moduleHash = createHash(hashFunction);
module.updateHash(moduleHash);
module.hash = moduleHash.digest(hashDigest);
module.renderedHash = module.hash.substr(0, hashDigestLength);
}
//...省略其余逻辑
}
复制代码
其中关键的 updateHash
方法,封装在每一个 module 类的实现中,调用关系以下:
上面图能够看到,module hash 内容包括:
每一个 module 中本身特有的须要写入 hash 中的信息
而对于 NormalModule 来讲,这个方法具体为:
updateHash(hash) {
this.updateHashWithSource(hash);
this.updateHashWithMeta(hash);
super.updateHash(hash);
}
复制代码
也就说会包含 souce 内容和生成文件相关的元信息 buildMeta。
module id 和 被使用到的 exports 信息
依赖的信息
各个依赖具体有哪些信息要写入 hash 源,由 xxxDependency.js 中 updateHash
方法决定。例以下面的代码
// 打包的入口 main.js
import { A } from './a.js'
import B from './b.js'
import 'test-module'
console.log(A)
B()
复制代码
转化为 module 后(module 生成的流程请回忆webpack系列之五module生成2),三个 import
会获得三个 HarmonyImportSideEffectDependency
。这里就以该依赖为例,看一下 hash 内容原始内容中写入依赖信息的过程,以下图
上面图能够看出,依赖的 module 会影响当前 module 的 hash,若是咱们修改顺序或者其余的操做形成依赖 module 的 id 改变了,那么当前 module 获得的 hash 也会改变。 因此 module 的 hash 内容不只包含了源码,还包含了和其打包构建相关的内容。由于当咱们修改了 webpack 的相关配置时,最终获得的代码颇有可能会改变,将这些会影响最终代码生成的配置写入生成 hash 的 buffer 中能够保证,当咱们仅修改 webpack 的打包配置,好比改变 module id 生成方式等,也能够获得一个 hash 值不一样的文件名。
在生成 chunk hash 以前,会先对 chunck 进行排序(为何要排序,这个问题先放一下,在咱们看完 chunk 生成以后再来解答)。 chunck hash 生成,第一步是 chunk.updateHash(chunkHash);
,具体代码以下(位于 Chunck.js 中):
updateHash(hash) {
hash.update(`${this.id} `);
hash.update(this.ids ? this.ids.join(",") : "");
hash.update(`${this.name || ""} `);
for (const m of this._modules) {
hash.update(m.hash);
}
}
复制代码
这部分逻辑很简单,将 id,ids,name 和其包含的全部 module 的 hash 信息写入。而后写入生成 chunck 的模板信息: template.updateHashForChunk(chunkHash, chunk)
。webpack 将 template 分为两种:mainTemplate 最终会生成包含 runtime 的代码 和 chunkTemplate,也就是咱们在第一篇文章里看到的经过 webpackJsonp 加载的 chunck 代码模板。
咱们主要看 mainTemplate 的 updateHashForChunk
方法
updateHashForChunk(hash, chunk) {
this.updateHash(hash);
this.hooks.hashForChunk.call(hash, chunk);
}
updateHash(hash) {
hash.update("maintemplate");
hash.update("3");
hash.update(this.outputOptions.publicPath + "");
this.hooks.hash.call(hash);
}
复制代码
这里会将 template 类型 "maintemplate" 和 咱们配置的 publicPath 写入。而后触发 的 hash 事件和 hashForChunk 事件会将一些文件的输出信息写入。例如:加载 chunck 所使用的 jsonp 方式,是经过 JsonpMainTemplatePlugin 实现的。在 hash hooks 中会触发其回调,将 jsonp 的相关信息写入 hash,例如:jsonp 回调函数的名称等。将相关信息都存入 hash 的 buffer 以后,调用 digest 方法生成最终的 hash,而后从中截取出须要的长度,chunk 的 hash 就获得了。
总的来看,chunk hash 依赖于其内部全部 module 的 hash,而且还依赖于咱们配置的各类输出 chunk 相关的信息。和 module hash 相似,这样才能保证当咱们修改了 webpack 的相关配置致使代码改变后会获得不一样的 hash 值。
到此还遗留了一个问题,为何在生成 chunk hash 时候要排序?
在 updateHashForChunk
过程当中,插件 TemplatePathPlugin 会在 hashForChunk hook 时被触发并执行一段下面的逻辑
// TemplatePathPlugin.js
mainTemplate.hooks.hashForChunk.tap(
"TemplatedPathPlugin",
(hash, chunk) => {
const outputOptions = mainTemplate.outputOptions;
const chunkFilename =
outputOptions.chunkFilename || outputOptions.filename;
// 文件名带 chunkhash
if (REGEXP_CHUNKHASH_FOR_TEST.test(chunkFilename))
hash.update(JSON.stringify(chunk.getChunkMaps(true).hash));
// 文件名带 contenthash
if (REGEXP_CONTENTHASH_FOR_TEST.test(chunkFilename)) {
hash.update(
JSON.stringify(
chunk.getChunkMaps(true).contentHash.javascript || {}
)
);
}
// 文件名带 name
if (REGEXP_NAME_FOR_TEST.test(chunkFilename))
hash.update(JSON.stringify(chunk.getChunkMaps(true).name));
}
);
复制代码
若是咱们在 webpack.config.js 中设置输出文件名称带有 chunkhash 的时候,好比: filename: [name].[chunkhash].js
,会查找当前全部 chunk 的 hash,获得一个下面的结构:
{
hash: { // chunkHashMap
0: 'chunk 0 的 hash',
...
},
name: nameHashMap,
contentHash: { // chunkContentHashMap
javascript: {
0: 'chunk 0 的 contentHash',
...
}
}
}
复制代码
而后将上面结果中 hash 内容转为字符串写入 hash buffer 中。因此说对于有 runtime 的 chunk 这一步依赖于全部不含 runtime 的 chunk 的 hash 值。所以在计算 chunk hash 以前会有一段排序的逻辑。再深刻思考一步,为何要依赖不含 runtime 的 chunk 的 hash 值呢?对于须要被异步加载的 chunk (即不含 runtime 的 chunk)在用到时会经过 script 标签加载,这时 src 中即是其文件名称,所以这个文件的名称须要被保存在含有 runtime 的 chunk 中。当文件名称包含 hash 值时,含 runtime 的 chunk 文件的内容会由于其余 chunk 的 hash 值的不一样而不一样,从而生成的 hash 值也应该随之改变。
hash 值生成以后,会调用 createChunkAssets 方法来决定最终输出到每一个 chunk 当中对应的文本内容是什么。
// Compilation.js
class Compilation extends Tapable {
...
createChunkAssets() {
for (let i = 0; i < this.chunks.length; i++) {
const chunk = this.chunks[i]
try {
const template = chunk.hasRuntime()
? this.mainTemplate
: this.chunkTemplate;
const manifest = template.getRenderManifest({
chunk,
hash: this.hash, // 此次 compilation 的 hash 值
fullHash: this.fullHash, // 此次 compilation 未被截断的 hash 值
outputOptions,
moduleTemplates: this.moduleTemplates,
dependencyTemplates: this.dependencyTemplates
}); // [{ render(), filenameTemplate, pathOptions, identifier, hash }]
for (const fileManifest of manifest) {
...
source = fileManifeset.render() // 渲染生成每一个 chunk 最终输出的代码
...
this.assets[file] = source;
...
}
}
....
}
}
...
}
复制代码
主要步骤:
在 createChunkAssets 方法内部会对最终须要输出的 chunk 进行遍历,根据这个 chunk 是否包含有 webpack runtime 代码来决定使用的渲染模板(mainTemplate/chunkTemplate)。其中 mainTemplate 主要用于包含 webpack runtime bootstrap 的 chunk 代码渲染生成工做,chunkTemplate 主要用于普通 chunk 的代码渲染工做。
mainTemplate 和 chunkTemplate 分别有本身的 getRenderManifest 方法,在这个方法中会生成 render 代码须要的全部信息,包括文件名称格式、对应的 render 函数,哈希值等。
咱们首先来看下包含有 webpack runtime 代码的 chunk 是如何输出最终的 chunk 文本内容的。
这种状况下使用的 mainTemplate,调用实例上的 getRenderManifest 方法获取 manifest 配置数组,其中每项包含的字段内容为:
// MainTemplate.js
class MainTemplate extends Tapable {
...
getRenderManifest(options) {
const result = [];
this.hooks.renderManifest.call(result, options);
return result;
}
...
}
复制代码
接下来会判断这个 chunk 是否有被以前已经输出过(输出过的 chunk 是会被缓存起来的)。若是没有的话,那么就会调用 render 方法去完成这个 chunk 的文本输出工做,即:compilation.mainTemplate.render
方法。
// MainTemplate.js
module.exports = class MainTemplate extends Tapable {
...
constructor() {
// 注册 render 钩子函数
this.hooks.render.tap(
"MainTemplate",
(bootstrapSource, chunk, hash, moduleTemplate, dependencyTemplates) => {
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(
// 调用 modules 钩子函数,用以渲染 runtime chunk 当中所须要被渲染的 module
this.hooks.modules.call(
new RawSource(""),
chunk,
hash,
moduleTemplate,
dependencyTemplates
)
);
source.add(")");
return source;
}
);
}
...
/** * @param {string} hash hash to be used for render call * @param {Chunk} chunk Chunk instance * @param {ModuleTemplate} moduleTemplate ModuleTemplate instance for render * @param {Map<Function, DependencyTemplate>} dependencyTemplates dependency templates * @returns {ConcatSource} the newly generated source from rendering */
render(hash, chunk, moduleTemplate, dependencyTemplates) {
// 生成 webpack runtime bootstrap 代码
const buf = this.renderBootstrap(
hash,
chunk,
moduleTemplate,
dependencyTemplates
);
// 调用 render 钩子函数
let source = this.hooks.render.call(
new OriginalSource(
Template.prefix(buf, " \t") + "\n",
"webpack/bootstrap"
),
chunk,
hash,
moduleTemplate,
dependencyTemplates
);
if (chunk.hasEntryModule()) {
source = this.hooks.renderWithEntry.call(source, chunk, hash);
}
if (!source) {
throw new Error(
"Compiler error: MainTemplate plugin 'render' should return something"
);
}
chunk.rendered = true;
return new ConcatSource(source, ";");
}
...
}
复制代码
这个方法内部首先调用 renderBootstrap 方法完成 webpack runtime bootstrap 代码的拼接工做,接下来调用 render hook,这个 render hook 是在 MainTemplate 的构造函数里面就完成了注册。 咱们能够看到这个 hook 内部,主要是在 runtime bootstrap 代码外面完成了一层包装,而后调用 modules hook 开始进行这个 runtime chunk 当中须要渲染的 module 的生成工做(具体每一个 module 如何去完成代码的拼接渲染工做后文会讲)。 render hook 调用完后,即获得了包含 webpack runtime bootstrap 代码的 chunk 代码,最终返回一个 ConcatSource 类型实例。 简化一下,大概以下图:
最终的代码会被保存在一个 ConcatSource 类的 children 中,而每一个 module 的最终代码在一个 ReplaceSource 的类中,这个类包含一个 replacements 的数组,里面存放了对源码转化的操做,数组中每一个元素结构以下:
[替换源码的起始位置,替换源码的终止位置,替换的最终内容,优先级]
复制代码
也就是说在 render 的过程当中,其实不会真的去改变源码字符串,而是将要更改的内容保存在了一个数组中,在最后输出静态文件的时候根据这个数组和源码来生成最终代码。这样保证了整个过程当中,咱们能够追溯对源码作了那些改变,而且在一些 hook 中,咱们能够灵活的修改这些操做。
runtime chunk
webpack config 提供了一个代码优化配置选项:是否将 runtime chunk 单独抽离成一个 chunk 并输出到最终的文件当中。这也决定了最终在 render hook 生成 runtime chunk 代码时最终所包含的内容。首先咱们来看下相关配置信息:
// webpack.config.js
module.exports = {
...
optimization: {
runtimeChunk: {
name: 'bundle'
}
}
...
}
复制代码
经过进行 optimization 字段的配置,能够出发 RuntimeChunkPlugin 插件的注册相关的事件。
module.exports = class RuntimeChunkPlugin {
constructor(options) {
this.options = Object.assign(
{
name: entrypoint => `runtime~${entrypoint.name}`
},
options
);
}
apply(compiler) {
compiler.hooks.thisCompilation.tap("RuntimeChunkPlugin", compilation => {
// 在 seal 阶段,生成最终的 chunk graph 后触发这个钩子函数,用以生成新的 runtime chunk
compilation.hooks.optimizeChunksAdvanced.tap("RuntimeChunkPlugin", () => {
// 遍历全部的 entrypoints(chunkGroup)
for (const entrypoint of compilation.entrypoints.values()) {
// 获取每一个 entrypoints 的 runtimeChunk(chunk)
const chunk = entrypoint.getRuntimeChunk();
// 最终须要生成的 runtimeChunk 的文件名
let name = this.options.name;
if (typeof name === "function") {
name = name(entrypoint);
}
if (
chunk.getNumberOfModules() > 0 ||
!chunk.preventIntegration ||
chunk.name !== name
) {
// 新建一个 runtime 的 chunk,在 compilation.chunks 中也会新增这一个 chunk。
// 这样在最终生成的 chunk 当中会包含一个 runtime chunk
const newChunk = compilation.addChunk(name);
newChunk.preventIntegration = true;
// 将这个新的 chunk 添加至 entrypoint(chunk) 当中,那么 entrypoint 也就多了一个新的 chunk
entrypoint.unshiftChunk(newChunk);
newChunk.addGroup(entrypoint);
// 将这个新生成的 chunk 设置为这个 entrypoint 的 runtimeChunk
entrypoint.setRuntimeChunk(newChunk);
}
}
});
});
}
};
复制代码
这样便经过 RuntimeChunkPlugin 这个插件将 webpack runtime bootstrap 单独抽离至一个 chunk 当中输出。最终这个 runtime chunk 仅仅只包含了 webpack bootstrap 相关的代码,不会包含其余须要输出的 module 代码。固然,若是你不想将 runtime chunk 单独抽离出来,那么这部分 runtime 代码最终会被打包进入到包含 runtime chunk 的 chunk 当中,这个 chunk 最终输出文件内容就不只仅须要包含这个 chunk 当中依赖的不一样 module 的最终代码,同时也须要包含 webpack bootstrap 代码。
var window = window || {}
// webpackBootstrap
(function(modules) {
// 包含了 webpack bootstrap 的代码
})([
/* 0 */ // module 0 的最终代码
(function(module, __webpack_exports__, __webpack_require__) {
}),
/* 1 */ // module 1 的最终代码
(function(module, __webpack_exports__, __webpack_require__) {
})
])
module.exports = window['webpackJsonp']
复制代码
以上就是有关使用 MainTemplate 去渲染完成 runtime chunk 的有关内容。
接下来咱们看下不包含 webpack runtime 代码的 chunk (使用 chunkTemplate 渲染模板)是如何输出获得最终的内容的。
首先调用 ChunkTemplate 类上提供的 getRenderManifest 方法来获取 chunk manifest 相关的内容。
// ChunkTemplate.js
class ChunkTemplate {
...
getRenderManifest(options) {
const result = []
// 触发 ChunkTemplate renderManifest 钩子函数
this.hooks.renderManifest.call(result, options)
return result
}
...
}
// JavascriptModulesPlugin.js
class JavascriptModulesPlugin {
apply(compiler) {
compiler.hooks.compilation.tap('JavascriptModulesPlugin', (compilation, { normalModuleFactory }) => {
...
// ChunkTemplate hooks.manifest 钩子函数
compilation.chunkTemplate.hooks.renderManifest.tap('JavascriptModulesPlugin', (result, options) => {
...
result.push({
render: () =>
// 每一个 chunk 代码的生成即调用 JavascriptModulesPlugin 提供的 renderJavascript 方法来进行生成
this.renderJavascript(
compilation.chunkTemplate, // chunk模板
chunk, // 须要生成的 chunk 实例
moduleTemplates.javascript, // 模块类型
dependencyTemplates // 不一样依赖所对应的渲染模板
),
filenameTemplate,
pathOptions: {
chunk,
contentHashType: 'javascript'
},
identifier: `chunk${chunk.id}`,
hash: chunk.hash
})
...
})
...
})
}
renderJavascript(chunkTemplate, chunk, moduleTemplate, dependencyTemplates) {
const moduleSources = Template.renderChunkModules(
chunk,
m => typeof m.source === "function",
moduleTemplate,
dependencyTemplates
)
const core = chunkTemplate.hooks.modules.call(
moduleSources,
chunk,
moduleTemplate,
dependencyTemplates
)
let source = chunkTemplate.hooks.render.call(
core,
chunk,
moduleTemplate,
dependencyTemplates
)
if (chunk.hasEntryModule()) {
source = chunkTemplate.hooks.renderWithEntry.call(source, chunk)
}
chunk.rendered = true
return new ConcatSource(source, ";")
}
}
复制代码
这样经过触发 renderManifest hook 获取到了渲染这个 chunk manifest 配置项。和 MainTemplate 获取到的 manifest 数组不一样的主要地方就在于其中的 render 函数,这里能够看到的就是渲染每一个 chunk 是调用的 JavascriptModulesPlugin 这个插件上提供的 render 函数。
获取到了 chunk 渲染所需的 manifest 配置项后,即开始调用 render 函数开始渲染这个 chunk 最终的输出内容了,即对应于 JavascriptModulesPlugin 上的 renderJavascript 方法。
renderChunkModules——生成每一个module的代码
在 webpack 总览中,咱们介绍过 webpack 打包以后的常见代码结构:
(function(modules){
...(webpack的函数)
return __webpack_require__(__webpack_require__.s = "./demo01/main.js");
})(
{
"./a.js": (function(){...}),
"./b.js": (function(){...}),
"./main.js": (function(){...}),
}
)
复制代码
一个当即执行函数,函数的参数是各个 module 组成的对象(某些时候是数组)。这里函数的参数就是 renderChunkModules 这个函数获得的:经过 moduleTemplate.render 方法获得每一个 module 的代码,而后将其封装为数组的形式: [/*module a.js*/, /*module b.js*/]
或者对象的形式: {'a.js':function, 'b.js': function}
的形式,做为参数添加到当即执行函数中。 renderChunkModules 方法代码以下:
class Template {
static renderChunkModules(
chunk,
filterFn,
moduleTemplate,
dependencyTemplates,
prefix = ""
) {
const source = new ConcatSource();
const modules = chunk.getModules().filter(filterFn); // 获取这个 chunk 所依赖的模块
let removedModules;
if (chunk instanceof HotUpdateChunk) {
removedModules = chunk.removedModules;
}
// 若是这个 chunk 没有依赖的模块,且 removedModules 不存在,那么当即返回,代码再也不继续向下执行
if (
modules.length === 0 &&
(!removedModules || removedModules.length === 0)
) {
source.add("[]");
return source;
}
// 遍历全部依赖的 module,每一个 module 经过使用 moduleTemplate.render 方法进行渲染获得最终这个 module 须要输出的内容
/** @type {{id: string|number, source: Source|string}[]} */
const allModules = modules.map(module => {
return {
id: module.id, // 每一个 module 的 id
source: moduleTemplate.render(module, dependencyTemplates, { // 渲染每一个 module
chunk
})
};
});
// 判断这个 chunk 所依赖的 module 的 id 是否存在边界值,若是存在边界值,那么这些 modules 将会放置于一个以边界数组最大最小值做为索引的数组当中;
// 若是没有边界值,那么 modules 将会被放置于一个以 module.id 做为 key,module 实际渲染内容做为 value 的对象当中
const bounds = Template.getModulesArrayBounds(allModules);
if (bounds) {
// Render a spare array
const minId = bounds[0];
const maxId = bounds[1];
if (minId !== 0) {
source.add(`Array(${minId}).concat(`);
}
source.add("[\n");
/** @type {Map<string|number, {id: string|number, source: Source|string}>} */
const modules = new Map();
for (const module of allModules) {
modules.set(module.id, module);
}
for (let idx = minId; idx <= maxId; idx++) {
const module = modules.get(idx);
if (idx !== minId) {
source.add(",\n");
}
source.add(`/* ${idx} */`);
if (module) {
source.add("\n");
source.add(module.source); // 添加每一个 module 最终输出的代码
}
}
source.add("\n" + prefix + "]");
if (minId !== 0) {
source.add(")");
}
} else {
// Render an object
source.add("{\n");
allModules.sort(stringifyIdSortPredicate).forEach((module, idx) => {
if (idx !== 0) {
source.add(",\n");
}
source.add(`\n/***/ ${JSON.stringify(module.id)}:\n`);
source.add(module.source);
});
source.add(`\n\n${prefix}}`);
}
return source
}
}
复制代码
咱们来看下在 chunk 渲染过程当中,如何对每一个所依赖的 module 进行渲染拼接代码的,即在 Template 类当中提供的 renderChunkModules 方法中,遍历这个 chunk 当中全部依赖的 module 过程当中,调用 moduleTemplate.render 完成每一个 module 的代码渲染拼接工做。
首先咱们来了解下3个和输出 module 代码相关的模板:
RuntimeTemplate
顾名思义,这个模板类主要是提供了和 module 运行时相关的代码输出方法,例如你的 module 使用的是 esModule 类型,那么导出的代码模块会带有__esModule
标识,而经过 import 语法引入的外部模块都会经过/* harmony import */
注释来进行标识。
dependencyTemplates
dependencyTemplates 模板数组主要是保存了每一个 module 不一样依赖的模板,在输出最终代码的时候会经过 dependencyTemplates 来完成模板代码的替换工做。
ModuleTemplate
ModuleTemplate 模板类主要是对外暴露了 render 方法,经过调用 moduleTemplate 实例上的 render 方法,即完成每一个 module 的代码渲染工做,这也是每一个 module 输出最终代码的入口方法。
如今咱们从 ModuleTemplate 模板开始:
// ModuleTemplate.js
module.exports = class ModuleTemplate extends Tapable {
constructor(runtimeTemplate, type) {
this.runtimeTemplate = runtimeTemplate
this.type = type
this.hooks = {
content: new SyncWaterfallHook([]),
module: new SyncWaterfallHook([]),
render: new SyncWaterfallHook([]),
package: new SyncWaterfallHook([]),
hash: new SyncHook([])
}
}
render(module, dependencyTemplates, options) {
try {
// replaceSource
const moduleSource = module.source(
dependencyTemplates,
this.runtimeTemplate,
this.type
);
const moduleSourcePostContent = this.hooks.content.call(
moduleSource,
module,
options,
dependencyTemplates
);
const moduleSourcePostModule = this.hooks.module.call(
moduleSourcePostContent,
module,
options,
dependencyTemplates
);
// 添加编译 module 外层包裹的函数
const moduleSourcePostRender = this.hooks.render.call(
moduleSourcePostModule,
module,
options,
dependencyTemplates
);
return this.hooks.package.call(
moduleSourcePostRender,
module,
options,
dependencyTemplates
);
} catch (e) {
e.message = `${module.identifier()}\n${e.message}`;
throw e;
}
}
}
复制代码
首先调用 module.source 方法,传入 dependencyTemplates, runtimeTemplate,以及渲染类型 type(默认为 javascript)。 module.source 方法执行完成后会返回一个 ReplaceSource 类,其中包含源码和一个 replacement 数组。其中 replacement 数组中保存了对源码处理操做。
FunctionModuleTemplatePlugin 会在 render hook 阶段被调用,将咱们写在文件中的代码封装为一个函数
children:[
'/***/ (function(module, __webpack_exports__, __webpack_require__) {↵↵'
'"use strict";↵'
CachedSource // 1,2 步骤中获得的结果
'↵↵/***/ })'
]
复制代码
最终打包,触发 package hook。FunctionModuleTemplatePlugin 会在这个阶段为咱们的最终代码增长一些注释,方便咱们查看代码。
source——代码装换 如今咱们要深刻 module.source,即在每一个 module 上定义的 source 方法:
// NormalModule.js
class NormalModule extends Module {
...
source(dependencyTemplates, runtimeTemplate, type = "javascript") {
const hashDigest = this.getHashDigest(dependencyTemplates);
const cacheEntry = this._cachedSources.get(type);
if (cacheEntry !== undefined && cacheEntry.hash === hashDigest) {
// We can reuse the cached source
return cacheEntry.source;
}
// JavascriptGenerator
const source = this.generator.generate(
this,
dependencyTemplates, // 依赖的模板
runtimeTemplate,
type
);
const cachedSource = new CachedSource(source);
this._cachedSources.set(type, {
source: cachedSource,
hash: hashDigest
});
return cachedSource;
}
...
}
复制代码
咱们看到在 module.source 方法内部调用了 generator.generate 方法,那么这个 generator 又是从哪里来的呢?事实上在经过 NormalModuleFactory 建立 NormalModule 的过程即完成了 generator 的建立,以用来生成每一个 module 最终渲染的 javascript 代码。
因此 module.source 中 generator.generate 的执行代码在 JavascriptGenerator.js 中
// JavascriptGenerator.js
class JavascriptGenerator {
generate(module, dependencyTemplates, runtimeTemplate) {
const originalSource = module.originalSource(); // 获取这个 module 的 originSource
if (!originalSource) {
return new RawSource("throw new Error('No source available');");
}
// 建立一个 ReplaceSource 类型的 source 实例
const source = new ReplaceSource(originalSource);
this.sourceBlock(
module,
module,
[],
dependencyTemplates,
source,
runtimeTemplate
);
return source;
}
sourceBlock(
module,
block,
availableVars,
dependencyTemplates,
source,
runtimeTemplate
) {
// 处理这个 module 的 dependency 的渲染模板内容
for (const dependency of block.dependencies) {
this.sourceDependency(
dependency,
dependencyTemplates,
source,
runtimeTemplate
);
}
...
for (const childBlock of block.blocks) {
this.sourceBlock(
module,
childBlock,
availableVars.concat(vars),
dependencyTemplates,
source,
runtimeTemplate
);
}
}
// 获取对应的 template 方法并执行,完成依赖的渲染工做
sourceDependency(dependency, dependencyTemplates, source, runtimeTemplate) {
const template = dependencyTemplates.get(dependency.constructor);
if (!template) {
throw new Error(
"No template for dependency: " + dependency.constructor.name
);
}
template.apply(dependency, source, runtimeTemplate, dependencyTemplates);
}
}
复制代码
在 JavascriptGenerator 提供的 generate 方法主要流程以下:
完成对源码转换的大部分操做在上面第二步中,这个过程就是调用每一个依赖对应的 dependencyTemplate 的 apply 方法。webpack 中全部的 xxDependency 类中会有一个静态的 Template 方法,这个方法即是该 dependency 对应的生成最终代码的方法(相关的可参考dependencyTemplates)。
咱们用下面一个简单的例子,详细看一下源码转换的过程。demo 以下
// 打包的入口 main.js
import { A } from './a.js'
console.log(A)
// a.ja
export const A = 'a'
export const B = 'B'
复制代码
前面几篇文章咱们介绍过,通过 webpack 中文件 make 的过程以后会获得全部文件的 module,同时每一个文件中 import/export 会转化为一个 dependency。以下
因此 main.js 模块,在执行 generate 方法中 for (const dependency of block.dependencies)
这一步时,会遇到有 5 类 dependency,一个一个来看
HarmonyCompatibilityDependency 它的 Template 代码以下:
HarmonyCompatibilityDependency.Template = class HarmonyExportDependencyTemplate {
apply(dep, source, runtime) {
const usedExports = dep.originModule.usedExports;
if (usedExports !== false && !Array.isArray(usedExports)) {
const content = runtime.defineEsModuleFlagStatement({
exportsArgument: dep.originModule.exportsArgument
});
source.insert(-10, content);
}
}
};
复制代码
这里 usedExport
变量中保存了 module 中被其余 module 使用过的 export。对于每一个 chunk 的入口模块来讲比较特殊,这个值会被直接赋为 true,而对于有 export default
语句的模块来讲,这个值是 default
,这两种状况在这里都生成一句以下的代码:
__webpack_require__.r(__webpack_exports__);
复制代码
__webpack_require__.r
方法会为 __webpack_exports__
对象增长一个 __esModule
属性,将其标识为一个 es module。webpack 会将咱们代码中暴露出的 export 都转化为 module.exports 的属性,对于有 __esModule
标识的模块,当咱们经过 import x from 'xx'
引入时,x 是 module.exports.default
的内容,不然的话会被当成为 CommonJs module 的规范,引入的是整个 module.exports。
HarmonyInitDependency 和 HarmonyCompatibilityDependency 依赖一块儿出现的还有 HarmonyInitDependency。这个 Template 方法中会遍历 module 下的全部依赖,若是依赖有 harmonyInit,则会执行。
for (const dependency of module.dependencies) {
const template = dependencyTemplates.get(dependency.constructor);
if (
template &&
typeof template.harmonyInit === "function" &&
typeof template.getHarmonyInitOrder === "function"
) {
//...
}
}
复制代码
harmonyInit 方法这里须要解释一下:当咱们在源码中使用到 import 和 export 时 , webpack 为处理这些引用的逻辑,须要在咱们源码的最开始有针对性的插入一些代码,能够认为是初始化的代码。例如咱们在 webpack 生成的代码中常见到的
__webpack_exports__["default"] = /*...*/
或者
__webpack_require__.d(__webpack_exports__, "A", function() { return A; });
复制代码
这些代码的生成逻辑就保存在对应的 dependency 的 harmonyInit 方法中,而在处理 HarmonyInitDependency 阶段会被执行。到这里,你也会更加明白咱们曾在讲 module 生成中 parse 阶段的时候提到过,若是检测到源码中有 import 或者 export 的时候,会增长一个 HarmonyInitDependency 依赖的缘由。
main.js 的 HarmonyImportSideEffectDependency 和 HarmonyImportSpecifierDependency 中的 harmonyInit
方法,都会在这里被调用,分别生成下面的代码
"/* harmony import */ var _a_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(1);↵" // 对应:import { A } from './a.js'
复制代码
ConstDepedency 和 HarmonyImportSideEffectDependency import { A } from './a.js'
为例:
/* harmony import */ var _a_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(1);
HarmonyImportSpecifierDependency 在处理 console.log(A)
中的 A 的时候被加入这个依赖,在这里会生成下面一个变量名称:
_a_js__WEBPACK_IMPORTED_MODULE_0__[/* A */ "a"]
复制代码
并用这个名称替换源码中的 A,最终将 A 对应到 a.js 中暴露出来的 A 变量上。
当 main.js 全部依赖处理完以后,会获得下面的数据
//ReplaceSource
replacements:[
[-10, -11, "__webpack_require__.r(__webpack_exports__);↵", 0],
[-1, -2, "/* harmony import */ var _a_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(1);↵", 1],
[0, 25, "", 2], // 0-25 对应源码:import { A } from './a.js'
[39, 39, "_a_js__WEBPACK_IMPORTED_MODULE_0__[/* A */ "a"]", 7], // 84-84 对应源码:console.log(A) 中的 A
]
复制代码
对照一下源码,把源码中对应位置的代码替换成 ReplaceSource 中的内容:
__webpack_require__.r(__webpack_exports__);
/* harmony import */ var _a_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./a.js */ "./demo01/a.js");
console.log(_a_js__WEBPACK_IMPORTED_MODULE_0__["A"])
复制代码
进行 webpack 处理后,咱们的 main.js 就会变成上面这样的代码。
下面咱们再看一下 a.js:
a.js 中有 4 类 dependency:HarmonyCompatibilityDependency、HarmonyInitDependency、HarmonyExportHeaderDependency、HarmonyExportSpecifierDependency。
HarmonyInitDependency 前面在 main.js 中已经介绍过这个 dependency,它会遍历全部的 dependency。在这个过程当中 a.js 代码 export const A
和 export const B
所对应的 HarmonyExportSpecifierDependency 中的 template.harmonyInit
方法将会在这时执行,而后获得下面两句
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "a", function() { return A; });
/* unused harmony export B */
复制代码
这样在最终的代码中 const A
就被注册到了 a.js 对应的 module 的 exports 上。而 const B
因为没被其余代码所引用,因此会被 webpack 的 tree-shaking 逻辑探测到,在这里只是转化为一句注释
HarmonyExportHeaderDependency
它对源码的处理很简单,代码以下:
HarmonyExportHeaderDependency.Template = class HarmonyExportDependencyTemplate {
apply(dep, source) {
const content = "";
const replaceUntil = dep.range
? dep.range[0] - 1
: dep.rangeStatement[1] - 1;
source.replace(dep.rangeStatement[0], replaceUntil, content);
}
};
复制代码
因为前面在 HarmonyInitDependency 的逻辑中已经完成了对 export 变量的处理,因此这里将 export const A = 'a'
和 export const B = 'b'
语句中的 export
替换为空字符串。
HarmonyExportSpecifierDependency 自己的 Template.apply 是空函数,因此这个依赖主要在 HarmonyInitDependency 时发挥做用。
完成对 a.js 中全部 dependency 的处理后,会获得下面的一个结果:
// ReplaceSource
children:[
[-1, -2, "/* harmony export (binding) */ __webpack_require__…", "a", function() { return A; });↵", 0],
[-1, -2, "/* unused harmony export B */↵", 1],
[0, 6, "", 2], // 0-6 对应源码:'export '
[21, 27, "", 3], // 21-27 对应源码:'export '
]
复制代码
一样的,若是把 a.js 源码中对应位置的代码替换一下,a.js 的源码就变成了下面这样:
__webpack_require__.r(__webpack_exports__);
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "A", function() { return A; });
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "B", function() { return B; });
const A = 'a'
const B = 'B'
复制代码
generate.render 这一步完成了对源码内容的转换,以后回到 ModuleTemplate.js 的 render 方法中,继续将独立的 module 整合成最终的可执行函数。
content、module、render、package——代码包裹 generate.render 完成以后,接下来触发 hooks.content 、 hooks.module 这2个钩子函数,主要是用来对于 module 完成依赖代码替换后的代码处理工做,开发者能够经过注册相关的钩子完成对于 module 代码的改造,由于这个时候获得代码尚未在外层包裹 webpack runtime 的代码,所以在这2个钩子函数对于 module 代码作改造最合适。
当上面2个 hooks 都执行完后,开始触发 hooks.render 钩子:
// FunctionModuleTemplatePlugin.js
class FunctionModuleTemplatePlugin {
apply(moduleTemplate) {
moduleTemplate.hooks.render.tap(
"FunctionModuleTemplatePlugin",
(moduleSource, module) => {
const source = new ConcatSource();
const args = [module.moduleArgument]; // module
// TODO remove HACK checking type for javascript
if (module.type && module.type.startsWith("javascript")) {
args.push(module.exportsArgument); // __webpack_exports__
if (module.hasDependencies(d => d.requireWebpackRequire !== false)) {
// 判断这个模块内部是否使用了被引入的其余模块,若是有的话,那么就须要加入 __webpack_require__
args.push("__webpack_require__"); // __webpack_require__
}
} else if (module.type && module.type.startsWith("json")) {
// no additional arguments needed
} else {
args.push(module.exportsArgument, "__webpack_require__");
}
source.add("/***/ (function(" + args.join(", ") + ") {\n\n");
if (module.buildInfo.strict) source.add('"use strict";\n'); // harmony module 会使用 use strict; 严格模式
// 将 moduleSource 代码包裹至这个函数当中
source.add(moduleSource);
source.add("\n\n/***/ })");
return source;
}
)
}
}
复制代码
这个钩子函数主要的工做就是完成对上面已经完成的 module 代码进行一层包裹,包裹的内容主要是 webpack 自身的一套模块加载系统,包括模块导入,导出等,每一个 module 代码最终生成的形式为:
/***/ (function(module, __webpack_exports__, __webpack_require__) {
// module 最终生成的代码被包裹在这个函数内部
// __webpack_exports__ / __webpack_require__ 相关的功能能够阅读 webpack runtime bootstrap 代码去了解
/***/ })
复制代码
当 hooks.render 钩子触发后完成 module 代码的包裹后,触发 hooks.package 钩子,这个主要是用于在 module 代码中添加注释的功能,就不展开说了,具体查阅FunctionModuleTemplatePlugin.js
。
到这里就完成了对于一个 module 的代码的渲染工做,最终在每一个 chunk 当中的每个 module 代码也就是在今生成。
module 代码生成以后便返回到上文JavascriptModulePlugin.renderJavascript
方法当中,继续后面生成每一个 chunk 最终代码的过程当中了。
整合成可执行函数 接下来触发 chunkTemplate.hooks.modules 钩子函数,若是你须要对于 chunk 代码有所修改,那么在这里能够经过 plugin 注册 hooks.modules 钩子函数来完成相关的工做。这个钩子触发后,继续触发 chunkTemplate.hooks.render 钩子函数,在JsonpChunkTemplatePlugin
这个插件当中注册了对应的钩子函数:
class JsonpChunkTemplatePlugin {
/** * @param {ChunkTemplate} chunkTemplate the chunk template * @returns {void} */
apply(chunkTemplate) {
chunkTemplate.hooks.render.tap(
"JsonpChunkTemplatePlugin",
(modules, chunk) => {
const jsonpFunction = chunkTemplate.outputOptions.jsonpFunction;
const globalObject = chunkTemplate.outputOptions.globalObject;
const source = new ConcatSource();
const prefetchChunks = chunk.getChildIdsByOrders().prefetch;
source.add(
`(${globalObject}[${JSON.stringify( jsonpFunction )}] = ${globalObject}[${JSON.stringify( jsonpFunction )}] || []).push([${JSON.stringify(chunk.ids)},`
);
source.add(modules);
const entries = getEntryInfo(chunk);
if (entries.length > 0) {
source.add(`,${JSON.stringify(entries)}`);
} else if (prefetchChunks && prefetchChunks.length) {
source.add(`,0`);
}
if (prefetchChunks && prefetchChunks.length) {
source.add(`,${JSON.stringify(prefetchChunks)}`);
}
source.add("])");
return source;
}
)
}
}
复制代码
这个钩子函数主要完成的工做就是将这个 chunk 当中全部已经渲染好的 module 的代码再一次进行包裹组装,生成这个 chunk 最终的代码,也就是最终会被写入到文件当中的代码。与此相关的是 JsonpTemplatePlugin,这个插件内部注册了 chunkTemplate.hooks.render 的钩子函数,在这个函数里面完成了 chunk 代码外层的包裹工做。咱们来看个经过这个钩子函数处理后生成的 chunk 代码的例子:
// a.js
import { add } from './add.js'
add(1, 2)
-------
// 在 webpack config 配置环节将 webpack runtime bootstrap 代码单独打包成一个 chunk,那么最终 a.js 所在的 chunk输出的代码是:
(window["webpackJsonp"] = window["webpackJsonp"] || []).push([[1],[
/* 0 */
/***/ (function(module, __webpack_exports__, __webpack_require__) {
// module id 为0的 module 输出代码,即 a.js 最终输出的代码
/***/ }),
/* 1 */
/***/ (function(module, __webpack_exports__, __webpack_require__) {
// module id 为1的 module 输出代码,即 add.js 最终输出的代码
/***/ })
],[[0,0]]]);
复制代码
到此为止,有关 renderJavascript 方法的流程已经梳理完毕了,这也是非 runtime bootstrap chunk 代码最终的输出时的处理流程。
以上就是有关 chunk 代码生成的流程分析即 createChunkAssets,当这个流程进行完后,全部须要生成到文件的 chunk 最终会保存至 compilation 的一个 key/value 结构当中:
compilation.assets = {
[输出文件路径名]: ConcatSource(最终 chunk 输出的代码)
}
复制代码
接下来针对保存在内容当中的这些 assets 资源作相关的优化工做,同时会暴露出一些钩子供开发者对于这些资源作相关的操做,例如可使用 compilation.optimizeChunkAssets 钩子函数去往 chunk 内添加代码等等,有关这些钩子的说明具体能够查阅webpack文档上有关assets优化的内容。
经历了上面全部的阶段以后,全部的最终代码信息已经保存在了 Compilation 的 assets 中。而后代码片断会被拼合起来,而且上一步 generator.generate 获得的 ReplaceSource 结果中,会遍历 replacement 中的操做,按照要替换的源码的前后位置(同一位置的话,按照 replacement 中的最后一个参数优先级前后)来一一对源码进行替换,而后代码最终代码。 webpack 配置中能够配置一些优化,例如压缩,因此在获得代码后会进行一些优化。 当 assets 资源相关的优化工做结束后,seal 阶段也就结束了。这时候执行 seal 函数接受到 callback,进入到 webpack 后续的流程。具体内容可查阅 compiler 编译器对象提供的 run 方法。这个 callback 方法内容会执行到 compiler.emitAssets 方法:
// Compiler.js
class Compiler extends Tapable {
...
emitAssets(compilation, callback) {
let outputPath;
const emitFiles = err => {
if (err) return callback(err);
asyncLib.forEach(
compilation.assets,
(source, file, callback) => {
let targetFile = file;
const queryStringIdx = targetFile.indexOf("?");
if (queryStringIdx >= 0) {
targetFile = targetFile.substr(0, queryStringIdx);
}
const writeOut = err => {
if (err) return callback(err);
const targetPath = this.outputFileSystem.join(
outputPath,
targetFile
);
if (source.existsAt === targetPath) {
source.emitted = false;
return callback();
}
let content = source.source();
if (!Buffer.isBuffer(content)) {
content = Buffer.from(content, "utf8");
}
source.existsAt = targetPath;
source.emitted = true;
this.outputFileSystem.writeFile(targetPath, content, callback);
};
if (targetFile.match(/\/|\\/)) {
const dir = path.dirname(targetFile);
this.outputFileSystem.mkdirp(
this.outputFileSystem.join(outputPath, dir),
writeOut
);
} else {
writeOut();
}
},
err => {
if (err) return callback(err);
this.hooks.afterEmit.callAsync(compilation, err => {
if (err) return callback(err);
return callback();
});
}
);
};
this.hooks.emit.callAsync(compilation, err => {
if (err) return callback(err);
outputPath = compilation.getPath(this.outputPath);
this.outputFileSystem.mkdirp(outputPath, emitFiles);
});
}
...
}
复制代码
在这个方法当中首先触发 hooks.emit 钩子函数,即将进行写文件的流程。接下来开始建立目标输出文件夹,并执行 emitFiles 方法,将内存当中保存的 assets 资源输出到目标文件夹当中,这样就完成了内存中保存的 chunk 代码写入至最终的文件。最终有关 emit assets 输出最终 chunk 文件的流程图见下: