这是 Webpack 中 NormalModule
的核心方法之一,负责执行模块构建,主要包括:
- 创建
loaderContext
,为所有 loader 提供统一上下文环境; - 调用
runLoaders
依次执行所有配置的 loader,对资源进行转换; - 支持
readResource
钩子用于读取资源内容; - 收集构建时产生的依赖(文件、目录、缺失、loader 本身);
- 处理构建结果(支持 SourceMap、AST、缓存等);
- 调用回调
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)
🌟功能总结:
该方法在构建模块过程中出现错误时调用,用来:
- 恢复模块上一次构建成功时的
buildMeta
; - 将当前模块标记为构建失败;
- 将错误信息添加到模块的错误列表中。
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
的封装,用于对模块请求路径判断是否应跳过解析。
判断逻辑:
- 如果没有
noParseRule
,则必须解析; - 如果是单个规则(非数组),直接调用
applyNoParseRule
; - 如果是数组规则,逐个尝试匹配,只要有一个匹配就跳过解析。
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;
}