
从安装到基础过滤:让每一次检查都精准命中目标
先说说为啥咱们需要专门的过滤工具。你可能用过lint-staged,它能让git暂存区的文件只经过lint检查,但默认情况下它会把所有暂存文件都扔给lint工具。可实际开发中,你改了个.md文档,难道也要让ESLint去检查吗?或者你明明只动了src/views里的文件,结果它把public目录下的静态资源也扫一遍,这不是白费功夫吗?lint-staged-filter就是干这个的——在lint-staged的基础上再加一层”筛选器”,帮你把真正需要检查的文件挑出来,其他的直接放行。
10分钟上手:安装与初始化的正确姿势
安装这一步其实很简单,但我见过不少人栽在”全局安装还是局部安装”的问题上。我 你直接局部安装,因为每个项目的node版本可能不一样,全局安装容易出现版本冲突。就像去年帮朋友的React项目配置时,他全局装了lint-staged-filter@1.0,结果项目里node是14,跑起来直接报错,后来局部装了匹配版本才解决。具体步骤你记一下:
确保项目里已经有lint-staged和husky(如果还没有,先跑npm install save-dev lint-staged husky
),然后装filter工具:npm install save-dev lint-staged-filter
。装完后,在package.json里加个配置项,或者新建一个.lintstagedrc.js文件——我个人更喜欢后者,因为JS文件可以写注释,方便团队其他人看懂。比如基础的配置文件可以这样写:
// .lintstagedrc.js
module.exports = {
'': (filenames) => {
// 引入filter工具
const { filter } = require('lint-staged-filter');
// 只保留.js和.jsx文件
const filtered = filter(filenames, {
include: ['/.js', '/.jsx']
});
// 如果有符合条件的文件,就执行eslint检查
return filtered.length > 0 ? [eslint ${filtered.join(' ')}
] [];
}
};
这里有个细节要注意:filenames参数是lint-staged传过来的暂存区文件列表,格式是相对路径,比如[‘src/components/Button.js’, ‘README.md’]。filter函数的作用就是从这个列表里挑出你想要的文件,剩下的就不执行后续命令了。你可以先别急着配复杂规则,就用这个简单配置试试:改个.js文件提交,看看README.md会不会被过滤掉——验证方法很简单,提交前运行npx lint-staged verbose
,控制台会显示”过滤后文件:xxx”,如果只有.js文件出现,说明基础配置生效了。
路径匹配:用glob模式给文件”画范围”
路径匹配这块是最容易搞晕的,我见过有人把正则表达式直接写进去,结果匹配出一堆奇怪的文件。其实lint-staged-filter默认支持glob模式,这东西比正则简单多了,比如/.js
表示所有子目录下的.js文件,src//.vue
只匹配src目录里的.vue文件。为啥用glob?因为它专门为文件路径设计,像”排除某个目录”这种需求,用!node_modules//
就搞定了,比正则写^(?!node_modules)..js$
直观10倍。
我给你 几个最常用的glob规则,直接拿去用:
规则示例 | 作用描述 | 适用场景 |
---|---|---|
src//.js |
匹配src下所有子目录的.js文件 | 只检查业务代码目录 |
!/node_modules// |
排除node_modules下所有文件 | 避免第三方依赖被检查 |
{src,pages}//.ts |
匹配src或pages目录的.ts文件 | 多源码目录项目 |
/.{js,jsx,ts,tsx} |
匹配多种文件类型 | 混合使用JS和TS的项目 |
上次帮一个用Next.js的团队配置时,他们的pages目录和src/components需要分开检查,就用了{pages,src/components}//.{js,tsx}
,既简洁又清晰。不过要注意,glob里的表示”所有子目录”,但如果你的项目有深层嵌套(比如src/views/user/profile/Edit.js),
src//.js
也能正确匹配,不用写成src////.js
这种麻烦样子。
文件类型过滤:别让无关文件占用检查时间
除了按路径过滤,按文件类型过滤可能是最常用的需求了。比如你改了个.css文件,结果ESLint跑起来报错”Unexpected token { in CSS file”——这就是因为没过滤文件类型。lint-staged-filter的include和exclude选项可以直接指定文件后缀,甚至可以结合文件内容来过滤(不过内容过滤比较复杂,咱们先聚焦基础类型)。
举个实际场景:你的项目里有.js、.vue、.scss三种文件,分别需要ESLint、eslint-plugin-vue、stylelint检查。这时候就可以分三个规则块配置:
// .lintstagedrc.js
module.exports = {
// JS文件规则
'.js': (filenames) => {
const { filter } = require('lint-staged-filter');
const jsFiles = filter(filenames, {
include: ['/.js'],
exclude: ['/.test.js'] // 排除测试文件
});
return jsFiles.length > 0 ? [eslint ${jsFiles.join(' ')}
] [];
},
// Vue文件规则
'.vue': (filenames) => {
const { filter } = require('lint-staged-filter');
const vueFiles = filter(filenames, {
include: ['src/views//.vue'] // 只检查视图目录的Vue文件
});
return vueFiles.length > 0 ? ['eslint ext .vue', 'stylelint "/.vue"'] [];
},
// SCSS文件规则
'.scss': (filenames) => {
const { filter } = require('lint-staged-filter');
const scssFiles = filter(filenames, {
include: ['src/styles//.scss'],
exclude: ['/_variables.scss'] // 排除变量文件
});
return scssFiles.length > 0 ? [stylelint ${scssFiles.join(' ')}
] [];
}
};
这里有个小技巧:如果你想让某个规则只对特定目录生效,直接在include里写完整路径就行。比如上面的Vue文件只检查src/views下的,因为components里的基础组件可能已经在单元测试里检查过了,提交时重复检查反而浪费时间。我之前在一个中台项目里就是这么配的,把检查范围缩小到业务页面后,每次提交的检查时间从45秒降到了8秒,开发同学都说”终于不用边喝咖啡边等检查了”。
进阶配置与实战场景:解决复杂项目的过滤难题
基础配置搞定后,你可能会遇到更复杂的场景:比如用monorepo管理多个包,每个包的src目录需要单独过滤;或者想根据文件修改时间来决定是否检查(比如只检查当天修改的文件)。这时候就需要用到lint-staged-filter的高级功能,甚至结合husky的钩子来联动配置。
结合husky:让过滤在提交前自动生效
你可能知道,husky能让git钩子(比如pre-commit、pre-push)执行自定义命令,而lint-staged通常和husky的pre-commit钩子配合使用。但很多人不知道,其实可以在husky钩子脚本里先运行过滤逻辑,再决定是否执行检查。比如你想在提交前先过滤文件,再根据过滤结果决定是否跑lint,甚至可以加一些自定义提示。
举个例子,在.husky/pre-commit文件里这样写:
#!/usr/bin/env sh
. "$(dirname -
"$0")/_/husky.sh"
获取暂存区文件列表
staged_files=$(git diff cached name-only diff-filter=ACM)
使用lint-staged-filter过滤文件
filtered_files=$(node -e "
const { filter } = require('lint-staged-filter');
const files = process.argv[1].split('n').filter(Boolean);
const result = filter(files, { include: ['src//.{js,ts}'] });
console.log(result.join('n'));
" "$staged_files")
如果没有过滤到文件,直接退出
if [ -z "$filtered_files" ]; then
echo "✨ 没有需要检查的JS/TS文件,提交继续~"
exit 0
fi
否则执行lint检查
echo "🔍 正在检查以下文件:n$filtered_files"
npx eslint $filtered_files
这样配置后,每次提交时会先在husky钩子里过滤文件,如果没有符合条件的文件,就直接跳过检查,省得等。上次帮一个用TypeScript的团队配置时,他们的设计师偶尔会提交图片文件,这时候就会显示”没有需要检查的JS/TS文件”,不用等ESLint跑一遍,体验感好了很多。不过要注意,husky的脚本文件需要有执行权限,如果你运行时提示”permission denied”,记得用chmod +x .husky/pre-commit
授权。
多场景适配:从单项目到monorepo的灵活配置
现在很多团队用monorepo(多包管理)架构,比如用pnpm workspace或lerna管理多个子包。这种项目的目录结构通常是packages/pkg1、packages/pkg2,每个包有自己的src目录和lint规则。这时候如果用全局过滤规则(比如src//.js
),会匹配到所有包的src目录,但你可能希望每个包只检查自己的文件。
这时候可以用lint-staged-filter的条件组合过滤,比如根据文件路径中的包名来过滤。假设你的项目结构是:
packages/
pkg1/
src/
index.js
pkg2/
src/
utils.js
你想让pkg1只检查自己的src目录,可以这样配置:
// .lintstagedrc.js
module.exports = {
'': (filenames) => {
const { filter } = require('lint-staged-filter');
// 只保留packages/pkg1/src下的文件
const filtered = filter(filenames, {
include: (file) => {
// 检查文件路径是否包含'packages/pkg1/src'
return file.includes('packages/pkg1/src');
}
});
return filtered.length > 0 ? [cd packages/pkg1 && npx eslint ${filtered.map(f => f.replace('packages/pkg1/', '')).join(' ')}
] [];
}
};
这里用了函数形式的include选项,比glob模式更灵活,可以写任意判断逻辑。如果你的monorepo有10个以上的包,手动写每个包的规则太麻烦,还可以动态读取packages目录下的子包名,生成过滤规则。比如用fs模块读取packages目录,然后循环生成include条件,这样新增包时不用改配置。
常见问题与解决方案:避开配置路上的”坑”
就算你按上面的步骤配置,也可能遇到一些问题。我 了三个最常见的”坑”,以及对应的解决办法,都是我和身边团队踩过的:
问题1:路径解析错误,提示”文件不存在”
这通常是因为lint-staged传的文件路径是相对路径,而你的lint配置文件(比如.eslintrc.js)里的规则可能用了绝对路径,或者命令执行的目录不对。比如在monorepo里,如果你在根目录运行npx eslint packages/pkg1/src/index.js
,而pkg1里的.eslintrc.js用了extends: './.eslintrc.base.js'
,ESLint会在根目录找这个文件,导致找不到。解决办法是在执行命令时切换到子包目录,就像上面例子里的cd packages/pkg1 && npx eslint ...
。
问题2:过滤规则冲突,include和exclude同时生效
比如你配置了include: ['src/
/.js’]和exclude: ['src/utils/
.js’],结果发现src/utils下的.js文件还是被检查了。这是因为glob模式里,exclude的优先级比include高,但如果你的exclude路径写得不对(比如少了),就会失效。正确的写法应该是exclude: ['src/utils/
/.js’],用表示utils下的所有子目录。你可以用npx glob "src/
/.js” ignore “src/utils//.js”测试路径是否正确,这是个很好的验证方法。
问题3:过滤后文件为空,导致命令报错
有时候过滤后没有文件,但lint命令还是会执行(比如eslint
后面没有文件名,会默认检查所有文件)。解决办法是在配置里判断过滤结果的长度,如果为0就返回空数组或直接退出。就像前面基础配置里写的return filtered.length > 0 ? [...] []
,lint-staged如果收到空数组,就不会执行命令。
可复用的配置模板:直接抄作业的实战案例
最后给你几个可直接复用的配置模板,覆盖常见场景,你可以根据项目情况修改:
场景1:基础JS/TS项目
// .lintstagedrc.js
module.exports = {
'.{js,jsx,ts,tsx}': (filenames) => {
const { filter } = require('lint-staged-filter');
const filtered = filter(filenames, {
include: ['src/
/‘],
exclude: [‘/
.test.{js,ts}’, ‘node_modules//‘]
});
return filtered.length > 0 ? [eslint ${filtered.join(' ')} fix
] [];
}
};
场景2:Vue项目(包含SFC和CSS)
// .lintstagedrc.js
module.exports = {
'
.{vue,js,jsx}’: (filenames) => {
const { filter } = require(‘lint-staged-filter’);
const filtered = filter(filenames, { include: [‘src/views//‘] });
return filtered.length > 0 ? [
‘eslint ext .vue,.js,.jsx fix’,
‘stylelint “/.{vue,css,scss}” fix’
] [];
}
};
场景3:monorepo项目(只检查当前修改的包)
// .lintstagedrc.js
const { readdirSync } = require('fs');
const { join } = require('path');
// 获取所有子包目录
const packages = readdirSync(join(__dirname, 'packages'), { withFileTypes: true })
.filter(dirent => dirent.isDirectory())
.map(dirent => dirent.name);
module.exports = {
'': (filenames) => {
const { filter } = require('lint-staged-filter');
// 只保留当前修改的包目录下的文件
const filtered = filter(filenames, {
include: packages.map(pkg => packages/${pkg}/src//
)
});
if (filtered.length === 0) return [];
// 获取涉及的包名
const pkgs = [...new Set(filtered.map(file => file.split('/')[1]))];
// 对每个包执行lint
return pkgs.map(pkg => cd packages/${pkg} && npm run lint
);
}
};
你可以把这些模板保存下来,新项目直接改改路径就能用。记得配置完后用git add
一个文件,然后运行npx lint-staged verbose
测试,控制台会显示过滤过程和执行的命令,有问题能及时发现。如果你试了这些方法,遇到过滤不生效或者命令报错,欢迎在评论区告诉我具体情况,咱们一起看看怎么解决。
你是不是装完lint-staged-filter一跑命令就报错?先别急着删依赖重装,我帮人解决过十几次这类问题,八成是版本没对上。你想想,是不是图方便直接全局装了?我之前帮一个同事看问题,他就是全局装了lint-staged-filter@2.0,结果项目里node还是14.x,跑起来直接红屏报错“Cannot find module”,后来把全局的卸载了,局部装了1.x版本才好。记着啊,这工具跟node版本绑定挺紧,尤其是1.5以上版本得node16+才行,装之前先看看package.json里的engines字段,或者直接run node -v
确认下本地版本,局部安装永远是最稳妥的,每个项目独立环境,就像给每个孩子单独准备餐具,不容易串味儿。
再说说“模块找不到”这个报错,你是不是看到控制台刷一堆“Error: Cannot find module ‘lint-staged-filter’”?别光想着删node_modules,有时候是npm缓存搞的鬼。我上次遇到这情况,先试了npm cache clean force
清缓存,然后删了node_modules和package-lock.json,最后npm install
,结果就好了——比直接删文件夹管用多了,因为缓存里可能还留着旧版本的残留文件。要是用yarn的话,就跑yarn cache clean
,道理都一样。 检查下package.json里有没有这工具的依赖,有时候npm install时网络波动,可能没装成功,依赖列表里找不到的话,补一句npm install save-dev lint-staged-filter
就行。
还有种常见报错是“路径解析错误”,比如提示“找不到xxx文件”,十有八九是你配置文件里写了绝对路径。举个例子,你是不是在include里写了/src//.js
?带个斜杠开头在Windows系统里就会出问题,因为Windows路径是C:projectsrc
,而你写的绝对路径会从根目录找,自然找不到。改成相对路径src//.js
就好,或者用node的path模块,比如path.join(__dirname, 'src/*/.js')
,让系统自己拼路径,不管是Mac还是Windows都能认。要是还搞不清路径对不对,直接在配置文件里console.log(filenames)
打印一下暂存区文件路径,对着调就简单多了。
lint-staged-filter和lint-staged有什么区别?
lint-staged是基础工具,主要功能是让git暂存区文件仅执行lint检查,但默认会处理所有暂存文件;而lint-staged-filter是对其的增强工具,可通过路径匹配、文件类型、条件组合等规则进一步筛选文件,实现更精准的检查范围控制,避免无关文件占用检查资源。
安装lint-staged-filter后执行命令报错怎么办?
首先检查版本兼容性,确保node版本与工具版本匹配( 局部安装而非全局安装,避免版本冲突);若提示“模块找不到”,可尝试删除node_modules和package-lock.json后重新安装依赖;若路径解析错误,检查配置文件中文件路径是否为项目相对路径,避免使用绝对路径。
路径匹配规则不生效可能是什么原因?
常见原因包括:glob模式使用错误(如缺少表示子目录,正确写法为src//.js而非src/.js)、include与exclude顺序冲突(exclude优先级更高,需确保排除规则路径完整)、路径中包含特殊字符未转义(如空格需用引号包裹),可通过npx glob命令测试路径匹配结果验证规则正确性。
能否根据文件修改时间过滤需要检查的文件?
可以。lint-staged-filter支持通过函数形式的include选项实现动态过滤,例如结合node的fs模块获取文件修改时间(mtime),判断是否在指定范围内(如24小时内修改的文件),示例代码可在配置文件中通过fs.statSync(file).mtime获取时间戳后进行条件判断。
lint-staged-filter如何与prettier等格式化工具配合使用?
只需在过滤后的文件列表基础上,添加对应工具的执行命令即可。例如配置中过滤出.js和.ts文件后,返回包含prettier的命令数组:return filtered.length > 0 ? [prettier write ${filtered.join(‘ ‘)}] [],确保格式化工具仅处理筛选后的目标文件,提升执行效率。