FORMA

Loader

Loader 在 Webpack 构建链中把源文件转为可打包的 JS 模块(如 TS、SCSS、Vue SFC)。见 工作原理

一、Loader 执行顺序

Loader 的执行顺序遵循 从右向左、从下到上 的原则,但通过 enforce 可以改变优先级。

1. 默认顺序(normal loader)

在配置数组中,Loader 从后往前执行(类似洋葱模型)。例如:

js
module: {
  rules: [{ test: /\.js$/, use: ["loader-c", "loader-b", "loader-a"] }];
}

执行顺序:loader-aloader-bloader-c

2. enforce 分类

可以为 Loader 指定 enforce 属性,分为四类,执行顺序为:

text
pre → normal → inline → post
  • pre:前置 loader(enforce: 'pre'
  • normal:普通 loader(默认)
  • inline:内联 loader(在 import 语句中直接使用,如 import '!style-loader!css-loader!./a.css'
  • post:后置 loader(enforce: 'post'

示例:

js
rules: [
  { test: /\.js$/, loader: "pre-loader", enforce: "pre" },
  { test: /\.js$/, loader: "normal-loader" }, // enforce 默认 'normal'
  { test: /\.js$/, loader: "post-loader", enforce: "post" },
];

执行顺序:pre-loadernormal-loaderpost-loader。如果有内联 loader,会在 normal 之后、post 之前执行。

3. 内联 loader 的覆盖符号

在 import 语句中,可以使用前缀改变 loader 执行顺序:

  • ! 禁用 normal loader
  • -! 禁用 pre 和 normal loader
  • !! 禁用所有除了内联 loader
js
// 只使用 style-loader 和 css-loader,跳过 pre/normal/post
import "!!style-loader!css-loader!./a.css";

二、Loader 的输入与输出

Loader 接收文件内容(字符串或 Buffer),返回转换后的内容。

1. 同步 Loader

直接 return 转换后的内容。

js
module.exports = function (source) {
  // source 是原始文件内容
  const result = source.replace(/console\.log\(.*?\)/g, "");
  return result;
};

也可以使用 this.callback 返回多个值:

js
module.exports = function (source) {
  const result = process(source);
  this.callback(null, result, sourceMap, meta);
  // 不返回值 (return;)
};

this.callback 参数:

  • error:Error 或 null
  • content:转换后的代码(String 或 Buffer)
  • sourceMap:可选的 source map
  • meta:可选的元数据(如 AST 等)

2. 异步 Loader

使用 this.async() 获取一个回调函数。

js
module.exports = function (source) {
  const callback = this.async();
  someAsyncProcess(source, (err, result) => {
    if (err) return callback(err);
    callback(null, result);
  });
};

3. 返回多种内容

  • 内容类型:可以是 JavaScript 代码字符串,也可以是其他语言(如 CSS、HTML)—— Webpack 会继续用其他 Loader 处理。
  • Source Map:便于调试转换后的代码。
  • 元数据:例如在 loader 中生成 AST,可以传递给下一个 loader。

三、Loader 工具库

1. loader-utils

提供常用的辅助函数。

js
const { getOptions, interpolateName } = require("loader-utils");

module.exports = function (source) {
  // 获取 loader 配置中的 options
  const options = getOptions(this);

  // 生成文件名(带 hash 等)
  const filename = interpolateName(this, "[name].[hash].[ext]", {
    content: source,
    context: this.rootContext,
  });

  // 发出一个文件(例如生成图片)
  this.emitFile(filename, source);

  return `module.exports = ${JSON.stringify(filename)};`;
};
  • interpolateName:支持占位符如 [name][ext][hash][contenthash][path] 等。
  • getOptions:从 loader context 中安全地提取 options。
  • parseQuery:解析类似 URL 的 query 字符串(较少用)。
  • stringifyRequest:将模块请求转换为相对于当前文件的相对路径。

2. schema-utils

校验 loader options 的 JSON Schema。

js
const { validate } = require("schema-utils");
const schema = {
  type: "object",
  properties: {
    test: { type: "string" },
    limit: { type: "number" },
  },
};

module.exports = function (source) {
  const options = getOptions(this);
  validate(schema, options, { name: "My Loader" });
  // ...
};

四、自定义 Loader 典型场景

1. 语言预处理(替换模板变量)

例如,将文件中的 {{VERSION}} 替换为构建时的版本号。

js
// version-loader.js
module.exports = function (source) {
  const version = process.env.npm_package_version || "0.0.0";
  return source.replace(/{{VERSION}}/g, version);
};

2. 按需引入替换(优化组件库导入)

import { Button, Input } from 'ui-library' 转换为具体路径,实现按需引入。

js
// optimize-import-loader.js
module.exports = function (source) {
  return source.replace(
    /import\s*\{\s*([^}]+)\s*\}\s*from\s*['"]ui-library['"]/g,
    (match, names) => {
      const imports = names
        .split(",")
        .map((name) => {
          const trimmed = name.trim();
          return `import ${trimmed} from 'ui-library/lib/${trimmed}';`;
        })
        .join("\n");
      return imports;
    },
  );
};

3. 移除调试代码(console.log、debugger)

js
// strip-debug-loader.js
module.exports = function (source) {
  const result = source.replace(/console\.(log|debug|info|warn)\(.*?\)\s*;?/g, "");
  return result.replace(/debugger\s*;?/g, "");
};

配合 enforce: 'pre' 确保在其他 loader 之前执行。

五、Pitch Loader(熔断机制)

每个 Loader 除了默认的 (默认的转换函数) 外,还可以导出一个 pitch 方法。pitch 在 loader 执行顺序的第一阶段被调用,它有机会短路后续的 loader。

1. pitch 的执行顺序

对于一组 loader [a, b, c]

  1. 先调用 a.pitchb.pitchc.pitch
  2. 如果某个 pitch 返回了非 undefined 的值,则停止执行后面的 pitch,并开始返回阶段:直接返回该结果给上一个 loader 的 normal 函数。

2. 短路机制

如果 b.pitch 返回了一个值,那么:

  • 不再调用 c.pitchc 的 normal 函数。
  • b 的 normal 函数不会被调用(因为 pitch 已经返回)。
  • 该值直接返回给 a 的 normal 函数(作为输入)。
  • 然后执行 a 的 normal 函数,最后输出。

示意图:

text
normal 执行顺序: a → b → c
pitch 执行顺序: a.pitch → b.pitch → c.pitch
如果 b.pitch 返回内容,则跳过 c 的所有函数,并且 b 的 normal 也不执行,结果回传给 a.normal。

3. pitch 的典型用途

  • 提前返回:如果 loader 已经知道最终输出(例如文件是空的、不需要处理),可以直接返回。
  • 收集信息:在 pitch 阶段读取配置或修改请求路径(例如 style-loader 使用 pitch 将 CSS 注入到 JS 中)。
  • 动态添加依赖this.addDependency 等。

示例:一个简单的 pitch loader,跳过后续处理

js
// skip-loader.js
module.exports = function (source) {
  // 正常情况下不会被调用(如果 pitch 返回了值)
  return source;
};
module.exports.pitch = function (remainingRequest, precedingRequest, data) {
  // 直接返回一段代码,不再执行后面的 loader
  return `module.exports = "skipped";`;
};

4. pitch 参数说明

  • remainingRequest:剩余请求字符串(从当前 loader 之后的所有 loader 加上资源路径)。
  • precedingRequest:前面的请求字符串。
  • data:可在 pitch 和 normal 之间共享的对象。

总结

方面关键点
执行顺序enforce: 'pre' > normal > inline > 'post';默认从右向左执行
同步/异步同步用 returnthis.callback;异步用 this.async()
工具库loader-utils(参数、命名)、schema-utils(校验)
典型场景变量替换、按需引入移除调试代码
Pitch Loader短路后续 loader,用于提前返回或修改请求

通过开发自定义 Loader,可以定制 Webpack 处理各种文件的方式,满足工程化中的特殊需求。高级用法常与 pitch 配合,实现类似 style-loader 的动态注入逻辑。

参考文献

以下链接在编写时均可正常访问:

资料说明
Loader官方
编写 Loader指南

Series

webpack

4 / 6