1. getUnsafeCacheData()
用处:
将模块的某些关键信息(如 parserOptions
和 generatorOptions
)保存下来,以便未来可以从缓存中快速恢复模块状态。用于优化模块构建的性能。
逻辑概览:
- 调用父类的
getUnsafeCacheData()
获取基础缓存数据。 - 将当前模块特有的配置项(
parserOptions
和generatorOptions
)附加上去。 - 返回这个组合后的数据对象。
js
/**
* 获取模块用于"非安全缓存"的数据。
* 该数据会被传递给 `restoreFromUnsafeCache` 方法用于恢复。
* 非安全缓存用于缓存 NormalModule 的部分构建结果以提升性能。
*
* @returns {UnsafeCacheData} 缓存数据对象
*/
getUnsafeCacheData() {
const data =
/** @type {NormalModuleUnsafeCacheData} */
(super.getUnsafeCacheData());
// 记录解析器和代码生成器的配置
data.parserOptions = this.parserOptions;
data.generatorOptions = this.generatorOptions;
return data;
}
restoreFromUnsafeCache(unsafeCacheData, normalModuleFactory)
用处:
外部调用接口,从缓存数据中恢复模块状态。避免重新解析模块,提高构建效率。
逻辑概览:
- 只是一个封装函数,内部调用
_restoreFromUnsafeCache()
方法处理实际的恢复工作。
js
/**
* 从非安全缓存中恢复模块状态。
* 外部调用入口。
*
* @param {NormalModuleUnsafeCacheData} unsafeCacheData 从 getUnsafeCacheData 获取的数据
* @param {NormalModuleFactory} normalModuleFactory 正在处理该模块的模块工厂
*/
restoreFromUnsafeCache(unsafeCacheData, normalModuleFactory) {
this._restoreFromUnsafeCache(unsafeCacheData, normalModuleFactory);
}
_restoreFromUnsafeCache(unsafeCacheData, normalModuleFactory)
用处:
真正执行模块的恢复逻辑。使用之前缓存的配置项和工厂方法重新初始化模块的 parser 和 generator。
逻辑概览:
- 调用父类的
_restoreFromUnsafeCache()
。 - 读取缓存中的
parserOptions
和generatorOptions
。 - 使用
normalModuleFactory
创建对应类型的 parser 和 generator。 - 不重新计算 sourceTypes 和大小,假设 generator 不变。
js
/**
* 实际执行从非安全缓存中恢复模块状态的内部逻辑。
*
* @param {object} unsafeCacheData 从缓存中读取的数据
* @param {NormalModuleFactory} normalModuleFactory 当前使用的模块工厂
*/
_restoreFromUnsafeCache(unsafeCacheData, normalModuleFactory) {
// 调用父类的恢复逻辑
super._restoreFromUnsafeCache(unsafeCacheData, normalModuleFactory);
// 恢复解析器配置
this.parserOptions = unsafeCacheData.parserOptions;
this.parser = normalModuleFactory.getParser(this.type, this.parserOptions);
// 恢复代码生成器配置
this.generatorOptions = unsafeCacheData.generatorOptions;
this.generator = normalModuleFactory.getGenerator(
this.type,
this.generatorOptions
);
// 假设代码生成器行为未变,缓存的 sourceTypes 和 size 仍可使用
}
createSourceForAsset(context, name, content, sourceMap, associatedObjectForCache)
用处:
用于 emitFile
等场景,生成一个资源对象(Webpack 内部的 Source
实例),供写入输出文件使用。
逻辑概览:
-
如果提供了
sourceMap
,并启用了useSourceMap
:- 如果是字符串:使用
OriginalSource
。 - 如果是对象:使用
SourceMapSource
。
- 如果是字符串:使用
-
如果没有
sourceMap
或未启用 source map,则返回RawSource
(纯文本)。
js
/**
* 创建一个资源对象(用于写入输出文件系统)。
* 依据是否启用了 sourceMap(源映射)决定使用哪种 Source 类型。
*
* @param {string} context 构建上下文路径
* @param {string} name 输出资源名(通常是相对路径)
* @param {string | Buffer} content 资源内容
* @param {(string | SourceMap)=} sourceMap 可选的 sourceMap 信息
* @param {object=} associatedObjectForCache 用于缓存上下文绑定的数据
* @returns {Source} 构建出的资源 Source 对象
*/
createSourceForAsset(context, name, content, sourceMap, associatedObjectForCache) {
if (sourceMap) {
if (
typeof sourceMap === "string" &&
(this.useSourceMap || this.useSimpleSourceMap)
) {
// 如果是字符串类型 sourceMap,且启用了 sourceMap 或简化版 sourceMap
return new OriginalSource(
content,
contextifySourceUrl(context, sourceMap, associatedObjectForCache)
);
}
if (this.useSourceMap) {
// 使用结构化的 sourceMap,构造 SourceMapSource
return new SourceMapSource(
content,
name,
contextifySourceMap(
context,
/** @type {SourceMap} */ (sourceMap),
associatedObjectForCache
)
);
}
}
// 未启用 sourceMap 或 sourceMap 不存在,直接返回原始内容
return new RawSource(content);
}
_createLoaderContext(resolver, options, compilation, fs, hooks)
用处:
为当前模块构建 loader 的执行上下文(loaderContext
),loader 执行时就通过这个对象访问各种 Webpack API 和工具。
逻辑概览:
-
定义多个内部工具函数和 util 方法,如:
getOptions()
:解析 loader 的options
。emitError()
/emitWarning()
:错误和警告上报。resolve()
/getResolve()
:模块解析接口。emitFile()
:发出资源文件。
-
封装成一个
loaderContext
对象,包含所有功能和信息。 -
执行 loader hook(
hooks.loader.call()
),允许插件修改 loaderContext。 -
最终返回
loaderContext
,用于 loader 执行。
js
/**
* 创建 loader 使用的上下文对象。
* 该上下文会作为 this 传入每个 loader 中。
* 提供了解析、日志、发射资源、依赖收集等功能。
*
* @private
* @template T
* @param {ResolverWithOptions} resolver 用于路径解析的 resolver 实例
* @param {WebpackOptions} options webpack 配置项
* @param {Compilation} compilation 当前的 compilation 对象
* @param {InputFileSystem} fs 用于读取文件的文件系统
* @param {NormalModuleCompilationHooks} hooks 相关钩子
* @returns {import("../declarations/LoaderContext").NormalModuleLoaderContext<T>} loader 上下文对象
*/
_createLoaderContext(resolver, options, compilation, fs, hooks) {
const { requestShortener } = compilation.runtimeTemplate;
// 获取当前 loader 名称(用于日志或错误来源标识)
const getCurrentLoaderName = () => {
const currentLoader = this.getCurrentLoader(loaderContext);
if (!currentLoader) return "(not in loader scope)";
return requestShortener.shorten(currentLoader.loader);
};
// 获取 resolve 使用的上下文对象
const getResolveContext = () => ({
fileDependencies: {
add: d => /** @type {TODO} */ (loaderContext).addDependency(d)
},
contextDependencies: {
add: d => /** @type {TODO} */ (loaderContext).addContextDependency(d)
},
missingDependencies: {
add: d => /** @type {TODO} */ (loaderContext).addMissingDependency(d)
}
});
// 构建 path 工具方法缓存
const getAbsolutify = memoize(() => absolutify.bindCache(compilation.compiler.root));
const getAbsolutifyInContext = memoize(() =>
absolutify.bindContextCache(this.context, compilation.compiler.root)
);
const getContextify = memoize(() => contextify.bindCache(compilation.compiler.root));
const getContextifyInContext = memoize(() =>
contextify.bindContextCache(this.context, compilation.compiler.root)
);
const utils = {
absolutify: (context, request) =>
context === this.context
? getAbsolutifyInContext()(request)
: getAbsolutify()(context, request),
contextify: (context, request) =>
context === this.context
? getContextifyInContext()(request)
: getContextify()(context, request),
createHash: type =>
createHash(type || compilation.outputOptions.hashFunction)
};
/** 构建 loaderContext 对象,暴露给 loader 使用 */
const loaderContext = {
version: 2,
getOptions: schema => {
const loader = this.getCurrentLoader(loaderContext);
let { options } = /** @type {LoaderItem} */ (loader);
// 字符串形式的 options:解析成对象
if (typeof options === "string") {
if (options.startsWith("{") && options.endsWith("}")) {
try {
options = parseJson(options);
} catch (err) {
throw new Error(`Cannot parse string options: ${err.message}`);
}
} else {
options = querystring.parse(options, "&", "=", { maxKeys: 0 });
}
}
if (options === null || options === undefined) options = {};
// 如果 schema 存在,验证 loader 配置是否符合 schema
if (schema) {
let name = "Loader";
let baseDataPath = "options";
let match;
if (schema.title && (match = /^(.+) (.+)$/.exec(schema.title))) {
[, name, baseDataPath] = match;
}
getValidate()(schema, options, { name, baseDataPath });
}
return options;
},
emitWarning: warning => {
if (!(warning instanceof Error)) {
warning = new NonErrorEmittedError(warning);
}
this.addWarning(new ModuleWarning(warning, { from: getCurrentLoaderName() }));
},
emitError: error => {
if (!(error instanceof Error)) {
error = new NonErrorEmittedError(error);
}
this.addError(new ModuleError(error, { from: getCurrentLoaderName() }));
},
getLogger: name => {
const currentLoader = this.getCurrentLoader(loaderContext);
return compilation.getLogger(() =>
[currentLoader?.loader, name, this.identifier()].filter(Boolean).join("|")
);
},
resolve(context, request, callback) {
resolver.resolve({}, context, request, getResolveContext(), callback);
},
getResolve(options) {
const child = options ? resolver.withOptions(options) : resolver;
return (context, request, callback) => {
if (callback) {
child.resolve({}, context, request, getResolveContext(), callback);
} else {
return new Promise((resolve, reject) => {
child.resolve({}, context, request, getResolveContext(), (err, result) => {
if (err) reject(err);
else resolve(result);
});
});
}
};
},
emitFile: (name, content, sourceMap, assetInfo) => {
const buildInfo = /** @type {BuildInfo} */ (this.buildInfo);
if (!buildInfo.assets) {
buildInfo.assets = Object.create(null);
buildInfo.assetsInfo = new Map();
}
const assets = buildInfo.assets;
const assetsInfo = buildInfo.assetsInfo;
assets[name] = this.createSourceForAsset(
options.context,
name,
content,
sourceMap,
compilation.compiler.root
);
assetsInfo.set(name, assetInfo);
},
addBuildDependency: dep => {
const buildInfo = /** @type {BuildInfo} */ (this.buildInfo);
if (!buildInfo.buildDependencies) {
buildInfo.buildDependencies = new LazySet();
}
buildInfo.buildDependencies.add(dep);
},
utils,
rootContext: options.context,
webpack: true,
sourceMap: Boolean(this.useSourceMap),
mode: options.mode || "production",
hashFunction: options.output.hashFunction,
hashDigest: options.output.hashDigest,
hashDigestLength: options.output.hashDigestLength,
hashSalt: options.output.hashSalt,
_module: this,
_compilation: compilation,
_compiler: compilation.compiler,
fs
};
// 合并用户自定义 loader 配置
Object.assign(loaderContext, options.loader);
// 触发 loader 钩子
hooks.loader.call(loaderContext, this);
return loaderContext;
}
getCurrentLoader(loaderContext, index = loaderContext.loaderIndex)
用处:
在 loader 执行期间,用于获取当前正在执行的 loader 信息(比如用于日志、错误来源标识等)。
逻辑概览:
- 检查当前 loader 数组是否存在,并且索引在范围内。
- 返回当前索引对应的 loader 对象,若无则返回
null
。
js
/**
* 获取当前正在执行的 loader。
*
* @param {TODO} loaderContext loader 上下文对象
* @param {number} index 当前 loader 的索引(默认从 loaderContext 中取)
* @returns {LoaderItem | null} 当前 loader 或 null
*/
getCurrentLoader(loaderContext, index = loaderContext.loaderIndex) {
if (
this.loaders &&
this.loaders.length &&
index < this.loaders.length &&
index >= 0 &&
this.loaders[index]
) {
return this.loaders[index];
}
return null;
}
createSource(context, content, sourceMap?, associatedObjectForCache?)
用处:
为模块构建最终生成的源码(Source
对象),用于后续生成阶段(比如输出到 .js
文件中)。和 createSourceForAsset
相似,但这是用于模块本身,不是额外的资源文件。
逻辑概览:
-
如果
content
是 Buffer,直接返回RawSource
。 -
如果模块没有
identifier
,也返回RawSource
。 -
有 identifier 时,判断是否启用了 source map:
- 有 source map 且启用了:返回
SourceMapSource
。 - 启用了但没有 map:返回
OriginalSource
。 - 否则返回
RawSource
。
- 有 source map 且启用了:返回
js
/**
* 创建 source 对象,用于模块最终构建时产出。
*
* @param {string} context 构建上下文
* @param {string | Buffer} content 源码内容
* @param {(string | SourceMapSource | null)=} sourceMap 可选的 sourceMap
* @param {object=} associatedObjectForCache 用于缓存路径绑定信息
* @returns {Source} Webpack 构造的 source 实例
*/
createSource(context, content, sourceMap, associatedObjectForCache) {
if (Buffer.isBuffer(content)) {
return new RawSource(content);
}
if (!this.identifier) {
return new RawSource(content);
}
const identifier = this.identifier();
if (this.useSourceMap && sourceMap) {
return new SourceMapSource(
content,
contextifySourceUrl(context, identifier, associatedObjectForCache),
contextifySourceMap(
context,
/** @type {TODO} */ (sourceMap),
associatedObjectForCache
)
);
}
if (this.useSourceMap || this.useSimpleSourceMap) {
return new OriginalSource(
content,
contextifySourceUrl(context, identifier, associatedObjectForCache)
);
}
return new RawSource(content);
}