Webpack Loader 执行机制

一、Loader 链式调用机制

Loader 的执行分为 Pitch 阶段Normal 阶段,两者共同构成链式调用逻辑。


1. Pitch 阶段
  • 执行顺序:从左到右(与 Normal 阶段相反)。

  • 核心作用 :拦截机制。如果某个 Loader 的 pitch 方法返回非 undefined 值,直接跳过后续 Loader,进入 Normal 阶段的逆向执行。

  • 伪代码逻辑

    javascript 复制代码
    const result = loaderA.pitch(remainingRequest, previousRequest, data);
    if (result !== undefined) {
      // 跳过后续 Loader,进入 Normal 阶段逆向执行
    }
2. Normal 阶段
  • 执行顺序:从右到左。
  • 核心作用:实际处理文件内容,上一个 Loader 的输出是下一个 Loader 的输入。

二、源码转换流程(runLoaders 核心逻辑)

Webpack 使用 loader-runner 模块处理 Loader 链。以下是简化后的源码分析:

关键源码:runLoaders 函数(简化版)
javascript 复制代码
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 配置

javascript 复制代码
// webpack.config.js
module.exports = {
  module: {
    rules: [
      {
        test: /\.txt$/,
        use: [
          './loaders/loaderA.js',
          './loaders/loaderB.js',
          './loaders/loaderC.js'
        ]
      }
    ]
  }
};

Loader 实现

javascript 复制代码
// 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]');
};

执行结果

css 复制代码
[Pitch A]
[Pitch B]  // B 的 pitch 返回拦截内容,跳过后续 Pitch
[Normal B] // 进入 Normal 阶段,从 B 开始逆向执行
[Normal A]
最终结果: "拦截内容-B-A"

五、关键总结

  1. Pitch 拦截 :通过 pitch 方法提前返回结果,优化构建流程。
  2. 执行方向
    • Pitch:从左到右。
    • Normal:从右到左(若未拦截)。
  3. 资源处理runLoaders 通过 iteratePitchingLoadersiterateNormalLoaders 实现链式调用。
相关推荐
一直在学习的小白~22 分钟前
node_modules 明明写进 .gitignore,却还是被 push/commit 的情况
前端·javascript·vue.js
前端小超超1 小时前
如何配置capacitor 打包的ios app固定竖屏展示?
前端·ios·web app
nightunderblackcat1 小时前
新手向:从零理解LTP中文文本处理
前端·javascript·easyui
kyle~1 小时前
python---PyInstaller(将Python脚本打包为可执行文件)
开发语言·前端·python·qt
User:你的影子1 小时前
WPF ItemsControl 绑定
开发语言·前端·javascript
会有钱的-_-1 小时前
基于webpack的场景解决
前端·vue.js·webpack·安全性测试
帅气的小峰2 小时前
【源码剖析】4-生产者-KafkaProducer分析
kafka·源码·实时计算
LFly_ice2 小时前
学习React-10-useTransition
前端·学习·react.js
咔咔一顿操作2 小时前
【CSS 3D 交互】实现精美翻牌效果:从原理到实战
前端·css·3d·交互·css3
知识分享小能手2 小时前
React学习教程,从入门到精通,React 构造函数(Constructor)完整语法知识点与案例详解(16)
前端·javascript·学习·react.js·架构·前端框架·vue