在配置 module.rules 时,很多人只关注 test(匹配哪些文件),却忽略了 include / exclude(限定在哪些范围内处理)。结果一个 babel-loader 把所有 node_modules 也跑了一遍,构建慢得离谱。本文讲清这两个字段的作用、区别和常见用法。
一、为什么需要 include / exclude
1. loader 默认「有 test 就全跑」
test 只负责筛选文件类型,一旦匹配,loader 就会对所有符合该类型的文件生效——包括 node_modules 里成千上万的第三方文件。
{
test: /\.m?js$/,
use: 'babel-loader',
}
这条配置看起来没问题,实际上会对 node_modules 下的每一个 JS 文件执行 Babel。后果是:
- 构建极慢:第三方包数量庞大,逐個转译耗时惊人。
- 完全没必要:npm 上的包通常已经发布为编译后的产物,不需要再次转译。
- 可能出错:重复编译有时会导致奇怪的行为。
正确做法是限定范围:
{
test: /\.m?js$/,
exclude: /node_modules/,
use: 'babel-loader',
}
2. 有时还需要「只处理某些目录」
反过来,有时希望 loader 只作用于特定目录(如只处理 src),避免误伤项目里其它位置的文件。这时用:
{
test: /\.m?js$/,
include: path.resolve(__dirname, 'src'),
use: 'babel-loader',
}
二、include 与 exclude 是什么
它们都是 rule 上的过滤条件,和 test 一起决定一条规则是否对某个文件生效。规则匹配的完整语义是:
命中该规则 = 满足 test(若配置) 且 满足 include(若配置) 且 不满足 exclude(若配置)
换句话说:
- include:白名单——只处理这些文件/目录。
- exclude:黑名单——排除这些文件/目录。
两者作用相反,可以只写其中一个,也可以同时写(文件必须「在 include 内」且「不在 exclude 内」)。
三者的取值类型一致:
| 类型 | 示例 |
|---|---|
| 字符串(必须是绝对路径) | path.resolve(__dirname, 'src') |
| 正则 | /node_modules/、/\.test\.js$/ |
| 数组(任一匹配即可) | [path.resolve(__dirname, 'src'), path.resolve(__dirname, 'packages')] |
| 函数(返回布尔值) | (resourcePath) => !resourcePath.includes('vendor') |
三、怎么用
1. 排除 node_modules(最常见)
{
test: /\.m?js$/,
exclude: /node_modules/,
use: 'babel-loader',
}
2. 白名单式:只处理 src
const path = require('path');
{
test: /\.m?js$/,
include: path.resolve(__dirname, 'src'),
use: 'babel-loader',
}
注意:字符串类型必须是绝对路径。直接写
'src'是不生效的,因为 webpack 匹配的是文件的绝对路径。
3. include 和 exclude 怎么选
| include | exclude | |
|---|---|---|
| 语义 | 白名单,只处理列出的 | 黑名单,排除列出的 |
| 范围控制 | 更严格,误伤更少 | 更宽松,容易漏网 |
| 适用场景 | 明确知道要处理哪些目录 | 主要想排除 node_modules |
实践建议:能用 include 就用 include,范围更明确、性能更可控;如果只是不想动 node_modules,exclude: /node_modules/ 更省事。
4. 数组:指定多个目录
{
test: /\.m?js$/,
include: [
path.resolve(__dirname, 'src'),
path.resolve(__dirname, 'packages'),
],
use: 'babel-loader',
}
5. 函数:更灵活的判断
{
test: /\.m?js$/,
include: (resourcePath) => !resourcePath.includes('node_modules'),
use: 'babel-loader',
}
6. 排除特定文件,但保留目录
例如排除所有 .test.js 测试文件:
{
test: /\.m?js$/,
exclude: /\.test\.js$/,
use: 'babel-loader',
}
7. 同时使用:include 为主,exclude 补充
{
test: /\.m?js$/,
include: path.resolve(__dirname, 'src'),
exclude: path.resolve(__dirname, 'src/legacy'),
use: 'babel-loader',
}
四、进阶:条件组合
rule 的条件对象支持 and / or / not 做组合,比单纯 include/exclude 更灵活:
{
test: /\.js$/,
// 排除 node_modules 中的测试文件
not: [/node_modules/],
// 或者用 and / or 组织多个条件
// and: [/src/, /\.js$/],
}
以及可以用 resourceQuery、issuer 等更细的条件:
{
test: /\.css$/i,
include: path.resolve(__dirname, 'src'),
exclude: /node_modules/,
use: ['style-loader', 'css-loader'],
}
五、常见坑
- include 写了相对路径:字符串必须是绝对路径,否则永远匹配不到。用
path.resolve(__dirname, 'xxx')最稳妥。 - 只写 exclude 却忘了根路径:
exclude: /node_modules/是正则,会匹配路径中任意位置的node_modules,一般没问题;但如果项目目录名里恰好含node_modules,可能误伤。 - 有些 node_modules 确实需要编译:某些库发布的是未转译的 ES6+ 或 TS 源码。这时不能简单排除整个
node_modules,需要把它们放回白名单:
{
test: /\.m?js$/,
// 排除 node_modules,但保留 package-a、package-b
exclude: /node_modules\/(?!(package-a|package-b)\/)/,
use: 'babel-loader',
}
- 和 oneOf 一起用时位置要对:
include/exclude应写在各条规则内部;需要叠加的规则(如 ESLint)通常放在oneOf外面。
六、完整配置示例
const path = require('path');
module.exports = {
module: {
rules: [
{
test: /\.m?jsx?$/,
include: path.resolve(__dirname, 'src'),
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: { cacheDirectory: true },
},
},
{
test: /\.tsx?$/,
include: path.resolve(__dirname, 'src'),
use: 'ts-loader',
},
{
test: /\.css$/i,
include: path.resolve(__dirname, 'src'),
use: ['style-loader', 'css-loader'],
},
{
test: /\.(png|jpe?g|gif|svg)$/i,
include: path.resolve(__dirname, 'src/assets'),
type: 'asset',
generator: { filename: 'images/[hash][ext][query]' },
},
],
},
};
小结
| 字段 | 作用 | 记忆方式 |
|---|---|---|
test | 按文件类型筛选 | 匹配「是什么文件」 |
include | 白名单,限定处理范围 | 只处理「这些」 |
exclude | 黑名单,排除特定范围 | 不处理「这些」 |
一句话:test 决定处理什么类型,include / exclude 决定处理什么范围。 最常用的组合就是 exclude: /node_modules/;想让范围更明确、构建更可控,就用 include 指向源码目录。用好这两个字段,能避免大量无谓的 loader 执行,是构建性能优化的第一步。