webpack 格式化模块 第 三 节

1. getUnsafeCacheData()

用处:

将模块的某些关键信息(如 parserOptionsgeneratorOptions)保存下来,以便未来可以从缓存中快速恢复模块状态。用于优化模块构建的性能。

逻辑概览:

  1. 调用父类的 getUnsafeCacheData() 获取基础缓存数据。
  2. 将当前模块特有的配置项(parserOptionsgeneratorOptions)附加上去。
  3. 返回这个组合后的数据对象。
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)

用处:

外部调用接口,从缓存数据中恢复模块状态。避免重新解析模块,提高构建效率。

逻辑概览:

  1. 只是一个封装函数,内部调用 _restoreFromUnsafeCache() 方法处理实际的恢复工作。
js 复制代码
/**
 * 从非安全缓存中恢复模块状态。
 * 外部调用入口。
 *
 * @param {NormalModuleUnsafeCacheData} unsafeCacheData 从 getUnsafeCacheData 获取的数据
 * @param {NormalModuleFactory} normalModuleFactory 正在处理该模块的模块工厂
 */
restoreFromUnsafeCache(unsafeCacheData, normalModuleFactory) {
	this._restoreFromUnsafeCache(unsafeCacheData, normalModuleFactory);
}

_restoreFromUnsafeCache(unsafeCacheData, normalModuleFactory)

用处:

真正执行模块的恢复逻辑。使用之前缓存的配置项和工厂方法重新初始化模块的 parser 和 generator。

逻辑概览:

  1. 调用父类的 _restoreFromUnsafeCache()
  2. 读取缓存中的 parserOptionsgeneratorOptions
  3. 使用 normalModuleFactory 创建对应类型的 parser 和 generator。
  4. 不重新计算 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 实例),供写入输出文件使用。

逻辑概览:

  1. 如果提供了 sourceMap,并启用了 useSourceMap

    • 如果是字符串:使用 OriginalSource
    • 如果是对象:使用 SourceMapSource
  2. 如果没有 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 和工具。

逻辑概览:

  1. 定义多个内部工具函数和 util 方法,如:

    • getOptions():解析 loader 的 options
    • emitError() / emitWarning():错误和警告上报。
    • resolve() / getResolve():模块解析接口。
    • emitFile():发出资源文件。
  2. 封装成一个 loaderContext 对象,包含所有功能和信息。

  3. 执行 loader hook(hooks.loader.call()),允许插件修改 loaderContext。

  4. 最终返回 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 信息(比如用于日志、错误来源标识等)。

逻辑概览:

  1. 检查当前 loader 数组是否存在,并且索引在范围内。
  2. 返回当前索引对应的 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 相似,但这是用于模块本身,不是额外的资源文件。

逻辑概览:

  1. 如果 content 是 Buffer,直接返回 RawSource

  2. 如果模块没有 identifier,也返回 RawSource

  3. 有 identifier 时,判断是否启用了 source map:

    • 有 source map 且启用了:返回 SourceMapSource
    • 启用了但没有 map:返回 OriginalSource
    • 否则返回 RawSource
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);
}
相关推荐
独立开阀者_FwtCoder1 小时前
CSS view():JavaScript 滚动动画的终结
前端·javascript·vue.js
咖啡教室1 小时前
用markdown语法制作一个好看的网址导航页面(markdown-web-nav)
前端·javascript·markdown
独立开阀者_FwtCoder1 小时前
Vue 团队“王炸”新作!又一打包工具发布!
前端·javascript·vue.js
天天扭码1 小时前
一分钟解决“3.无重复字符的最长字串问题”(最优解)
前端·javascript·算法
独立开阀者_FwtCoder1 小时前
Promise 引入全新 API!效率提升 300%!
前端·javascript·后端
陈明勇1 小时前
三句话搞定周末出行攻略!我用 AI 生成一日游可视化页面,还能秒上线!
前端·人工智能·mcp
_一条咸鱼_1 小时前
Vue 样式深入剖析:从基础到源码级理解(十)
前端·javascript·面试
懒羊羊我小弟1 小时前
Vue与React组件化设计对比
前端·vue.js·react.js
_朱志强1 小时前
解决前端vue项目在linux上,npm install,node-sass 安装失败的问题
linux·前端·vue.js
excel2 小时前
webpack 检出图 第 二 节
前端