webpack 的 loader 按「生效位置」分成四类:normal、pre、post、inline。大部分时候我们只用 normal,但在需要控制处理先后(比如先做代码检查、最后做某种收尾处理)时,就必须理解这四类以及它们的执行顺序。本文把这套机制讲清楚。
一、四类 loader 分别是什么
1. normal loader(普通 loader)
最常用的类型,直接写在 module.rules 里,不额外声明 enforce:
module.exports = {
module: {
rules: [
{ test: /\.js$/, use: 'babel-loader' },
],
},
};
2. pre loader(前置 loader)
通过 enforce: 'pre' 声明,表示在普通 loader 之前执行:
{
test: /\.js$/,
enforce: 'pre',
use: 'eslint-loader',
}
典型用途是代码检查:要在 Babel 转译之前检查原始源码,所以必须前置。
3. post loader(后置 loader)
通过 enforce: 'post' 声明,表示在普通 loader 之后执行:
{
test: /\.js$/,
enforce: 'post',
use: 'some-post-loader',
}
用于需要在所有常规处理完成后才做的事,比如对最终产物做某种加工。
4. inline loader(内联 loader)
不在配置文件里写,而是直接写在 import / require 语句中,用 ! 分隔:
import styles from 'style-loader!css-loader!./styles.css';
它也可以传递参数:
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 阶段之前执行,用于提前拦截、传递数据。
// 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 从右到左」,就得到最终的执行顺序:
| 阶段 | 顺序 |
|---|---|
| pitch | post → 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 都禁用) |
示例:
// 只用 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 类别,顺序由前缀和固有规则决定。
五、完整示例
配置:
module.exports = {
module: {
rules: [
{ test: /\.js$/, enforce: 'pre', use: 'pre-loader' },
{ test: /\.js$/, use: 'normal-loader' },
{ test: /\.js$/, enforce: 'post', use: 'post-loader' },
],
},
};
代码中内联一个 loader:
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 最先执行。
六、注意事项
enforce只对配置里的规则有效,inline loader 无法声明 pre/post。- pitch 返回值会中断链,调试时若发现某些 loader 没执行,先检查是否有 loader 的
pitch提前返回了。 - 顺序错了结果就错了:最常见的是 ESLint / 类型检查放在 Babel 之后,导致检查对象变成转译产物。用
enforce: 'pre'固定。 -!和!!容易记混:记忆方法是「!数量越多,禁用得越多」——-!禁用 pre 和 normal,!!禁用全部。- 同一 rule 的
use数组是右到左,写配置时按「从后往前」的直觉排列容易搞反。
小结
| 类型 | 声明方式 | normal 阶段位置 |
|---|---|---|
| pre | enforce: 'pre' | 最先 |
| normal | 默认 | 其次 |
| inline | import 中 ! 连接 | 再次 |
| post | enforce: 'post' | 最后 |
- 数组顺序:
[post, inline, normal, pre] - pitch 顺序:post → inline → normal → pre
- 执行顺序:pre → normal → inline → post
一句话:loader 是一条双阶段流水线——pitch 从左往右,处理源码从右往左;四类 loader 的处理顺序是 pre → normal → inline → post。 记住这条,就能准确控制每个 loader 在什么时机介入。