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 实现链式调用。
相关推荐
斯~内克2 小时前
Electron 菜单系统深度解析:从基础到高级实践
前端·javascript·electron
数据知道2 小时前
【YAML】一文掌握 YAML 的详细用法(YAML 备忘速查)
前端·yaml
dr李四维2 小时前
vue生命周期、钩子以及跨域问题简介
前端·javascript·vue.js·websocket·跨域问题·vue生命周期·钩子函数
旭久2 小时前
react+antd中做一个外部按钮新增 表格内部本地新增一条数据并且支持编辑删除(无难度上手)
前端·javascript·react.js
windyrain2 小时前
ant design pro 模版简化工具
前端·react.js·ant design
浪遏2 小时前
我的远程实习(六) | 一个demo讲清Auth.js国外平台登录鉴权👈|nextjs
前端·面试·next.js
GISer_Jing3 小时前
React-Markdown详解
前端·react.js·前端框架
太阳花ˉ3 小时前
React(九)React Hooks
前端·react.js
拉不动的猪4 小时前
vue与react的简单问答
前端·javascript·面试