在实际工程中,配置文件上百行乃是常事,如何保证各个 loader 按照预想方式工作?
问题解析
在复杂的 Webpack 配置中,loader 的执行顺序至关重要。当配置文件达到上百行时,确保 loader 按照预期顺序执行是工程化实践中的关键问题。
核心概念
Loader 执行顺序
默认情况下,loader 按照从右到左、从下到上的顺序执行:
module.exports = {
module: {
rules: [
{
test: /\.js$/,
use: ['loader-a', 'loader-b', 'loader-c']
// 执行顺序:loader-c -> loader-b -> loader-a
}
]
}
};
Enforce 强制执行顺序
| 类型 | 执行时机 | 说明 |
|---|---|---|
pre |
在所有普通 loader 之前 | 最先执行 |
normal |
默认 | 按配置顺序 |
inline |
内联 | 官方不推荐 |
post |
在所有普通 loader 之后 | 最后执行 |
详细解答
使用 enforce 控制 loader 顺序
module.exports = {
module: {
rules: [
// 1. Pre loader:最先执行
{
test: /\.js$/,
enforce: 'pre', // 在所有普通 loader 之前执行
use: 'eslint-loader'
},
// 2. Normal loader:默认顺序
{
test: /\.js$/,
use: ['babel-loader', 'cache-loader']
// 执行顺序:cache-loader -> babel-loader
},
// 3. Post loader:最后执行
{
test: /\.js$/,
enforce: 'post', // 在所有普通 loader 之后执行
use: 'some-post-loader'
}
]
}
};
实际应用场景
场景 1:ESLint 必须在 Babel 之前执行
module.exports = {
module: {
rules: [
{
test: /\.js$/,
enforce: 'pre', // 确保 ESLint 先执行,检查原始代码
exclude: /node_modules/,
use: {
loader: 'eslint-loader',
options: {
fix: true
}
}
},
{
test: /\.js$/,
exclude: /node_modules/,
use: 'babel-loader' // 后执行,转换代码
}
]
}
};
场景 2:Source Map 处理
module.exports = {
module: {
rules: [
{
test: /\.js$/,
enforce: 'pre', // 先添加 source map 支持
use: 'source-map-loader'
},
{
test: /\.js$/,
use: 'babel-loader' // 后转换代码
}
]
}
};
场景 3:PostCSS 和 CSS 处理
module.exports = {
module: {
rules: [
{
test: /\.css$/,
enforce: 'post', // 最后执行,添加浏览器前缀等
use: 'postcss-loader'
},
{
test: /\.css$/,
use: [
'style-loader', // 最后(从右到左)
'css-loader' // 最先
]
}
]
}
};
深入理解
Loader 的四种类型详解
// Webpack 内部将 loader 分为四类,按优先级排序:
// pre -> normal -> inline -> post
module.exports = {
module: {
rules: [
{
test: /\.js$/,
enforce: 'pre',
use: 'loader-a' // 第 1 个执行
},
{
test: /\.js$/,
use: 'loader-b' // 第 2 个执行(normal)
},
{
test: /\.js$/,
enforce: 'post',
use: 'loader-c' // 第 3 个执行
}
]
}
};
Inline Loader(不推荐)
// 在代码中直接指定 loader(官方不推荐)
import Styles from 'style-loader!css-loader?modules!./styles.css';
// 使用 !! 前缀禁用其他 loader
import Styles from '!!css-loader!./styles.css';
// 使用 ! 前缀禁用 normal loader
import Styles from '!css-loader!./styles.css';
不推荐原因:
- 破坏配置文件的单一职责
- 难以维护和追踪
- 与配置文件中的规则可能冲突
Loader 的 pitch 阶段
// loader 有两个阶段:pitch 和 normal
// pitch 阶段:从左到右执行
// normal 阶段:从右到左执行
module.exports = {
// pitch 阶段(从左到右)
pitch: function(remainingRequest, precedingRequest, data) {
console.log('pitch 阶段');
},
// normal 阶段(从右到左)
default: function(content) {
console.log('normal 阶段');
return content;
}
};
最佳实践
大型项目的 loader 组织策略
// 将 loader 配置按功能拆分
const jsLoaders = [
{
test: /\.js$/,
enforce: 'pre',
use: 'eslint-loader'
},
{
test: /\.js$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: {
cacheDirectory: true
}
}
}
];
const cssLoaders = [
{
test: /\.css$/,
use: [
'style-loader',
{
loader: 'css-loader',
options: { importLoaders: 1 }
},
'postcss-loader'
]
}
];
const imageLoaders = [
{
test: /\.(png|jpe?g|gif)$/,
type: 'asset/resource'
}
];
module.exports = {
module: {
rules: [...jsLoaders, ...cssLoaders, ...imageLoaders]
}
};
使用 oneOf 优化匹配性能
module.exports = {
module: {
rules: [
{
// 使用 oneOf,匹配到一个后不再继续匹配
oneOf: [
{
test: /\.js$/,
use: 'babel-loader'
},
{
test: /\.css$/,
use: ['style-loader', 'css-loader']
},
{
test: /\.(png|jpe?g|gif)$/,
type: 'asset/resource'
}
]
}
]
}
};
完整的 loader 配置示例
module.exports = {
module: {
rules: [
// ========== Pre Loaders ==========
{
test: /\.js$/,
enforce: 'pre',
exclude: /node_modules/,
use: {
loader: 'eslint-loader',
options: {
cache: true,
fix: true
}
}
},
// ========== Normal Loaders ==========
{
test: /\.js$/,
exclude: /node_modules/,
use: [
{
loader: 'cache-loader' // 缓存 babel 编译结果
},
{
loader: 'thread-loader', // 多线程编译
options: {
workers: 2
}
},
{
loader: 'babel-loader',
options: {
cacheDirectory: true
}
}
]
},
{
test: /\.css$/,
use: [
'style-loader',
{
loader: 'css-loader',
options: {
importLoaders: 1,
modules: true
}
},
{
loader: 'postcss-loader',
options: {
postcssOptions: {
plugins: ['autoprefixer']
}
}
}
]
},
// ========== Post Loaders ==========
{
test: /\.js$/,
enforce: 'post',
use: 'some-custom-loader'
}
]
}
};
面试要点
-
默认执行顺序:从右到左、从下到上
-
enforce 的三种值:
pre:在所有普通 loader 之前执行post:在所有普通 loader 之后执行- 默认(normal):按配置顺序
-
inline loader 不推荐:破坏配置一致性,难以维护
-
实际应用场景:
- ESLint 使用
pre确保在 Babel 之前检查原始代码 - Source map loader 使用
pre确保先处理 source map - PostCSS 通常放在 CSS loader 链的最后
- ESLint 使用
-
性能优化:使用
oneOf避免不必要的匹配,使用cache-loader缓存结果