webpack 格式化模块 第 四 节

这是 Webpack 中 NormalModule 的核心方法之一,负责执行模块构建,主要包括:

  1. 创建 loaderContext,为所有 loader 提供统一上下文环境;
  2. 调用 runLoaders 依次执行所有配置的 loader,对资源进行转换;
  3. 支持 readResource 钩子用于读取资源内容;
  4. 收集构建时产生的依赖(文件、目录、缺失、loader 本身);
  5. 处理构建结果(支持 SourceMap、AST、缓存等);
  6. 调用回调 callback 返回结果或错误。
js 复制代码
/**
 * @param {WebpackOptions} options webpack 配置项
 * @param {Compilation} compilation 当前 compilation 实例
 * @param {ResolverWithOptions} resolver 模块路径解析器
 * @param {InputFileSystem} fs 输入文件系统(用于读取文件)
 * @param {NormalModuleCompilationHooks} hooks 构建钩子集合
 * @param {function((WebpackError | null)=): void} callback 构建完成后的回调函数
 * @returns {void}
 */
_doBuild(options, compilation, resolver, fs, hooks, callback) {
	// 创建 loader 执行上下文,供后续所有 loader 使用
	const loaderContext = this._createLoaderContext(
		resolver,
		options,
		compilation,
		fs,
		hooks
	);

	// 定义 loader 执行的结果类型:[源码内容, sourceMap, 额外信息]
	/** @typedef {[string | Buffer, string | SourceMapSource, Record<string, any>]}  Result */

	/**
	 * 处理构建结果(包括错误和正常构建结果)
	 * @param {Error | null} err 错误信息
	 * @param {(Result | null)=} _result loader 处理结果
	 * @returns {void}
	 */
	const processResult = (err, _result) => {
		if (err) {
			// 如果错误不是 Error 实例,转换为 Webpack 自定义错误类型
			if (!(err instanceof Error)) {
				err = new NonErrorEmittedError(err);
			}
			// 获取当前执行的 loader(用于错误信息定位)
			const currentLoader = this.getCurrentLoader(loaderContext);
			const error = new ModuleBuildError(err, {
				from:
					currentLoader &&
					compilation.runtimeTemplate.requestShortener.shorten(
						currentLoader.loader
					)
			});
			// 通过回调将错误传出
			return callback(error);
		}

		// 强制类型转换
		const result = /** @type {Result} */ (_result);
		const source = result[0];
		const sourceMap = result.length >= 1 ? result[1] : null;
		const extraInfo = result.length >= 2 ? result[2] : null;

		// 如果最终结果既不是 string 也不是 Buffer,报错
		if (!Buffer.isBuffer(source) && typeof source !== "string") {
			const currentLoader = this.getCurrentLoader(loaderContext, 0);
			const err = new Error(
				`最终 loader (${
					currentLoader
						? compilation.runtimeTemplate.requestShortener.shorten(
								currentLoader.loader
							)
						: "unknown"
				}) 没有返回 Buffer 或 String`
			);
			const error = new ModuleBuildError(err);
			return callback(error);
		}

		// 是否为二进制模块
		const isBinaryModule =
			this.generatorOptions && this.generatorOptions.binary !== undefined
				? this.generatorOptions.binary
				: this.binary;

		// 创建模块源代码(最终产物)
		this._source = this.createSource(
			/** @type {string} */ (options.context),
			isBinaryModule ? asBuffer(source) : asString(source),
			sourceMap,
			compilation.compiler.root
		);

		// 清除旧的 source size 缓存
		if (this._sourceSizes !== undefined) this._sourceSizes.clear();

		// 如果提供 AST,则保存(供后续优化、分析用)
		this._ast =
			typeof extraInfo === "object" &&
			extraInfo !== null &&
			extraInfo.webpackAST !== undefined
				? extraInfo.webpackAST
				: null;

		// 成功回调
		return callback();
	};

	// 构建信息对象
	const buildInfo = /** @type {BuildInfo} */ (this.buildInfo);
	buildInfo.fileDependencies = new LazySet(); // 依赖的文件
	buildInfo.contextDependencies = new LazySet(); // 上下文依赖
	buildInfo.missingDependencies = new LazySet(); // 缺失依赖
	buildInfo.cacheable = true; // 默认构建结果是可缓存的

	// 执行构建前钩子(beforeLoaders)
	try {
		hooks.beforeLoaders.call(
			this.loaders,
			this,
			/** @type {LoaderContext<any>} */ (loaderContext)
		);
	} catch (err) {
		processResult(/** @type {Error} */ (err));
		return;
	}

	// 如果存在 loaders,则初始化 buildDependencies
	if (this.loaders.length > 0) {
		/** @type {BuildInfo} */
		(this.buildInfo).buildDependencies = new LazySet();
	}

	// 调用 loader-runner 执行所有 loaders
	runLoaders(
		{
			resource: this.resource, // 模块路径
			loaders: this.loaders,   // 所有的 loader
			context: loaderContext,  // loader 上下文对象

			// 自定义资源加载逻辑(可被插件拦截)
			processResource: (loaderContext, resourcePath, callback) => {
				const resource = loaderContext.resource;
				const scheme = getScheme(resource); // 如 file://、http://
				hooks.readResource
					.for(scheme)
					.callAsync(loaderContext, (err, result) => {
						if (err) return callback(err);
						if (typeof result !== "string" && !result) {
							// 不支持的协议
							return callback(
								new UnhandledSchemeError(
									/** @type {string} */ (scheme),
									resource
								)
							);
						}
						return callback(null, result); // 返回原始源码内容
					});
			}
		},
		// 所有 loader 执行完成的回调
		(err, result) => {
			// 清理 loaderContext 上挂载的临时属性(避免泄漏)
			loaderContext._compilation =
				loaderContext._compiler =
				loaderContext._module =
				// eslint-disable-next-line no-warning-comments
				// @ts-ignore
				loaderContext.fs =
					undefined;

			// 没有结果,说明构建失败
			if (!result) {
				/** @type {BuildInfo} */
				(this.buildInfo).cacheable = false;
				return processResult(
					err || new Error("loader-runner 没有返回任何结果"),
					null
				);
			}

			const buildInfo = /** @type {BuildInfo} */ (this.buildInfo);

			// 合并 loader 收集到的依赖信息
			const fileDependencies =
				/** @type {NonNullable<KnownBuildInfo["fileDependencies"]>} */
				(buildInfo.fileDependencies);
			const contextDependencies =
				/** @type {NonNullable<KnownBuildInfo["contextDependencies"]>} */
				(buildInfo.contextDependencies);
			const missingDependencies =
				/** @type {NonNullable<KnownBuildInfo["missingDependencies"]>} */
				(buildInfo.missingDependencies);

			fileDependencies.addAll(result.fileDependencies);
			contextDependencies.addAll(result.contextDependencies);
			missingDependencies.addAll(result.missingDependencies);

			// 将每个 loader 的路径加入构建依赖
			for (const loader of this.loaders) {
				const buildDependencies =
					/** @type {NonNullable<KnownBuildInfo["buildDependencies"]>} */
					(buildInfo.buildDependencies);
				buildDependencies.add(loader.loader);
			}

			// 标记缓存性
			buildInfo.cacheable = buildInfo.cacheable && result.cacheable;

			// 处理 loader 的最终返回结果
			processResult(err, result.result);
		}
	);
}

markModuleAsErrored(error)

🌟功能总结:

该方法在构建模块过程中出现错误时调用,用来:

  1. 恢复模块上一次构建成功时的 buildMeta
  2. 将当前模块标记为构建失败;
  3. 将错误信息添加到模块的错误列表中。
js 复制代码
/**
 * 标记当前模块构建出错
 * @param {WebpackError} error 构建中出现的错误
 * @returns {void}
 */
markModuleAsErrored(error) {
	// 恢复上一次构建成功时的 buildMeta 信息
	this.buildMeta = { ...this._lastSuccessfulBuildMeta };
	this.error = error;
	this.addError(error);
}

applyNoParseRule(rule, content)

功能总结:

该方法用于根据 noParse 配置判断某个模块内容是否需要跳过解析(比如不执行 AST 解析以提升性能)。

支持三种规则类型:

  • 字符串:检查模块源码是否以该字符串开头;
  • 函数 :执行函数返回 true 表示跳过;
  • 正则表达式:测试是否匹配成功。
js 复制代码
/**
 * 应用 noParse 规则
 * @param {TODO} rule noParse 规则(可以是字符串、函数或正则表达式)
 * @param {string} content 模块源码内容
 * @returns {boolean} 是否不进行解析
 */
applyNoParseRule(rule, content) {
	if (typeof rule === "string") {
		return content.startsWith(rule); // 以该字符串开头就跳过解析
	}
	if (typeof rule === "function") {
		return rule(content); // 自定义函数返回 true 就跳过
	}
	return rule.test(content); // 正则匹配
}

shouldPreventParsing(noParseRule, request)

功能总结:

这是 applyNoParseRule 的封装,用于对模块请求路径判断是否应跳过解析

判断逻辑:

  1. 如果没有 noParseRule,则必须解析;
  2. 如果是单个规则(非数组),直接调用 applyNoParseRule
  3. 如果是数组规则,逐个尝试匹配,只要有一个匹配就跳过解析。
js 复制代码
/**
 * 判断模块是否应跳过解析
 * @param {TODO} noParseRule noParse 配置项
 * @param {string} request 请求路径
 * @returns {boolean} true 表示跳过解析,false 表示进行解析
 */
shouldPreventParsing(noParseRule, request) {
	if (!noParseRule) {
		return false;
	}
	if (!Array.isArray(noParseRule)) {
		return this.applyNoParseRule(noParseRule, request);
	}
	for (let i = 0; i < noParseRule.length; i++) {
		const rule = noParseRule[i];
		if (this.applyNoParseRule(rule, request)) {
			return true;
		}
	}
	return false;
}
相关推荐
_一条咸鱼_33 分钟前
深入解析 Vue API 模块原理:从基础到源码的全方位探究(八)
前端·javascript·面试
患得患失9491 小时前
【前端】【难点】前端富文本开发的核心难点总结与思路优化
前端·富文本
执键行天涯1 小时前
在vue项目中package.json中的scripts 中 dev:“xxx“中的xxx什么概念
前端·vue.js·json
雯0609~1 小时前
html:文件上传-一次性可上传多个文件,将文件展示到页面(可删除
前端·html
涵信1 小时前
2024年React最新高频面试题及核心考点解析,涵盖基础、进阶和新特性,助你高效备战
前端·react.js·前端框架
mmm.c1 小时前
应对多版本vue,nvm,node,npm,yarn的使用
前端·vue.js·npm
混血哲谈1 小时前
全新电脑如何快速安装nvm,npm,pnpm
前端·npm·node.js
天天扭码1 小时前
项目登录注册页面太丑?试试我“仿制”的丝滑页面(全源码可复制)
前端·css·html
桂月二二2 小时前
Vue3服务端渲染深度实战:SSR架构优化与企业级应用
前端·vue.js·架构
萌萌哒草头将军2 小时前
🚀🚀🚀 这六个事半功倍的 Pinia 库,你一定要知道!
前端·javascript·vue.js