创见博客
webpack loader 的四种类型与执行顺序:pre、normal、inline、post
七崽爱吃小饼干2026/09/18阅读 0专栏 webpack原理

webpack 的 loader 按「生效位置」分成四类:normal、pre、post、inline。大部分时候我们只用 normal,但在需要控制处理先后(比如先做代码检查、最后做某种收尾处理)时,就必须理解这四类以及它们的执行顺序。本文把这套机制讲清楚。

一、四类 loader 分别是什么

1. normal loader(普通 loader)

最常用的类型,直接写在 module.rules 里,不额外声明 enforce:

js
module.exports = {
  module: {
    rules: [
      { test: /\.js$/, use: 'babel-loader' },
    ],
  },
};

2. pre loader(前置 loader)

通过 enforce: 'pre' 声明,表示在普通 loader 之前执行:

js
{
  test: /\.js$/,
  enforce: 'pre',
  use: 'eslint-loader',
}

典型用途是代码检查:要在 Babel 转译之前检查原始源码,所以必须前置。

3. post loader(后置 loader)

通过 enforce: 'post' 声明,表示在普通 loader 之后执行:

js
{
  test: /\.js$/,
  enforce: 'post',
  use: 'some-post-loader',
}

用于需要在所有常规处理完成后才做的事,比如对最终产物做某种加工。

4. inline loader(内联 loader)

不在配置文件里写,而是直接写在 import / require 语句中,用 ! 分隔:

js
import styles from 'style-loader!css-loader!./styles.css';

它也可以传递参数:

js
import styles from 'style-loader!css-loader?modules!./styles.css';

为什么需要 pre / post

考虑一个 JS 文件要同时经过 ESLint 和 Babel:如果顺序不确定,Babel 先把代码转译了,ESLint 检查的就是转译后的产物,规范和定位都会失真。用 enforce: 'pre' 把 ESLint 固定在最前,才能保证检查的是源码。post 同理,用于确保「最后执行」。

二、loader 的两个阶段:pitch 与 normal

理解执行顺序前,必须先知道每个 loader 其实有两个可选阶段:

  • normal 阶段:普通的 loader 函数,接收上一个 loader 的输出(source),返回处理后的内容。
  • pitch 阶段:可选的 pitch 函数,在 normal 阶段之前执行,用于提前拦截、传递数据。
js
// my-loader.js
module.exports = function (source) {
  // normal 阶段
  return source;
};

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

关键规则是:

  • pitch 阶段:从左到右执行。
  • normal 阶段:从右到左执行。
  • 如果某个 loader 的 pitch 返回了值,就会跳过它右边所有 loader 的 pitch 和 normal,直接把该值交给左边的 loader。

用一条 use: ['a', 'b', 'c'] 的链来记:

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

三、四类 loader 的完整执行顺序

把四类 loader 合到一条链上,实际的数组顺序是:

[ post, inline, normal, pre ]

基于「pitch 从左到右、normal 从右到左」,就得到最终的执行顺序:

阶段顺序
pitchpost → inline → normal → pre
normal(真正处理源码)pre → normal → inline → post

换句话说:

  • 处理源码时:pre 最先,接着 normal,然后 inline,最后 post。
  • pitch 阶段:完全相反,post 最先,pre 最后。

只记 normal 阶段那句口诀即可:pre → normal → inline → post。

单条规则里的 use 数组

同一类 loader 内部如果有多个(比如 use: ['style-loader', 'css-loader']),同样遵循「normal 从右到左」:先执行 css-loader,再执行 style-loader。

四、inline loader 的三种前缀

内联 loader 可以通过前缀,决定是否屏蔽配置里的其他 loader:

前缀含义效果
无正常叠加inline + 所有已配置 loader
!禁用 normal只用 inline + pre + post
-!禁用 pre 和 normal只用 inline + post
!!禁用全部只用 inline(pre、normal、post 都禁用)

示例:

js
// 只用 inline 指定的 loader,忽略配置里的所有 loader
import data from '!!raw-loader!./data.txt';

// 用 inline + 配置里的 pre/post,忽略 normal
import data from '!raw-loader!./data.txt';

注意:inline loader 不能设置 enforce,它天然属于 inline 类别,顺序由前缀和固有规则决定。

五、完整示例

配置:

js
module.exports = {
  module: {
    rules: [
      { test: /\.js$/, enforce: 'pre', use: 'pre-loader' },
      { test: /\.js$/, use: 'normal-loader' },
      { test: /\.js$/, enforce: 'post', use: 'post-loader' },
    ],
  },
};

代码中内联一个 loader:

js
import result from 'inline-loader!./file.js';

此时对 file.js 生效的 loader 数组是 [post-loader, inline-loader, normal-loader, pre-loader],于是:

  • pitch 阶段:post → inline → normal → pre
  • normal 阶段:pre → normal → inline → post

如果给每个 loader 都打上日志,运行时会先看到 post 的 pitch,最后看到 post 的 normal,中间则是 pre 的 normal 最先执行。

六、注意事项

  1. enforce 只对配置里的规则有效,inline loader 无法声明 pre/post。
  2. pitch 返回值会中断链,调试时若发现某些 loader 没执行,先检查是否有 loader 的 pitch 提前返回了。
  3. 顺序错了结果就错了:最常见的是 ESLint / 类型检查放在 Babel 之后,导致检查对象变成转译产物。用 enforce: 'pre' 固定。
  4. -! 和 !! 容易记混:记忆方法是「! 数量越多,禁用得越多」——-! 禁用 pre 和 normal,!! 禁用全部。
  5. 同一 rule 的 use 数组是右到左,写配置时按「从后往前」的直觉排列容易搞反。

小结

类型声明方式normal 阶段位置
preenforce: 'pre'最先
normal默认其次
inlineimport 中 ! 连接再次
postenforce: 'post'最后
  • 数组顺序:[post, inline, normal, pre]
  • pitch 顺序:post → inline → normal → pre
  • 执行顺序:pre → normal → inline → post

一句话:loader 是一条双阶段流水线——pitch 从左往右,处理源码从右往左;四类 loader 的处理顺序是 pre → normal → inline → post。 记住这条,就能准确控制每个 loader 在什么时机介入。

评论
0/100