lint-staged 与 pre-commit 最佳实践
在前端工程化里,pre-commit 阶段最常见的诉求是两类:
- 在真正提交之前,自动格式化和校验本次暂存的代码
- 拦截明显不合规的提交,避免低质量代码进入仓库
如果直接在 pre-commit 里跑整仓 eslint、prettier、stylelint、测试命令,通常会有两个问题:
- 提交很慢,影响开发体验
- 明明只改了 2 个文件,却要扫描整个项目,性价比很低
这就是 lint-staged 存在的意义:它只处理本次 已暂存的文件,让提交前检查更快、更聚焦。
而在工程实践里,lint-staged 通常会和 husky、commitlint 组合使用;如果团队还想进一步统一提交信息录入体验,也可以再配上 commitizen + cz-git:
husky:管理 Git Hookspre-commit:在提交代码前触发质量检查lint-staged:只对 staged files 执行格式化和校验commitlint:校验提交信息是否符合规范commit-msg:在提交信息写入后进行规范检查commitizen + cz-git:用交互式向导生成规范提交信息
这篇文档给一套适合团队项目直接落地的实践方案。
1. 这几个工具分别解决什么问题
先把职责划清楚,不然后续配置很容易混淆。
1.1 husky
husky 的职责很简单:把脚本挂到 Git Hook 上。
比如:
pre-commit阶段执行lint-stagedcommit-msg阶段执行commitlint
它自己不负责 lint,也不负责格式化,只负责“在正确的时机触发正确的命令”。
1.2 lint-staged
lint-staged 的职责是:只对 已暂存文件 执行任务。
例如:
- 对本次提交涉及的
ts/tsx/js文件执行eslint --fix - 对本次提交涉及的
md/json/css文件执行prettier --write - 对本次提交涉及的
css/scss/vue文件执行stylelint --fix
这比整仓扫描快很多,也更符合提交前校验的场景。
1.3 commitlint
commitlint 负责约束提交信息格式,例如:
它通常绑定到 commit-msg Hook,而不是 pre-commit。
2. 一套推荐的组合关系
推荐把三者这样组合:
pre-commit->lint-stagedcommit-msg->commitlint- Git Hooks 的创建和管理 ->
husky
也就是说:
- 开发者执行
git add - 开发者执行
git commit pre-commit先运行lint-staged- 如果代码校验通过,再进入提交信息阶段
commit-msg再运行commitlint- 提交信息也合法,提交才真正完成
这套流程是目前前端团队里最常见、也最稳定的做法。
3. 为什么 pre-commit 更适合配合 lint-staged
pre-commit 的核心目标是:快、稳、可重复执行。
因此这个阶段适合放:
eslint --fixprettier --writestylelint --fix- 少量、快速、只针对本次改动的校验
不太适合放:
- 整仓单元测试
- 整仓类型检查
- 全量构建
- E2E 测试
这些任务通常更适合:
pre-push- CI 流水线
否则一次提交卡几十秒甚至几分钟,团队通常会开始想办法绕过 Hook,最后规范就失效了。
4. 接入步骤
下面以 pnpm 项目为例,给一套完整接入流程。
4.1 安装依赖
如果你暂时只想做 pre-commit + lint-staged,也可以先不安装 commitlint。
4.2 初始化 husky
执行后通常会生成:
.husky/目录package.json中的prepare脚本
例如:
prepare 的作用是:项目安装依赖后,自动启用 Husky Hooks。
4.3 配置 lint-staged
lint-staged 有多种配置方式:
package.json里的lint-staged字段.lintstagedrc.jsonlint-staged.config.mjslint-staged.config.cjs
最佳实践建议:
- 简单项目可以直接放在
package.json - 团队项目优先用独立配置文件
- ESM 项目优先使用
lint-staged.config.mjs - CommonJS 项目优先使用
lint-staged.config.cjs
推荐配置示例:
这套配置的含义是:
- JS/TS 文件先走
eslint --fix,再走prettier --write - 样式和
vue文件先走stylelint --fix,再走prettier --write - 文档和配置文件只走
prettier
4.4 修改 pre-commit Hook
把 .husky/pre-commit 改成:
如果你的项目已经有别的前置命令,也可以按顺序写多行,但建议保持 pre-commit 尽可能轻量。
4.5 配置 commitlint
如果你还希望同时规范提交信息,可以新增 commitlint.config.mjs:
4.6 新增 commit-msg Hook
创建 .husky/commit-msg:
这样就形成了完整链路:
pre-commit做代码质量校验commit-msg做提交信息校验
5. 推荐的项目结构
接入完成后,一个典型项目的结构通常如下:
如果项目规模较小,也可以把 lint-staged 配置直接写在 package.json:
但从可维护性看,独立配置文件通常更清晰。
6. 三者如何配合工作
下面用一次真实提交来说明整个流程。
假设你改了这些文件:
src/pages/login/index.vuesrc/utils/request.tsREADME.md
然后执行:
此时会发生:
- Git 进入
pre-commit husky触发.husky/pre-commitlint-staged找到当前已暂存文件vue/ts/md文件分别匹配对应规则- 执行
eslint --fix、stylelint --fix、prettier --write - 如果任务成功,继续进入
commit-msg husky触发.husky/commit-msgcommitlint校验fix(login): ...是否符合规范- 全部通过后,提交完成
所以三者的关系不是互相替代,而是:
husky负责触发lint-staged负责 staged files 任务分发commitlint负责提交信息校验
7. lint-staged 的几个关键知识点
这一部分是实践里最容易遗漏的。
7.1 它只处理 staged files
lint-staged 只会处理当前暂存区里的文件,不会直接扫描整个仓库。
因此正确用法是:
而不是指望它代替整仓的 lint 命令。
7.2 它会自动把修改后的文件加入本次提交
这是一个很重要的点。
现在的 lint-staged 已经会自动把任务改动后的文件重新纳入本次提交,所以配置里 不要再手写 git add。
错误示例:
不推荐这样写,因为官方已经把这部分能力内置进去了,手动再写 git add 反而容易引入不必要的问题。
7.3 有修改类命令时,顺序要明确
如果多个命令都会改文件,比如:
eslint --fixstylelint --fixprettier --write
那就应该使用数组语法,按顺序执行:
不要把多个可能改同一文件的任务拆成重叠规则并发执行,否则容易出现竞争问题。
7.4 注意重叠 glob 带来的冲突
这是一个很常见的坑。
错误示例:
这里 *.ts 文件会同时匹配两条规则,可能导致两个任务同时修改同一个文件。
更稳妥的写法是:
或者像本文前面的推荐配置那样,直接按文件类型拆清楚。
7.5 不要在任务命令里手动再写文件路径
lint-staged 会把匹配到的 staged files 自动作为参数传给命令,所以通常应该这样写:
而不是这样写:
后一种写法本质上又退回成了“扫固定目录”,既失去了 staged files 的优势,也可能让命令行为和预期不一致。
7.6 lint-staged 默认会用 stash 保护现场
官方 README 明确提到,lint-staged 默认会在运行任务前创建 git stash 备份当前状态,以降低任务失败时的数据风险。
这意味着:
- 部分暂存文件通常能得到更稳妥的处理
- 任务失败时,工具会尽量回滚本次 Hook 过程中的变更
- 你看到的“备份原始状态”“回滚修改”等输出,通常是正常行为
所以如果提交失败,不要第一反应就怀疑 Hook 坏了,先看具体是哪条任务没通过。
7.7 不要在 lint-staged 里默认跑整仓 tsc --noEmit
这个问题在 TypeScript 项目里非常典型。
lint-staged 默认会把匹配到的 staged files 作为参数传给命令,而 tsc 在带文件参数时,行为和整仓类型检查并不一样,容易让人误以为“已经做了类型校验”。
更稳妥的做法是:
- 把整仓
tsc --noEmit放到pre-push或 CI - 如果你确实要在
lint-staged中调用它,就用函数写法,避免自动拼接文件参数
例如:
但从团队实践角度,整仓类型检查通常不建议放在默认 pre-commit。
8. Monorepo 项目怎么做
如果你是 Monorepo,lint-staged 也支持多配置文件。
官方文档说明:对于某个 staged file,lint-staged 会优先使用 离该文件最近的配置文件。
例如:
这意味着:
- 根目录规则可以处理通用文件,比如
md/json - 子包目录可以配置自己的
eslint/stylelint规则 - 不同子项目可以有不同校验策略
如果团队希望统一配置,也可以只保留根目录一个 lint-staged.config.mjs。
9. 一套可以直接挪用的团队方案
如果你希望把这套能力从 0 到 1 接到团队项目里,并且希望新人照着文档就能落地,推荐直接按下面步骤执行。
这一套方案默认约束的是 3 件事:
- 提交前自动格式化和修复本次暂存文件
- 提交前只检查 staged files,不跑整仓慢任务
- 提交信息必须符合
Conventional Commits
9.1 适用前提
下面这套配置默认你项目里已经有这些基础工具中的一部分或全部:
eslintprettierstylelint
如果项目暂时没有 stylelint,可以先把样式相关规则删掉,只保留 JS/TS 和文档类文件。
9.2 第一步:安装依赖
先在项目根目录安装:
如果你项目还没有这些工具,也建议一并装好:
9.3 第二步:初始化 Husky
执行:
执行后,一般会出现两个结果:
- 根目录生成
.husky/ package.json里生成prepare脚本
这时候先检查一下 package.json,至少要有下面这一段:
如果项目原本就有其他脚本,只需要把 prepare 合进去,不要覆盖掉已有配置。
9.4 第三步:新建 lint-staged.config.mjs
在项目根目录创建 lint-staged.config.mjs,内容可以直接用下面这版:
这份配置适合大多数前端项目,规则含义如下:
- 代码文件:先
eslint --fix,再prettier --write - 样式文件:先
stylelint --fix,再prettier --write - 文档和配置文件:只走
prettier
如果你的项目没有 vue、stylelint 或 scss,把对应后缀和命令删掉即可。
9.5 第四步:配置 pre-commit
把 .husky/pre-commit 改成下面这样:
这一步的作用是:每次执行 git commit 时,先让 lint-staged 处理本次暂存文件。
如果你是手动创建 Hook 文件,在 macOS/Linux 环境下建议再执行一次:
9.6 第五步:新建 commitlint.config.mjs
在项目根目录创建 commitlint.config.mjs:
如果你的团队已经统一了 type 或 scope,可以后续再继续加规则;从 0 到 1 落地时,先用官方推荐配置就够了。
9.7 第六步:配置 commit-msg
创建或修改 .husky/commit-msg:
这样就把提交信息检查也接上了。
如果你是手动新建这个文件,在 macOS/Linux 下一样建议执行:
9.8 第七步:整理成统一项目结构
接完以后,项目根目录推荐长这样:
如果你们是团队项目,建议把这些文件全部提交到仓库,避免每个人本地自己配一套。
9.9 第八步:跑一遍验证
接入完成后,按下面顺序验证。
先测试 pre-commit:
如果配置正确:
lint-staged会执行- 对应文件会被自动格式化或修复
- 某条规则失败时,提交会被拦截
再测试 commit-msg:
如果配置正确,这条提交会被 commitlint 拦截。
最后测试一条正常提交:
如果代码和提交信息都合规,提交会成功。
9.10 团队项目建议这样约定
如果你是给团队落地,而不是给个人项目试水,建议顺手把下面几条也写进团队规范:
pre-commit只放快任务,不放整仓测试和构建lint-staged不要手写git add- 会改文件的任务必须按顺序写成数组
commit-msg统一用commitlint- 慢任务统一放到
pre-push或 CI
这样后面团队规模上来以后,不需要再返工整个提交流程。
9.11 一份可以直接复制的最终配置
如果你只想看最终结果,下面这套可以直接挪用。
package.json:
lint-staged.config.mjs:
commitlint.config.mjs:
.husky/pre-commit:
.husky/commit-msg:
这套方案的特点是:
- 从 0 到 1 接入成本低
- 文件结构清楚,适合团队共享
- 职责划分明确,后续扩展也方便
10. 结合 commitizen + cz-git 使用提交向导
前面的方案解决的是“提交前检查代码”和“提交信息格式校验”。
如果你们团队还希望开发者更容易写出合规的提交信息,可以在这条链路上再加一层提交向导:
commitizen:负责启动交互式提交流程cz-git:作为commitizen的 adapter,负责具体的问答和格式化输出commitlint:负责在commit-msg阶段做最终兜底校验
这三个工具放在一起,各自职责会更清楚:
commitizen + cz-git负责“帮助你写对”commitlint负责“兜底拦截写错的提交信息”
10.1 推荐理解成“引导输入 + 最终校验”
很多团队第一次接这套方案时,容易把 cz-git 和 commitlint 的职责混在一起。
更准确的理解是:
- 开发者执行
git cz、cz,或者项目里约定好的脚本命令 commitizen调起cz-gitcz-git通过交互式问题引导你填写type、scope、subject- 生成一条符合约定格式的提交信息
- Git 进入
commit-msg阶段后,commitlint再做一次校验
这样做的好处是:
- 对新人更友好,不需要死记
Conventional Commits - 团队提交流程更统一
- 即使有人直接执行
git commit -m,commitlint也还能兜底
10.2 安装依赖
如果你已经接入了上面的 husky + lint-staged + commitlint,现在只需要再补装两个依赖:
如果你希望全局直接使用 git cz 或 cz,还可以额外全局安装 commitizen:
但从团队项目治理角度,更推荐:
- 项目内把
commitizen、cz-git固定为本地依赖 - 团队统一通过脚本命令触发
这样不同成员不会因为全局版本差异导致体验不一致。
10.3 在 package.json 里指定 cz-git 适配器
根据 cz-git 官方文档,项目里需要告诉 commitizen:当前仓库使用哪个 adapter。
可以在 package.json 里加上:
这里建议把脚本名写成 cm,而不是 commit。原因是 commitizen 官方特别提醒过:如果项目里同时有 Hook 或 precommit 相关链路,脚本名叫 commit 时,某些 npm 脚本行为可能会让 precommit 被重复触发。
接好以后,团队可以统一使用:
如果你本机全局装了 commitizen,也可以直接用:
或者:
10.4 cz-git 的配置,优先放到 commitlint.config.mjs
cz-git 官方更推荐:如果项目本身已经在用 commitlint,那就把交互提示相关配置直接写在 commitlint 配置文件里,避免把 package.json 塞得太重。
最简单的接法是先保留你现有的 commitlint 配置,只额外补一个 prompt 字段:
这意味着:
commitlint继续负责校验提交信息cz-git读取同一份配置里的prompt选项
如果后面你们团队想继续定制:
- 可选的
type列表 - 是否启用 emoji
scope的选择方式- issue / breaking change 的提示文案
也建议继续优先放在 commitlint.config.mjs 里维护。
10.5 一套和本文方案配套的最小落地配置
如果你希望把“代码检查 + 提交信息向导 + 提交信息校验”一次接完整,最小版本可以是这样:
package.json:
commitlint.config.mjs:
.husky/pre-commit:
.husky/commit-msg:
10.6 实际使用流程
日常提交时,推荐团队这样操作:
然后按交互提示依次选择或填写:
type,比如feat、fix、docsscope,比如blog、build、lint-stagedsubject,也就是一句简短说明
提交时整条链路会按下面顺序执行:
commitizen启动交互式提交cz-git生成提交信息pre-commit执行lint-stagedcommit-msg执行commitlint- 全部通过后,提交完成
10.7 团队落地时的两个建议
第一,不要把 commitizen + cz-git 误当成 commitlint 的替代品。
原因很简单:
- 向导是“帮助你写”
- 校验是“防止你绕过向导后写错”
第二,不要强制把交互式提交绑进默认 pre-commit。
更稳的做法是:
- 团队约定平时优先用
pnpm run cm或git cz - 仓库继续保留
commit-msg -> commitlint作为兜底
这样开发体验和稳定性通常会更平衡。
11. 验证步骤
如果你是第一次落地这套方案,建议按下面的检查单走一遍:
- 确认
package.json已经有prepare - 确认根目录存在
.husky/pre-commit - 确认根目录存在
.husky/commit-msg - 确认根目录存在
lint-staged.config.mjs - 确认根目录存在
commitlint.config.mjs - 故意提交一条错误信息,验证
commitlint会拦截 - 故意提交一个格式有问题的文件,验证
lint-staged会处理
全部通过以后,这套链路才算真正接好了。
12. 常见问题
12.1 为什么 pre-commit 很慢
常见原因:
- 在 Hook 里跑了整仓任务
- 跑了测试、构建、类型检查
lint-staged规则写得过重
解决方式:
pre-commit只保留 staged files 相关检查- 慢任务移到
pre-push或 CI
12.2 为什么文件被改了但没有提交成功
这是因为某个任务修改了文件后,后续校验又失败了。默认情况下,lint-staged 会处理回滚和暂存状态保护。
如果你看到了文件被格式化但提交被中断,通常不是异常,而是某个检查没通过。
12.3 使用后提交失败,感觉代码丢失了,怎么还原
这个场景最常见的原因不是“代码真的没了”,而是 lint-staged 在失败时把工作区和暂存区状态回滚了,导致你以为刚才的修改被吞掉了。
官方文档明确说明:lint-staged 默认会在运行前创建一个 git stash 备份,用来防止数据丢失。
遇到这种情况,先不要继续执行 git reset --hard 或手动乱改文件,优先按下面步骤恢复。
第一步,查看 stash 列表:
如果是 lint-staged 生成的备份,通常会看到类似这样的记录:
第二步,把这份备份恢复回来,并尽量恢复到暂存区:
如果你更习惯用 stash@{0} 这种写法,也可以:
恢复后建议立刻执行:
确认文件和暂存状态都已经回来。
如果确认恢复成功,再决定是:
- 修正 Hook 报错后重新提交
- 临时把修改先保存到新分支或额外 commit 中
如果你确定这条 stash 已经没用了,可以最后再手动删除:
需要注意两个例外场景:
- 如果项目用了
--no-stash,那lint-staged不会生成备份 stash - 如果项目用了
--diff,官方文档说明它会隐式启用--no-stash
这两种情况下,就不能指望从 lint-staged automatic backup 里恢复,只能去看:
- 当前工作区是否还保留改动
- IDE 的 Local History
- 你自己是否有额外 stash、分支或 patch 备份
12.4 为什么有的文件没被处理
先检查 3 件事:
- 文件是否真的
git add进暂存区了 - glob 规则是否匹配到该文件
- 对应工具本身是否忽略了该文件,例如
.prettierignore
12.5 Windows 下有什么要注意的
建议注意两点:
- Hook 文件使用
UTF-8编码 - 手动创建 Hook 时,注意
"$1"等参数写法不要被 shell 转义破坏
13. 一页总结
lint-staged + pre-commit 的核心价值,不是“把所有检查都塞进提交前”,而是:
- 只检查本次提交的文件
- 用最快的方式拦截低级问题
- 把慢任务留给
pre-push或 CI
推荐团队采用下面这套职责分工:
husky管理 Git Hookspre-commit运行lint-stagedlint-staged只处理 staged filescommitizen + cz-git负责引导生成提交信息commit-msg运行commitlintpre-push或 CI 承担整仓类型检查、测试和构建
如果你希望它长期稳定,不要忽略这几个最佳实践:
- 不要在
lint-staged里再写git add - 修改类命令要用数组语法保证顺序
- 避免重叠 glob 造成并发冲突
- 不要在默认
pre-commit里跑整仓慢任务 - 团队统一配置文件位置和 Hook 写法
当这套链路配置清楚以后,开发者的提交流程会更稳定,代码质量也更容易长期维持。

