阅读 2637

webpack系列之七-文件生成

作者:崔静、肖磊

经过前几篇文章我们介绍了 webpack 如何从配置文件的入口开始,将每一个文件转变为内部的 module,然后再由 module 整合成一个一个的 chunk。这篇文章我们来看一下最后一步 —— chunk 如何转变为最终的 js 文件。

总流程

上篇文章主要是梳理了在 seal 阶段的开始, webpack 内部是如何将有依赖关系的 module 统一组织到一个 chunk 当中的。现在继续来看 seal 阶段,chunk 生成之后的部分,我们从 optimizeTree.callAsync 看起

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 => {
		  //...
		});
	});
}

复制代码

上面代码中,按照从上往下的顺序依次看,经历的主流程如下:

总流程图

主要步骤为:生成 moduleId,生成 chunkId,生成 hash,然后生成最终输出文件的内容,同时每一步之间都会暴露 hook , 提供给插件修改的机会。接下来我们一一看一下核心逻辑:id 生成,hash 生成,文件内容生成

id 生成

webpack 会对 module 和 chunk 分别生成id,这二者在逻辑上基本相同。我们先以 module id 为例来看 id 生成的过程(在 webpack 为 module 生成 id 的逻辑位于 applyModuleIds 方法中),代码如下

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 的流程主要分两步:

  • 找到当前未使用的 id 和 已经使用的最大的 id。举个例子:如果已经使用的 id 是 [3, 6, 7 ,8],那么经过第一步处理后,nextFreeModuleId = 9, unusedIds = [0, 1, 2, 4, 5]

  • 给没有 id 的 module 设置 id。设置 id 时,优先使用 unusedIds 中的值。

在设置 id 的时候,有一个判断 module2.id === null,也就是说若在这一步之前,已经被设置过 id 值,那么这里便直接忽略。在设置 id 之前,会触发两个钩子:

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 进行排序。

hash

在 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);
}
复制代码

主结构其实就是两部分:

  • 为 module 生成 hash
  • 为 chunk 生成 hash

webpack 中计算 hash 值底层所使用的是 Node.js 中的 crypto, 主要用到了两个方法:

  • hash.update 可以简单认为是增加用于生成 hash 的原始内容(以下统一简称为 hash 源)
  • digest 方法用来得到最终 hash 值。

下面我们先看 module 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 内容包括:

  • 每个 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 内容原始内容中写入依赖信息的过程,如下图

    dep-hash继承

    上面图可以看出,依赖的 module 会影响当前 module 的 hash,如果我们修改顺序或者其他的操作造成依赖 module 的 id 改变了,那么当前 module 得到的 hash 也会改变。 所以 module 的 hash 内容不仅包含了源码,还包含了和其打包构建相关的内容。因为当我们修改了 webpack 的相关配置时,最终得到的代码很有可能会改变,将这些会影响最终代码生成的配置写入生成 hash 的 buffer 中可以保证,当我们仅修改 webpack 的打包配置,比如改变 module id 生成方式等,也可以得到一个 hash 值不同的文件名。

chunck 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 值也应该随之改变。

create assets

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;
					...
				}
			}
			....
		}
	}
	...
}
复制代码

主要步骤:

  1. 获取对应的渲染模板

在 createChunkAssets 方法内部会对最终需要输出的 chunk 进行遍历,根据这个 chunk 是否包含有 webpack runtime 代码来决定使用的渲染模板(mainTemplate/chunkTemplate)。其中 mainTemplate 主要用于包含 webpack runtime bootstrap 的 chunk 代码渲染生成工作,chunkTemplate 主要用于普通 chunk 的代码渲染工作。

  1. 然后通过 getRenderManifest 获取到 render 需要的内容。

mainTemplate 和 chunkTemplate 分别有自己的 getRenderManifest 方法,在这个方法中会生成 render 代码需要的所有信息,包括文件名称格式、对应的 render 函数,哈希值等。

  1. 执行 render() 得到最终的代码。
  2. 获取文件路径,保存到 assets 中。

我们首先来看下包含有 webpack runtime 代码的 chunk 是如何输出最终的 chunk 文本内容的。

mainTemplate 渲染生成包含 webpack runtime bootstrap 代码的 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 类型实例。 简化一下,大概如下图:

chunk代码生成逻辑

最终的代码会被保存在一个 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 的有关内容。

chunkTemplate 渲染生成普通 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 方法。

emit-assets-chunk

  1. Template.renderChunkModules 获取每个 chunk 当中所依赖的所有 module 最终需要渲染的代码
  2. chunkTemplate.hooks.modules 触发 hooks.modules 钩子,用以在最终生成 chunk 代码前对 chunk 最修改
  3. chunkTemplate.hooks.render 当上面2个步骤都进行完后,调用 hooks.render 钩子函数,完成这个 chunk 最终的渲染,即在外层添加包裹函数。

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;
		}
	}
}
复制代码

emit-assets-module

  1. 首先调用 module.source 方法,传入 dependencyTemplates, runtimeTemplate,以及渲染类型 type(默认为 javascript)。 module.source 方法执行完成后会返回一个 ReplaceSource 类,其中包含源码和一个 replacement 数组。其中 replacement 数组中保存了对源码处理操作。

  2. FunctionModuleTemplatePlugin 会在 render hook 阶段被调用,将我们写在文件中的代码封装为一个函数

    children:[
      '/***/ (function(module, __webpack_exports__, __webpack_require__) {↵↵'
      '"use strict";↵'
      CachedSource // 1,2 步骤中得到的结果
      '↵↵/***/ })'
    ]
    复制代码
  3. 最终打包,触发 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 代码。

getGenerator

所以 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 方法主要流程如下:

  1. 生成一个 ReplaceSource 对象,并将源码保存到对象中。这个对象还会包含一个 replacements 数组和 source 方法(source 方法会在生成最终代码时被调用,根据 replacements 的内容,将源码 _source 中对应位置代码进行替换,从而得到最终代码)。
  2. 根据每一个依赖对源码做相应的处理,可能是替换某些代码,也可能是插入一些代码。这些对源码转化的操作,将保存在 ReplaceSource 的 replacements 的数组中。(具体请参见dependencyTemplates 这里就不展开讨论了)
  3. 处理 webpack 内部特有的变量
  4. 如有有 block ,则对每个 block 做 1-4 的处理(当我们使用异步加载时,对应的 import 的内容会被放在 block 中)。

完成对源码转换的大部分操作在上面第二步中,这个过程就是调用每个依赖对应的 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。如下

module中包含dep

所以 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' 为例:

  • ConstDenpendency 会将这一句替换为空字符串
  • HarmonyImportSideEffectDependency 在此没有实际的作用 所以这两个 dependency 一起的作用其实是将这一句转化为 /* 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 Aexport 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 文件的流程图见下:

emit-assets-main-process