创见博客
webpack loader 的分类:同步、异步、raw、pitching
七崽爱吃小饼干2026/09/19阅读 0专栏 webpack原理

上一篇讲了 loader 的本质是「把源码转换成模块的函数」。但实际上,loader 不止一种形态:有的是简单同步函数,有的要处理异步任务,有的接收二进制数据,还有的能在「正式处理之前」抢先介入。本文按能力把 loader 分成四类:同步、异步、raw、pitching。

一、同步 loader

最常见的形态。loader 函数直接处理并 return 结果:

js
// loaders/replace-loader.js
module.exports = function (source) {
  return source.replace(/__VERSION__/g, '1.0.0');
};

如果还需要传递 Source Map 或额外的元信息,可以使用 this.callback(同样是同步调用):

js
module.exports = function (source) {
  // callback(err, content, sourceMap, meta)
  this.callback(null, source.toUpperCase(), null, { upper: true });
};

特点

  • 实现简单,适合纯文本替换、拼接、简单转换。
  • 返回值就是下一个 loader 的输入。
  • this.callback 和 return 二选一,不要同时用。

适用场景:文本替换、注入 banner、简单语法转换。

二、异步 loader

当处理过程涉及异步操作(读文件、调用异步编译器、请求接口)时,必须用异步方式。通过 this.async() 拿到回调:

js
const fs = require('fs');
const path = require('path');

module.exports = function (source) {
  const callback = this.async();

  fs.readFile(path.resolve(__dirname, 'template.txt'), 'utf-8', (err, template) => {
    if (err) {
      callback(err); // 出错时把错误传给 callback
      return;
    }
    callback(null, template + source);
  });
};

关键规则

  1. 必须先调用 this.async() 获取 callback,之后所有逻辑通过它返回结果。
  2. callback 只能被调用一次,且签名是 (err, content, sourceMap, meta)。
  3. 调用 this.async() 后不要再 return 内容,返回值会被忽略,webpack 也会给出警告。
  4. 必须保证 callback 一定被调用,否则构建会一直挂起。

适用场景:babel-loader、sass-loader 这类内部有异步编译过程的 loader。

三、raw loader

默认情况下,loader 拿到的 source 是字符串。但图片、字体、压缩包等二进制文件不能用字符串安全表示——这时需要声明 raw:

js
// loaders/image-size.js
module.exports = function (source) {
  // 当 raw = true 时,source 是一个 Buffer
  const size = source.length;
  return `module.exports = ${size};`;
};

module.exports.raw = true;

特点

  • 设置 module.exports.raw = true 后,source 变成 Buffer。
  • 相应地,loader 返回的也应是 Buffer(或交给下一个 loader 处理的内容)。
  • 不设置时,webpack 会把内容按 UTF-8 转成字符串。

适用场景:处理图片、字体、二进制协议、需要按字节操作的场景。

四、pitching loader

除了主函数(normal 阶段),loader 还可以导出一个可选的 pitch 函数。它在 normal 阶段之前执行,用来「抢先介入」。

js
module.exports = function (source) {
  // normal 阶段
  return source;
};

module.exports.pitch = function (remainingRequest, precedingRequest, data) {
  // pitch 阶段
};

pitch 接收三个参数:

参数含义
remainingRequest当前 loader 右侧剩余 loader 和资源的请求串
precedingRequest当前 loader 左侧已执行 loader 的请求串
data在 pitch 与 normal 之间共享的数据对象

1. 执行时机

回顾 loader 链的两阶段:

pitch:  a ──▶ b ──▶ c
normal: a ◀── b ◀── c

pitch 从左到右,normal 从右到左。所以 pitch 是「进入正式处理前」的钩子。

2. 提前返回,跳过后续 loader

如果 pitch 返回了一个非 undefined 的值,右侧所有 loader 的 pitch 和 normal 都会被跳过,这个值直接作为结果交给左边:

js
module.exports.pitch = function (remainingRequest, precedingRequest, data) {
  if (process.env.SKIP === '1') {
    // 直接返回结果,不再执行右侧 loader
    return 'module.exports = "skipped";';
  }
};

这是 pitching loader 最强大的能力,常用于「短路」某些处理链。

3. 短路时的完整执行顺序(示例)

用一个具体例子把顺序看清楚。配置三个 loader:

js
// webpack.config.js
module.exports = {
  module: {
    rules: [
      {
        test: /\.js$/,
        use: ['a-loader', 'b-loader', 'c-loader'],
      },
    ],
  },
};

每个 loader 都打印日志:

js
// a-loader.js
module.exports = function (source) {
  console.log('a normal');
  return `/* a */ ${source}`;
};
module.exports.pitch = function () {
  console.log('a pitch');
};

// b-loader.js —— 它的 pitch 会返回一个值,触发短路
module.exports = function (source) {
  console.log('b normal');
  return source;
};
module.exports.pitch = function () {
  console.log('b pitch');
  return 'console.log("from b.pitch");';
};

// c-loader.js
module.exports = function (source) {
  console.log('c normal');
  return source;
};
module.exports.pitch = function () {
  console.log('c pitch');
};

情况一:没有任何 pitch 返回值(正常链)

a pitch → b pitch → c pitch → 读取文件 → c normal → b normal → a normal

情况二:b.pitch 返回了值(短路)

a pitch → b pitch(返回)→ a normal(以 b.pitch 的返回值为输入)

对比表:

阶段不短路b.pitch 短路
pitcha → b → ca → b(返回)
读文件读不读
normalc → b → aa

也就是说,一旦某个 loader 的 pitch 返回了值:

  • 它右侧所有 loader(含它自己)的 pitch 和 normal 都被跳过;
  • 资源文件不会被读取;
  • 该值直接交给它左侧的 loader 继续 normal 处理(示例中即 a 的 normal)。

用图表示:

不短路:
pitch   a ──▶ b ──▶ c
             读文件
normal  a ◀── b ◀── c

b.pitch 返回:
pitch   a ──▶ b(return)
normal  a        ← source = b.pitch 的返回值,c 与 b 均不执行、文件不读取

这也是 style-loader 能借助 pitch 工作的原因:它可以在 pitch 阶段直接产出一段代码并短路后续处理,从而避免重复的编译工作。

4. 在 pitch 和 normal 之间传数据

pitch 里的 data 对象会传给同一个 loader 的 normal 阶段:

js
module.exports = function (source) {
  return `/* ${this.data.msg} */\n${source}`;
};

module.exports.pitch = function (remainingRequest, precedingRequest, data) {
  data.msg = 'generated-by-pitch';
};

适用场景:需要提前拦截、短路处理链、或为后续处理准备上下文的 loader(如 style-loader 就借助 pitch 机制工作)。

说明:网上有时把这一类写成「patching loader」,正确的叫法是 pitching loader——因为它对应的是 loader 的 pitch 阶段。

五、四种能力可以组合

这四个分类是维度不同的,一个 loader 可以同时具备多种:

组合含义
同步 + pitch主处理是同步的,同时定义了 pitch
异步 + raw异步处理二进制数据
raw + pitchpitch 中即可决定是否跳过二进制处理
全部灵活但通常没必要

例如一个「异步读取并处理二进制图片」的 loader:

js
module.exports = function (source) {
  const callback = this.async(); // 异步
  // source 是 Buffer(raw)
  doSomethingAsync(source).then((res) => callback(null, res));
};
module.exports.raw = true;

module.exports.pitch = function (remainingRequest, precedingRequest, data) {
  data.startedAt = Date.now(); // pitch
};

六、对照总结

类型标志输入/输出适用
同步直接 return / this.callback字符串简单文本处理
异步this.async()字符串异步编译、IO
rawmodule.exports.raw = trueBuffer图片、二进制
pitchingmodule.exports.pitch—(预处理钩子)拦截、短路、传数据

七、编写 loader 的注意事项

  1. 一个 loader 只做一件事,需要多步时拆成多个串联,便于复用和测试。
  2. 异步 loader 必须且只能调用一次 callback,否则构建挂起或报错。
  3. 不要在异步 loader 里 return 内容,也不要混用同步与异步返回。
  4. raw 要成对考虑:设了 raw = true 就成了 Buffer 世界,输出也要匹配。
  5. pitch 返回值意味着「短路」,调试时若发现后续 loader 没执行,先检查 pitch。
  6. loader 应保持无副作用,写文件、改全局这类操作交给 plugin。

小结

loader 的四种形态,本质是在回答四个问题:

  • 同步还是异步? —— 处理逻辑是否涉及异步操作。
  • 文本还是二进制? —— 用 raw 切换字符串/Buffer。
  • 要不要提前介入? —— 用 pitch 在 normal 之前拦截、短路、传数据。

理解这四点,再看社区里那些复杂的 loader(如 style-loader、sass-loader、thread-loader),就能明白它们为什么长成那样,也更容易写出自己的 loader。

评论
0/100