2025年10月10日6 分钟
如何保证 Webpack Loader 按照预想方式工作
深入理解 Webpack Loader 的执行顺序、分类、Pitch 机制和调试技巧,确保 Loader 链按预期工作
Loader 的基本执行顺序
Webpack Loader 的执行顺序是从右到左、从下到上:
module: {
rules: [
{
test: /\.css$/,
use: ['style-loader', 'css-loader', 'postcss-loader'],
// ③ ←──────── ② ←──────── ①
// 注入 DOM 解析 CSS PostCSS 处理
},
],
}
源文件 .css
│
▼ ① postcss-loader(处理 Tailwind、autoprefixer)
PostCSS 处理后的 CSS
│
▼ ② css-loader(解析 @import、url(),转为 JS 模块)
CSS 的 JS 模块表示
│
▼ ③ style-loader(将 CSS 注入 DOM 的 <style> 标签)
最终 JS 代码(包含注入逻辑)
为什么是从右到左?
这是函数组合(compose) 的概念:
// Loader 链本质上等价于函数组合
const result = styleLoader(cssLoader(postcssLoader(source)))
// ③ ② ①
// 就像 Unix 管道
// cat file.css | postcss | css-loader | style-loader
// 源文件 ① ② ③
Loader 的四种分类
Webpack 的 rule.enforce 属性可以控制 Loader 的执行阶段:
module: {
rules: [
// pre loader:最先执行
{
test: /\.js$/,
enforce: 'pre',
use: 'eslint-loader', // 代码检查在最前面
},
// normal loader:默认,按配置顺序执行
{
test: /\.js$/,
use: 'babel-loader',
},
// post loader:最后执行
{
test: /\.js$/,
enforce: 'post',
use: 'coverage-loader', // 代码覆盖率在最后
},
],
}
完整执行顺序:
源文件
│
▼ Pre Loaders(enforce: 'pre')
eslint-loader
│
▼ Normal Loaders(默认)
babel-loader
│
▼ Post Loaders(enforce: 'post')
coverage-loader
│
▼
最终结果
实际上还有一种 inline loader(行内 loader):
import 'style-loader!css-loader!./style.css'
// 不推荐使用,但了解其存在
完整顺序: pre → normal → inline → post
Pitch 机制
每个 Loader 除了正常的处理函数,还可以有一个 pitch 方法。Pitch 的执行顺序和正常处理相反:
Pitch 阶段(从左到右)
─────────────────────────────────────────────▶
style-loader css-loader postcss-loader
pitch() ──────▶ pitch() ────▶ pitch()
│
▼ 正常阶段(从右到左)
◀─────────────────────────────────────────────
style-loader css-loader postcss-loader
◀── normal() ◀── normal() ◀── normal()
Pitch 的"熔断"机制
如果 pitch 方法返回了值,就会跳过后续 Loader,直接进入前面 Loader 的 normal 阶段:
// style-loader 的 pitch 实现(简化)
module.exports.pitch = function (remainingRequest) {
// remainingRequest = 'css-loader!postcss-loader!./style.css'
// 直接返回一段 JS 代码(不再执行 css-loader 和 postcss-loader 的 normal)
return `
var content = require(${JSON.stringify('!!' + remainingRequest)})
var style = document.createElement('style')
style.textContent = content
document.head.appendChild(style)
`
}
如果 style-loader 的 pitch 返回了值:
style-loader css-loader postcss-loader
pitch() ───── 返回值!
│
▼ 跳过后续 pitch 和 normal
直接得到结果
但 require() 会再次触发 css-loader → postcss-loader 的处理
如何保证 Loader 按预期工作
1. 正确的顺序配置
// CSS 处理链(必须严格按顺序)
{
test: /\.scss$/,
use: [
'style-loader', // 4. 注入 DOM(开发模式)
// MiniCssExtractPlugin.loader, // 4. 提取为文件(生产模式)
{
loader: 'css-loader', // 3. 解析 CSS 依赖
options: {
modules: true, // CSS Modules
importLoaders: 2, // 确保 @import 的文件也经过后面 2 个 loader
},
},
'postcss-loader', // 2. PostCSS 处理(autoprefixer)
'sass-loader', // 1. SCSS → CSS
],
}
importLoaders 很关键:
/* main.scss */
@import './variables.scss'; /* 这个文件也需要经过 sass-loader 和 postcss-loader */
如果不设置 importLoaders: 2,@import 的文件只会经过 css-loader,不会经过 postcss-loader 和 sass-loader。
2. 使用 oneOf 避免重复匹配
module: {
rules: [
{
oneOf: [
// CSS Modules(.module.css)
{
test: /\.module\.css$/,
use: [
'style-loader',
{ loader: 'css-loader', options: { modules: true } },
'postcss-loader',
],
},
// 普通 CSS
{
test: /\.css$/,
use: ['style-loader', 'css-loader', 'postcss-loader'],
},
// 匹配到第一个就停止,避免同一文件被多个 rule 处理
],
},
],
}
3. 条件匹配精确控制
{
test: /\.(ts|tsx)$/,
include: [
path.resolve(__dirname, 'src'),
path.resolve(__dirname, 'shared'), // 也需要编译的目录
],
exclude: [
/node_modules/,
/\.test\.(ts|tsx)$/, // 排除测试文件
],
use: 'swc-loader',
},
4. 内联 Loader 控制
// 在 import 语句中可以覆盖 Loader 行为
// 禁用所有 pre 和 normal loader
import file from '-!raw-loader!./file.txt'
// 禁用所有 loader
import file from '!!raw-loader!./file.txt'
// ! 前缀:禁用 normal loader
import file from '!raw-loader!./file.txt'
5. 编写自定义 Loader 的规范
// my-custom-loader.js
module.exports = function (source) {
// this.cacheable() // 标记可缓存(默认 true)
// this.async() // 如果是异步处理
// this.getOptions() // 获取 loader options
// this.emitFile() // 输出额外文件
// source 是上一个 Loader 传过来的结果
const result = transform(source)
// 返回处理后的结果
return result
// 也可以返回多个值(source, sourceMap, meta)
// this.callback(null, result, sourceMap, meta)
}
// Loader 规则:
// 1. 单一职责:每个 Loader 只做一件事
// 2. 链式调用:输出给下一个 Loader 的输入
// 3. 无状态:不在 Loader 中保存状态
// 4. 使用 loader-utils 解析选项
6. 调试 Loader 执行
// 方法1:添加调试 Loader
{
test: /\.tsx$/,
use: [
'swc-loader',
{
loader: path.resolve(__dirname, 'debug-loader.js'),
// 插入在两个 Loader 之间,打印中间结果
},
'some-other-loader',
],
}
// debug-loader.js
module.exports = function (source) {
console.log('=== Debug Loader ===')
console.log('Resource:', this.resourcePath)
console.log('Source length:', source.length)
console.log('First 200 chars:', source.slice(0, 200))
return source // 透传,不修改
}
总结
| 要点 | 说明 |
|---|---|
| 执行顺序 | 从右到左、从下到上(函数组合) |
| 分类 | pre → normal → inline → post |
| Pitch | 从左到右,可以熔断跳过后续 |
importLoaders | 确保 @import 文件也走完整 Loader 链 |
oneOf | 避免同一文件匹配多个 rule |
include/exclude | 精确控制处理范围 |
| 单一职责 | 每个 Loader 只做一件事 |