Webpack 之所以能把各种各样的资源都当成模块来处理,靠的就是 loader 机制,而 loader 的接入入口就是配置文件中的 module.rules。这份配置决定了哪些文件会被哪些 loader 处理、按什么顺序处理、处理时传什么参数。配置写得合理,构建流程清晰高效;配置写得混乱,轻则打包结果不符合预期,重则构建直接报错。本文把 module.rules 的各个属性拆开来讲,配合可运行的示例,帮你彻底搞懂模块匹配规则和 loader 的处理逻辑。

一、module.rules 的基本结构与匹配属性
先看一个最简单的例子,认识一下 rules 数组里每一项长什么样:
module.exports = {
module: {
rules: [
{
test: /\.css$/,
use: ['style-loader', 'css-loader']
}
]
}
};rules 是一个数组,数组中的每个对象称为一条规则(Rule)。Webpack 在遇到每个模块时,会依次遍历这些规则,判断当前模块的路径是否命中条件,命中了就把规则中声明的 loader 应用到该模块上。
匹配相关的属性主要有三个:test、include 和 exclude。test 通常是一个正则表达式,用来匹配文件的绝对路径。注意这里匹配的是完整路径,不只是文件名,所以 /\.css$/ 表示以 .css 结尾的文件。书写正则时要留意 $ 锚点,如果写成 /css/,那么 main.css.js 这种文件也会被误匹配进来,引发一些很难排查的问题。
include 和 exclude 用来进一步限定范围,二者都接受字符串或数组形式的路径,一般配合 path.resolve 使用:
const path = require('path');
module.exports = {
module: {
rules: [
{
test: /\.js$/,
include: path.resolve(__dirname, 'src'),
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env']
}
}
}
]
}
};这条规则的含义是:只处理 src 目录下的 js 文件,同时排除 node_modules。实践中 include 和 exclude 尽量只保留一个,范围明确的项目用 include 更精确,能把第三方依赖彻底挡在外面,减少不必要的转译开销。如果两者同时存在,Webpack 会先判断 exclude,被排除的文件不会再看 include。
二、use 的多种写法与 loader 执行顺序
use 属性指定要使用的 loader,写法相当灵活。最简单的是字符串形式,比如 use: 'babel-loader';需要传参数时改成对象形式;多个 loader 协作时用数组形式:
module.exports = {
module: {
rules: [
{
test: /\.scss$/,
use: [
'style-loader',
'css-loader',
{
loader: 'sass-loader',
options: {
sassOptions: {
outputStyle: 'expanded'
}
}
}
]
}
]
}
};这里有一个非常关键的知识点:数组中 loader 的执行顺序是从右到左、从下到上。以上面的配置为例,scss 文件会先经过 sass-loader 编译成 css,再交给 css-loader 处理其中的 @import 和 url(),最后由 style-loader 把样式注入到页面的 style 标签中。如果把顺序写反,比如把 style-loader 放到最后,构建时就会报错,因为 style-loader 期望接收的是 css-loader 输出的结果,而不是 sass 源码。
可以把这个链式过程想象成流水线:文件从最右侧进入,每经过一个 loader 被加工一次,最终从最左侧输出。记住这个方向,配置多 loader 时就不容易出错。如果觉得从右到左的写法读起来别扭,也可以用 enforce 属性显式控制优先级,写成 enforce: 'pre' 的规则会在普通规则之前执行,enforce: 'post' 则在之后执行。常见的用法是把 eslint-loader 配成 pre,让代码检查先于转译发生:
module.exports = {
module: {
rules: [
{
test: /\.js$/,
enforce: 'pre',
use: 'eslint-loader'
},
{
test: /\.js$/,
use: 'babel-loader'
}
]
}
};三、oneOf、resourceQuery 与资源文件处理实战
默认情况下,一个文件会匹配所有命中的规则,每个规则里的 loader 都会执行一遍。但有些场景下我们希望多个规则互斥,命中一条就不再继续匹配,这时可以把它们包进 oneOf:
module.exports = {
module: {
rules: [
{
oneOf: [
{ test: /\.css$/, use: ['style-loader', 'css-loader'] },
{ test: /\.scss$/, use: ['style-loader', 'css-loader', 'sass-loader'] },
{ test: /\.(png|jpe?g|gif|svg)$/, type: 'asset/resource' },
{ test: /\.(woff2?|eot|ttf)$/, type: 'asset/resource' },
{ test: /\.js$/, use: 'babel-loader' }
]
}
]
}
};oneOf 数组内的规则按顺序匹配,一旦某条规则的 test 命中,后面的规则直接跳过。这样做的好处一是省去不必要的匹配开销,二是逻辑上更清晰,特别适合按文件类型划分处理方式的场景。需要注意的是,如果一条规则可能需要重复命中(比如 js 文件既要 lint 又要转译),就不要放进 oneOf,否则只有写在前面那条会生效,这也是不少人配置后 lint 突然失效的原因。
对于需要按查询参数区分处理的场景,resourceQuery 很实用。例如同一个 svg 文件,带 ?inline 参数时以内联方式引入,否则作为独立资源打包:
module.exports = {
module: {
rules: [
{
test: /\.svg$/,
oneOf: [
{
resourceQuery: /inline/,
type: 'asset/inline'
},
{
type: 'asset/resource'
}
]
}
]
}
};在代码里就可以通过 import icon from './icon.svg?inline' 来选择处理方式。这种按需分流的方式在组件库和图标系统中特别常见,一份资源多种用法,全靠规则配置来支撑。
最后再提一个容易忽略的细节:Webpack 5 中处理图片、字体这类资源时,推荐使用内置的 type: 'asset/resource',代替老项目里的 file-loader 和 url-loader。如果想控制小文件转 base64 的阈值,可以用 type: 'asset' 配合 parser.dataUrlCondition.maxSize,不再需要额外安装任何 loader。
四、常见踩坑点与排查思路
第一类坑是正则写得不够严谨。比如用 /\.js$/ 匹配时会连带命中 .min.js,如果这类压缩过的第三方文件也被 babel 转译,会拖慢构建速度还可能破坏压缩代码。解决办法是在 exclude 中加上对压缩文件的排除,或者用 include 只圈定源码目录。
第二类坑是 loader 顺序错误。表现为构建时报类似 You may need an appropriate loader to handle this file type 或者输出内容乱码。排查时先画出处理链,确认每一步的输入输出是否衔接得上。postcss-loader、less-loader 这类编译型 loader 永远要放在靠右的位置。
第三类坑是 options 写错位置。有人会把 options 直接写在规则顶层,正确的位置是写在 use 数组的对象里,或者简写形式 loader 加 options。另外 rule.options 实际上是 rule.use[0].options 的简写,只在单一 loader 时可用。掌握这些细节后,配合 webpack --config` 加 profile 参数或可视化分析工具,基本可以快速定位绝大多数模块处理相关的问题。
Webpackmodule.rulesloader配置修改时间:2026-09-12 21:02:37