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
信息的引用关系(正向和反向)。
✅ 核心逻辑:
-
如果
newInfo
为undefined
:- 删除
assetsInfo[file]
,即清除资源信息。
- 删除
-
否则:
- 用
newInfo
设置新的assetsInfo[file]
。
- 用
-
若存在旧的
related
字段:- 遍历其每个 key,清除
_assetsRelatedIn
中相关引用。
- 遍历其每个 key,清除
-
若存在新的
related
字段:- 遍历其每个 key,建立
_assetsRelatedIn
中的引用映射(反向依赖图)。
- 遍历其每个 key,建立
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.files
和chunk.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);
}
}