lint-staged 这个工具,几乎所有做前端工程化的人迟早都会碰到。但它到底解决了什么问题、为什么能解决、什么时候其实没必要上它,很多人只是照着文档配了一遍就没再深究。我在几个项目里把它从零到一落地过,也踩过不少配置之外、文档里不会写的坑,这篇文章就把我实际使用中的理解和经验一次性说清楚。
1. 为什么需要 lint-staged:全量 lint 的卡顿困境
先说一个最常见也最容易被忽略的痛点:当项目运行 eslint . 或者 eslint src 的时候,表面上看是“检查整个项目的代码质量”,实际在稍微大一点的项目里,这个命令跑一次可能要十几秒甚至几十秒。如果还挂了 stylelint、prettier --check、类型检查之类,一套组合拳下来,每次提交代码前光等校验就够喝一壶的。
这个问题的根源在于,很多 lint 工具没有做增量缓存机制,每次执行都会重新解析所有文件。哪怕你只改了一个文件里的一个变量名,eslint . 依然会把整个 node_modules 之外的全部源码翻一遍。在项目几百个文件起步、引入了 TypeScript 和一堆复杂规则之后,这个成本会指数级上升。而 lint-staged 的核心设计思路就是:不要检查全部文件,只检查 Git 暂存区里那些即将被提交的文件。
也就是说,它的名字其实已经解释了它的一切行为——staged 在 Git 语境里是“已暂存”的意思,lint-staged 就是“对已暂存的文件执行 lint 检查”。它通过 git diff --name-only --cached 之类的底层命令,找到当前暂存区里的文件列表,然后只把这些文件交给 eslint、prettier、stylelint 去处理。
这种思路带来的体验提升是立竿见影的。假设项目一共 800 个文件,你这次提交只改了 3 个文件,lint-staged 只会 lint 这 3 个,耗时从十几秒直接降到一两秒甚至几百毫秒。而且因为每次只处理少量文件,即使某个文件出了问题,报错信息也聚焦得多,修复起来更快。可以说,lint-staged 是 Git hooks 时代让“提交前自动校验”变得真正可用的关键一环。
有人可能会问:那我不在提交时跑,靠编辑器里的 ESLint 插件实时报错不行吗?当然可以,编辑器插件是开发期的第一道防线,但它的覆盖面取决于编辑器是否加载了正确的配置、是否覆盖了所有文件类型、以及开发者有没有打开对应文件。lint-staged 的价值在于它作为提交前最后一道强制关卡,不受编辑器状态影响,只要代码进入暂存区,就必然走一次校验,跑不掉的。这一道关卡,配合 Husky 之类的 Git hooks 管理工具,才能真正做到“不合格的代码进不了提交历史”。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 从依赖到流水线:lint-staged 的底层工作流程拆解
lint-staged 的工作流程,如果拆开来看,其实就是一个非常清晰的管道(pipeline)。它做的事情不一定复杂,但顺序和细节直接影响最终的可靠性。理解了这个流程,后面配置里很多“奇怪”的现象就都能解释通了。
2.1 它如何知道要检查哪些文件
这是整个工具最关键的一步。lint-staged 底层会调用 Git 命令,默认逻辑大致是:通过 git diff --name-only --cached --diff-filter=ACMR 获取当前暂存区中已添加、已复制、已修改、已重命名的文件列表。注意它默认排除了删除(D)的文件,因为文件都删了,lint 它没有意义;同时它也只会拿到“相对仓库根目录的路径”,而不是绝对路径。
这里面有一个很容易被忽略的能力:lint-staged 支持配置 --diff 参数,你可以让它基于任意两次提交之间的差异来生成文件列表,而不局限于“当前暂存区”。这在某些 CI 场景下非常有用,比如你只想检查某个 MR 里与上一个版本相比改动的文件。不过日常本地提交,绝大多数场景用的都是默认的暂存区模式。
2.2 匹配规则是怎么过滤文件的
拿到文件列表之后,lint-staged 会拿着这个列表去和你配置里的 glob pattern(也就是 *.js、*.ts、*.{js,vue} 之类的匹配模式)做匹配。只有同时满足下面两个条件的文件才会交给对应的 lint 命令:
- 该文件确实在 Git 的暂存区里(或在你通过
--diff指定的变更集里)。 - 该文件的路径能匹配上配置中的某个 glob 模式。
举个例子,如果你配置的是 "*.js": "eslint --fix",那么暂存区里即便有 foo.ts 和 bar.js,也只有 bar.js 会被送去 lint。这也意味着,如果某个文件类型你忘了写对应的匹配规则,它就会像空气一样直接放行,不会报错——这是配置里最隐蔽的“静默失效”方式之一,后面我会专门讲。
2.3 任务执行顺序和文件暂存回写
文件匹配完成之后,lint-staged 会把每个匹配模式对应的命令并发地跑起来。默认情况下,多个任务之间是并行的,这也是它能保持高速的原因之一。但这里有一个非常经典的坑:如果两个任务处理的是同一批文件的同一区域,比如 eslint --fix 和 prettier --write 同时跑,最终结果就可能会互相覆盖,导致文件被改了一半、格式化了一半,甚至产生语法错误。
文档里其实早就给出了建议,那就是使用 && 把多条命令串联起来,让它们按顺序执行:
json复制{
"lint-staged": {
"*.{js,jsx,ts,tsx}": "eslint --fix && prettier --write"
}
}
这里 eslint --fix 先把可自动修复的代码风格问题修掉,再交给 prettier --write 做最后的格式化。顺序上尽量把 ESLint 放在 Prettier 前面。因为 Prettier 的格式化会重排代码结构,你在 ESLint 里设定的某些规则(比如 indent、max-len)可能依赖原始行结构,先 Prettier 再 ESLint 有可能导致 ESLint 再次改动 Prettier 刚格式化好的代码,造成“格式化结果不稳定”的循环。先 ESLint 再 Prettier,让 Prettier 做最终输出,是更稳定的顺序。
另一个隐蔽的点是:lint-staged 在执行完任务之后,如果你用的命令带 --fix 或 --write 这类会修改文件的参数,它会把修改后
