Webpack Loader 执行机制
一、Loader 链式调用机制
Loader 的执行分为 Pitch 阶段和 Normal 阶段,两者共同构成链式调用逻辑。
1. Pitch 阶段
- 执行顺序:从左到右(与 Normal 阶段相反)。
-
核心作用:拦截机制。如果某个 Loader 的
pitch
方法返回非undefined
值,直接跳过后续 Loader,进入 Normal 阶段的逆向执行。 -
伪代码逻辑:
const result = loaderA.pitch(remainingRequest, previousRequest, data); if (result !== undefined) { // 跳过后续 Loader,进入 Normal 阶段逆向执行 }
2. Normal 阶段
- 执行顺序:从右到左。
- 核心作用:实际处理文件内容,上一个 Loader 的输出是下一个 Loader 的输入。
二、源码转换流程(runLoaders
核心逻辑)
Webpack 使用 loader-runner
模块处理 Loader 链。以下是简化后的源码分析:
关键源码:runLoaders
函数(简化版)
function runLoaders(resource, loaders, context, callback) {
const loaderContext = context || {};
let loaderIndex = 0; // 当前执行的 Loader 索引
let processOptions = {
resourceBuffer: null,
readResource: fs.readFile.bind(fs)
};
// 迭代执行 Pitch 阶段
iteratePitchingLoaders(processOptions, loaderContext, (err, result) => {
if (err) return callback(err);
callback(null, ...result);
});
function iteratePitchingLoaders(options, loaderContext, callback) {
if (loaderIndex >= loaders.length) {
// 所有 Pitch 执行完毕,读取资源
return processResource(options, loaderContext, callback);
}
const currentLoader = loaders[loaderIndex];
const pitchFn = currentLoader.pitch;
loaderIndex++; // 移动到下一个 Loader
if (!pitchFn) {
// 没有 pitch 方法,继续下一个 Loader
return iteratePitchingLoaders(options, loaderContext, callback);
}
// 执行当前 Loader 的 pitch 方法
pitchFn.call(
loaderContext,
loaderContext.remainingRequest,
loaderContext.previousRequest,
(currentLoader.data = {})
), (err, ...args) => {
if (args.length > 0) {
const hasResult = args.some(arg => arg !== undefined);
if (hasResult) {
// Pitch 返回结果,跳过后续 Loader,逆向执行 Normal
loaderIndex--;
iterateNormalLoaders(options, loaderContext, args, callback);
return;
}
}
// 继续下一个 Pitch
iteratePitchingLoaders(options, loaderContext, callback);
});
}
function processResource(options, loaderContext, callback) {
// 读取原始资源内容
options.readResource(loaderContext.resource, (err, buffer) => {
const resourceBuffer = buffer;
iterateNormalLoaders(options, loaderContext, [resourceBuffer], callback);
});
}
function iterateNormalLoaders(options, loaderContext, args, callback) {
if (loaderIndex < 0) {
// 所有 Normal 阶段完成
return callback(null, args);
}
const currentLoader = loaders[loaderIndex];
const normalFn = currentLoader.normal || currentLoader;
loaderIndex--; // 逆向执行
// 执行当前 Loader 的 Normal 方法
normalFn.call(loaderContext, args[0], (err, ...returnArgs) => {
if (err) return callback(err);
iterateNormalLoaders(options, loaderContext, returnArgs, callback);
});
}
}
三、执行流程详解
-
Pitch 阶段从左到右执行:
- 依次调用每个 Loader 的
pitch
方法。 - 若某个
pitch
返回结果,跳过后续 Loader,直接进入 Normal 阶段。
- 依次调用每个 Loader 的
-
读取资源文件:
- 若所有
pitch
均未拦截,读取原始文件内容。
- 若所有
-
Normal 阶段从右到左执行:
- 将资源内容传递给最后一个 Loader 处理,结果逆向传递。
四、典型使用案例
案例:自定义 Loader 链观察执行顺序
Loader 配置:
// webpack.config.js
module.exports = {
module: {
rules: [
{
test: /\.txt$/,
use: [
'./loaders/loaderA.js',
'./loaders/loaderB.js',
'./loaders/loaderC.js'
]
}
]
}
};
Loader 实现:
// loaderA.js
module.exports = function(source) {
console.log('[Normal A]');
return source + '-A';
};
module.exports.pitch = function() {
console.log('[Pitch A]');
};
// loaderB.js
module.exports = function(source) {
console.log('[Normal B]');
return source + '-B';
};
module.exports.pitch = function() {
console.log('[Pitch B]');
// 返回非 undefined 值,拦截后续 Loader
return '拦截内容';
};
// loaderC.js
module.exports = function(source) {
console.log('[Normal C]');
return source + '-C';
};
module.exports.pitch = function() {
console.log('[Pitch C]');
};
执行结果:
[Pitch A]
[Pitch B] // B 的 pitch 返回拦截内容,跳过后续 Pitch
[Normal B] // 进入 Normal 阶段,从 B 开始逆向执行
[Normal A]
最终结果: "拦截内容-B-A"
五、关键总结
-
Pitch 拦截:通过
pitch
方法提前返回结果,优化构建流程。 -
执行方向:
- Pitch:从左到右。
- Normal:从右到左(若未拦截)。
-
资源处理:
runLoaders
通过iteratePitchingLoaders
和iterateNormalLoaders
实现链式调用。