webpack 核心编译器 十四 节

1. emitAsset(file, source, assetInfo = {})

作用

assets 中添加一个新的资源或更新已有资源,同时处理冲突、合并信息等。

核心逻辑

  • 如果 file 已存在于 assets

    • 判断新旧 source 是否相同:

      • 不同:报错(资源冲突),记录错误并强制覆盖。
      • 相同:合并已有的 assetInfo 和传入的 assetInfo
  • 如果 file 不存在:

    • 直接添加资源和资源信息。
  • 最终通过 _setAssetInfo 更新 assetsInfo 和引用关系 _assetsRelatedIn

js 复制代码
/**
 * 发出一个资源(asset),添加到 compilation.assets 中。
 * 
 * @param {string} file - 文件名
 * @param {Source} source - 资源内容
 * @param {AssetInfo} assetInfo - 资源的附加信息(可选)
 */
emitAsset(file, source, assetInfo = {}) {
	// 如果该文件已存在于 assets 中
	if (this.assets[file]) {
		// 如果新旧内容不同
		if (!isSourceEqual(this.assets[file], source)) {
			// 报告资源冲突错误
			this.errors.push(
				new WebpackError(
					`Conflict: Multiple assets emit different content to the same filename ${file}${assetInfo.sourceFilename
						? `. Original source ${assetInfo.sourceFilename}`
						: ""}`
				)
			);
			// 仍然覆盖原有资源
			this.assets[file] = source;
			this._setAssetInfo(file, assetInfo);
			return;
		}
		// 如果资源内容相同,只更新信息(合并 oldInfo 与 assetInfo)
		const oldInfo = this.assetsInfo.get(file);
		const newInfo = { ...oldInfo, ...assetInfo };
		this._setAssetInfo(file, newInfo, oldInfo);
		return;
	}
	// 不存在该资源时,直接添加
	this.assets[file] = source;
	this._setAssetInfo(file, assetInfo, undefined);
}

2. _setAssetInfo(file, newInfo, oldInfo = this.assetsInfo.get(file))

作用

更新某个资源的 assetInfo,并维护 related 信息的引用关系(正向和反向)。

核心逻辑

  • 如果 newInfoundefined

    • 删除 assetsInfo[file],即清除资源信息。
  • 否则:

    • newInfo 设置新的 assetsInfo[file]
  • 若存在旧的 related 字段:

    • 遍历其每个 key,清除 _assetsRelatedIn 中相关引用。
  • 若存在新的 related 字段:

    • 遍历其每个 key,建立 _assetsRelatedIn 中的引用映射(反向依赖图)。
js 复制代码
/**
 * 设置资源信息,并更新"反向依赖表" _assetsRelatedIn。
 * 
 * @private
 * @param {string} file - 文件名
 * @param {AssetInfo} newInfo - 新的资源信息
 * @param {AssetInfo=} oldInfo - 旧的资源信息(默认为当前已有的信息)
 */
_setAssetInfo(file, newInfo, oldInfo = this.assetsInfo.get(file)) {
	// 删除资源信息
	if (newInfo === undefined) {
		this.assetsInfo.delete(file);
	} else {
		this.assetsInfo.set(file, newInfo);
	}

	const oldRelated = oldInfo && oldInfo.related;
	const newRelated = newInfo && newInfo.related;

	// 移除旧的 related 关系
	if (oldRelated) {
		for (const key of Object.keys(oldRelated)) {
			const remove = name => {
				const relatedIn = this._assetsRelatedIn.get(name);
				if (!relatedIn) return;
				const entry = relatedIn.get(key);
				if (!entry) return;
				entry.delete(file);
				if (entry.size !== 0) return;
				relatedIn.delete(key);
				if (relatedIn.size === 0) this._assetsRelatedIn.delete(name);
			};
			const entry = oldRelated[key];
			if (Array.isArray(entry)) {
				for (const name of entry) {
					remove(name);
				}
			} else if (entry) {
				remove(entry);
			}
		}
	}

	// 添加新的 related 关系
	if (newRelated) {
		for (const key of Object.keys(newRelated)) {
			const add = name => {
				let relatedIn = this._assetsRelatedIn.get(name);
				if (!relatedIn) {
					this._assetsRelatedIn.set(name, (relatedIn = new Map()));
				}
				let entry = relatedIn.get(key);
				if (!entry) {
					relatedIn.set(key, (entry = new Set()));
				}
				entry.add(file);
			};
			const entry = newRelated[key];
			if (Array.isArray(entry)) {
				for (const name of entry) {
					add(name);
				}
			} else if (entry) {
				add(entry);
			}
		}
	}
}

3. updateAsset(file, newSourceOrFunction, assetInfoUpdateOrFunction = undefined)

作用

更新某个资源的内容和附加信息。

核心逻辑

  • 如果资源不存在,则抛错。

  • 使用新值(或通过函数)替换 assets[file]

  • 如果提供了 assetInfo 更新:

    • 如果是函数,传入旧 info 得到新 info。
    • 如果是对象,则使用 cachedCleverMerge 合并旧 info 和新 info。
  • 更新 info 依赖 _setAssetInfo

js 复制代码
/**
 * 更新资源内容及其信息。
 * 
 * @param {string} file - 文件名
 * @param {Source | function(Source): Source} newSourceOrFunction - 新资源或生成新资源的函数
 * @param {(AssetInfo | function(AssetInfo | undefined): AssetInfo) | undefined} assetInfoUpdateOrFunction - 更新 assetInfo 的方式(对象或函数)
 */
updateAsset(file, newSourceOrFunction, assetInfoUpdateOrFunction = undefined) {
	if (!this.assets[file]) {
		throw new Error(`Called Compilation.updateAsset for not existing filename ${file}`);
	}

	// 更新 source 内容(直接替换或使用函数)
	this.assets[file] =
		typeof newSourceOrFunction === "function"
			? newSourceOrFunction(this.assets[file])
			: newSourceOrFunction;

	// 更新 assetInfo
	if (assetInfoUpdateOrFunction !== undefined) {
		const oldInfo = this.assetsInfo.get(file) || EMPTY_ASSET_INFO;
		if (typeof assetInfoUpdateOrFunction === "function") {
			this._setAssetInfo(file, assetInfoUpdateOrFunction(oldInfo), oldInfo);
		} else {
			this._setAssetInfo(file, cachedCleverMerge(oldInfo, assetInfoUpdateOrFunction), oldInfo);
		}
	}
}

4. renameAsset(file, newFile)

作用

将一个资源从旧文件名 file 改为新文件名 newFile,并处理所有引用和关联。

核心逻辑

  • 如果旧文件不存在,抛错。

  • 如果新文件存在且资源内容不同,报错(资源重命名冲突)。

  • 更新所有引用该资源的其他资源(通过 _assetsRelatedIn 反向依赖表):

    • 把引用旧 file 的地方改为 newFile
  • 调整 assets 映射和 assetsInfo

  • 更新 chunk.fileschunk.auxiliaryFiles 中的文件名。

js 复制代码
/**
 * 重命名一个资源。
 * 
 * @param {string} file - 原文件名
 * @param {string} newFile - 新文件名
 */
renameAsset(file, newFile) {
	const source = this.assets[file];
	if (!source) {
		throw new Error(`Called Compilation.renameAsset for not existing filename ${file}`);
	}
	// 如果新文件已存在且内容不同,则报错
	if (this.assets[newFile] && !isSourceEqual(this.assets[file], source)) {
		this.errors.push(
			new WebpackError(
				`Conflict: Called Compilation.renameAsset for already existing filename ${newFile} with different content`
			)
		);
	}
	const assetInfo = this.assetsInfo.get(file);

	// 更新所有引用当前文件的 relatedIn 映射
	const relatedInInfo = this._assetsRelatedIn.get(file);
	if (relatedInInfo) {
		for (const [key, assets] of relatedInInfo) {
			for (const name of assets) {
				const info = this.assetsInfo.get(name);
				if (!info) continue;
				const related = info.related;
				if (!related) continue;
				const entry = related[key];
				let newEntry;
				if (Array.isArray(entry)) {
					newEntry = entry.map(x => (x === file ? newFile : x));
				} else if (entry === file) {
					newEntry = newFile;
				} else continue;
				this.assetsInfo.set(name, {
					...info,
					related: {
						...related,
						[key]: newEntry
					}
				});
			}
		}
	}

	// 迁移 assetInfo
	this._setAssetInfo(file, undefined, assetInfo);
	this._setAssetInfo(newFile, assetInfo);

	// 更新 assets 映射
	delete this.assets[file];
	this.assets[newFile] = source;

	// 更新 chunk 中的文件引用
	for (const chunk of this.chunks) {
		{
			const size = chunk.files.size;
			chunk.files.delete(file);
			if (size !== chunk.files.size) {
				chunk.files.add(newFile);
			}
		}
		{
			const size = chunk.auxiliaryFiles.size;
			chunk.auxiliaryFiles.delete(file);
			if (size !== chunk.auxiliaryFiles.size) {
				chunk.auxiliaryFiles.add(newFile);
			}
		}
	}
}

5. deleteAsset(file)

作用

从所有资源映射中彻底删除某个资源,并递归删除它的 related 文件(如果它们未被其他资源引用)。

核心逻辑

  • 如果资源不存在,直接返回。

  • 删除 assets[file]

  • 通过 _setAssetInfo(file, undefined, oldInfo) 清除资源信息。

  • 遍历其 assetInfo.related 字段:

    • 检查每个 related 文件是否还被引用:

      • 若没有其他引用,递归删除之。
  • 清除所有 chunk 对该资源的引用。

js 复制代码
/**
 * 删除资源及其相关信息,并递归删除未被引用的 related 资源。
 * 
 * @param {string} file - 文件名
 */
deleteAsset(file) {
	if (!this.assets[file]) return;

	// 删除资源内容
	delete this.assets[file];
	const assetInfo = this.assetsInfo.get(file);

	// 清除资源信息并更新反向引用表
	this._setAssetInfo(file, undefined, assetInfo);

	const related = assetInfo && assetInfo.related;
	if (related) {
		for (const key of Object.keys(related)) {
			const checkUsedAndDelete = file => {
				// 如果没有其他资源引用该文件,递归删除
				if (!this._assetsRelatedIn.has(file)) {
					this.deleteAsset(file);
				}
			};
			const items = related[key];
			if (Array.isArray(items)) {
				for (const file of items) {
					checkUsedAndDelete(file);
				}
			} else if (items) {
				checkUsedAndDelete(items);
			}
		}
	}

	// 删除 chunk 中的引用
	for (const chunk of this.chunks) {
		chunk.files.delete(file);
		chunk.auxiliaryFiles.delete(file);
	}
}
相关推荐
墨绿色的摆渡人15 分钟前
论文笔记(七十五)Auto-Encoding Variational Bayes
前端·论文阅读·chrome
今晚吃什么呢?36 分钟前
前端面试题之CSS中的box属性
前端·css
我是大龄程序员39 分钟前
Babel工作理解
前端
CopyLower1 小时前
提升 Web 性能:使用响应式图片优化体验
前端
南通DXZ1 小时前
Win7下安装高版本node.js 16.3.0 以及webpack插件的构建
前端·webpack·node.js
Mintopia2 小时前
深入理解 Three.js 中的 Mesh:构建 3D 世界的基石
前端·javascript·three.js
前端太佬2 小时前
暂时性死区(Temporal Dead Zone, TDZ)
前端·javascript·node.js
Mintopia2 小时前
Node.js 中 http.createServer API 详解
前端·javascript·node.js
xRainco2 小时前
Redux从简单到进阶(Redux、React-redux、Redux-toolkit)
前端
印第安老斑鸠啊2 小时前
由一次CI流水线失败引发的对各类构建工具的思考
前端