ESLint、Prettier、husky 和 lint-staged 分别做什么?怎样配合?
ESLint 按规则找出可能的 bug 和不推荐的写法,部分规则能自动修复;Prettier 不按规则,而是把整段代码重新打印成统一格式;husky 帮你配置 Git 钩子,比如在提交前执行命令;lint-staged 只对暂存区的文件运行这些检查。常见做法是 husky 的 pre-commit 钩子调用 lint-staged,lint-staged 再对改动的文件跑 ESLint 和 Prettier。
简答
团队里有人提交了一段声明了却没用的变量,有人缩进用两个空格、有人用四个,代码评审时大家都在争格式。这四个工具就是用来把这些事交给机器的。
ESLint 和 Prettier 分工不同。ESLint 是 linter,按一条条规则检查代码,比如
no-unused-vars找出没用到的变量,能发现真正的 bug,部分规则还能用--fix自动修。Prettier 是格式化工具,它不按规则挑错,而是把整段代码按统一风格重新打印一遍。Prettier 官方的说法是:格式交给 Prettier,抓 bug 交给 linter。husky 和 lint-staged 负责“什么时候跑”。husky 用来配置 Git 钩子,比如在
git commit之前执行一段命令。lint-staged 只拿暂存区里的文件去跑命令,不会把整个仓库检查一遍,所以提交前的检查很快。常见组合是 husky 的pre-commit钩子执行npx lint-staged,lint-staged 再对改动的 JS 文件依次跑eslint --fix和prettier --write。要注意,ESLint 里有些格式类规则会和 Prettier 冲突,可以用
eslint-config-prettier关掉它们。另外 Git 钩子可以被跳过,所以 CI 里还要再跑一次检查。
ESLint 和 Prettier 为什么要一起用
Prettier 文档把 linter 的规则分成两类。一类是格式规则,比如 max-len、keyword-spacing,管的是空格、换行、逗号放哪。另一类是代码质量规则,比如 no-unused-vars、prefer-promise-reject-errors,管的是代码有没有问题。
Prettier 只负责第一类,而且做法和 linter 不一样:它没有可以单独开关的格式规则,而是把整个程序从头重新打印一遍,所以格式不可能写错。第二类它完全不管,这一类恰恰是 linter 最重要的部分,因为能抓到真正的 bug。
ESLint 文档对自己的定义是一个可配置的 JavaScript linter,帮你发现并修复代码里的问题。规则是它的核心,每条规则检查代码是否满足某个预期;有的规则能提供自动修复,用 --fix 命令行参数或者编辑器插件就能应用,修复不会改变代码逻辑。插件可以带来更多规则,比如针对 TypeScript 或 React 的规则。
两个工具都管格式时就会打架:ESLint 的某条格式规则要求这样写,Prettier 重新打印成了另一种样子。Prettier 文档的建议是用 eslint-config-prettier 关掉和 Prettier 冲突或者多余的规则。它也提到可以用 eslint-plugin-prettier 把 Prettier 当成一条 ESLint 规则来跑,但一般不推荐,因为编辑器里会满屏红线,速度也比直接跑 Prettier 慢。
提交前自动检查是怎么串起来的
lint-staged 的 README 给出的推荐安装步骤是三步:安装 lint-staged;配置一个 pre-commit Git 钩子去运行它,README 推荐 husky;再配置要对哪些文件跑哪些命令。
husky 这一步用 npx husky init 完成。husky 文档说明,这条命令会在 .husky/ 目录下创建一个 pre-commit 脚本,并更新 package.json 里的 prepare 脚本。把脚本内容改成运行 lint-staged:
# .husky/pre-commit
npx lint-staged
然后在 package.json 里配置 lint-staged,键是文件匹配规则,值是要运行的命令:
{
"lint-staged": {
"*.{js,jsx}": ["eslint --fix", "prettier --write"],
"!(*.js|*.jsx)": "prettier --write"
}
}
为什么命令后面不写文件路径?因为 lint-staged 会把匹配到的暂存文件作为参数追加到命令后面,README 特意提醒不要自己传路径。命令改过的文件,默认会自动加回暂存区,一起提交。
为什么 JS 文件要写成数组,而不是写两条 "*": "prettier --write" 和 "*.js": "eslint --fix"?README 解释,lint-staged 默认并行执行不同匹配规则的任务,两条规则都匹配到同一个文件时,prettier 和 eslint 可能同时改写它,出现竞争。写成数组的命令按顺序执行,再用取反的匹配规则让其他文件只跑 Prettier,就不会重叠。
检查失败时会发生什么
ESLint 报出无法自动修复的错误时,命令以非零状态退出,lint-staged 也就失败,提交被中止。lint-staged 默认在运行任务前会先创建一个 git stash 作为备份,防止改动丢失。
但钩子不是强制的。git commit --no-verify 可以跳过本地钩子,husky 也可以用 HUSKY=0 关闭,所以提交前检查只是让问题尽早暴露,CI 里仍然要运行 eslint . 和 prettier --check . 作为最后一道关。
常见追问
为什么不在 pre-commit 里直接跑 eslint .?
可以,但仓库大了以后每次提交都要检查所有文件,会很慢,而且会被别人留下的旧问题卡住。lint-staged 只检查这次提交改到的文件,速度快,也只要求你为自己的改动负责。
prepare 脚本有什么用?
npm 文档说明,在项目里执行不带包名的
npm install时会运行prepare脚本。husky 把自己的安装命令写在这里,团队成员拉下代码、装完依赖,Git 钩子就自动配好了,不用每人手动设置。
ESLint 的配置文件应该怎么写?
现在的 ESLint 使用 Flat Config,也就是
eslint.config.js导出的配置数组,具体写法见《ESLint Flat Config 和旧配置方式有什么区别?》。