Webpack Loader 执行机制

11 阅读2分钟

一、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);
    });
  }
}

三、执行流程详解

  1. Pitch 阶段从左到右执行

    • 依次调用每个 Loader 的 pitch 方法。
    • 若某个 pitch 返回结果,跳过后续 Loader,直接进入 Normal 阶段。
  2. 读取资源文件

    • 若所有 pitch 均未拦截,读取原始文件内容。
  3. 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"

五、关键总结

  1. Pitch 拦截:通过 pitch 方法提前返回结果,优化构建流程。
  2. 执行方向
    • Pitch:从左到右。
    • Normal:从右到左(若未拦截)。
  3. 资源处理runLoaders 通过 iteratePitchingLoadersiterateNormalLoaders 实现链式调用。