ESLint、Prettier 和 Git Hooks 怎么配合保证代码规范?
一句话回答
ESLint 管代码质量(未使用的变量、==、Hooks 用法错误),Prettier 管格式(缩进、引号、换行)。两者的格式规则会冲突,用 eslint-config-prettier 关掉 ESLint 里和格式相关的规则,格式统一交给 Prettier。ESLint 9 起默认使用扁平配置 eslint.config.js。提交时用 husky 注册 Git Hooks,在 pre-commit 里运行 lint-staged,只检查暂存的文件;在 commit-msg 里用 commitlint 校验提交信息。本地的检查可以被 --no-verify 跳过,所以 CI 还要再跑一遍。
详细解析
分工:质量和格式
| ESLint | Prettier | |
|---|---|---|
| 关心什么 | 可能的错误和不好的写法 | 代码长什么样 |
| 例子 | no-unused-vars、eqeqeq、react-hooks/rules-of-hooks |
分号、引号、缩进、每行最大宽度 |
| 可配置程度 | 规则多,可以按需开关 | 选项很少,刻意减少争论 |
| 能否自动修复 | 部分规则可以 --fix |
总是直接重写格式 |
ESLint 过去也有很多格式类规则,和 Prettier 同时开启就会互相"打架":一个要加分号,一个要删分号。解决办法是在 ESLint 配置的最后加上 eslint-config-prettier,关掉所有和 Prettier 冲突的规则。另一种做法是 eslint-plugin-prettier,把 Prettier 作为一条 ESLint 规则运行,格式问题也以 lint 报错的形式出现,但会更慢,编辑器里也会出现大量红线。更推荐两个工具分别运行。
ESLint 的扁平配置
ESLint 9 起默认使用 eslint.config.js,ESLint 10 移除了旧的 .eslintrc.* 配置。和旧格式相比:
- 配置是一个数组,每一项用
files指定作用范围,后面的配置覆盖前面的 - 不再用字符串名称去查找插件和共享配置,而是直接
import进来,来源一目了然 env改成languageOptions.globals,常配合globals包使用;.eslintignore改成配置里的ignores或globalIgnores()defineConfig()提供类型提示,并支持在配置对象里用extends复用别的配置
Git Hooks:husky + lint-staged
- husky 负责安装 Git Hooks:
npx husky init会创建.husky/pre-commit,并在package.json里加上"prepare": "husky",其他人执行npm install时会自动启用钩子 - lint-staged 只对本次暂存的文件运行命令,并把文件路径作为参数传给命令。全量检查一个大仓库要几十秒,只检查改动的文件通常只要几秒
- 命令修改了文件(
eslint --fix、prettier --write)后,lint-staged 会把修改自动加回到这次提交中;有无法自动修复的错误时,提交被中止
提交信息:commitlint
commitlint 按 Conventional Commits 规范检查提交信息,例如 feat: 新增导出功能、fix(login): 修复验证码不刷新。规范的提交信息可以用来自动生成 CHANGELOG、判断版本号该升哪一位。它在 commit-msg 钩子里运行,读取 Git 传入的提交信息文件。
多道防线
编辑器保存时 → Prettier 格式化 + ESLint 实时提示 反馈最快
git commit → lint-staged 检查暂存文件 + commitlint 可以被 --no-verify 跳过
CI(PR 合并前) → eslint . + prettier --check . + 类型检查 最后兜底,不通过不能合并
编辑器层面,在 .vscode/settings.json 里开启 editor.formatOnSave,把默认格式化工具设为 Prettier,再把这个文件和推荐插件列表提交到仓库,让团队配置一致。
代码示例
// eslint.config.js
import js from '@eslint/js'
import { defineConfig, globalIgnores } from 'eslint/config'
import eslintConfigPrettier from 'eslint-config-prettier/flat'
import globals from 'globals'
export default defineConfig([
globalIgnores(['dist/', 'coverage/']),
{
files: ['**/*.{js,jsx}'],
extends: [js.configs.recommended],
languageOptions: {
globals: { ...globals.browser },
parserOptions: { ecmaFeatures: { jsx: true } },
},
rules: {
eqeqeq: 'error',
},
},
// 放在最后,关掉和 Prettier 冲突的格式类规则
eslintConfigPrettier,
])
package.json 中的 lint-staged 配置:
{
"scripts": {
"prepare": "husky"
},
"lint-staged": {
"*.{js,jsx,ts,tsx,vue}": ["eslint --fix", "prettier --write"],
"*.{css,md,json}": "prettier --write"
}
}
两个钩子文件和 commitlint 配置:
# .husky/pre-commit
npx lint-staged
# .husky/commit-msg
npx --no -- commitlint --edit $1
// commitlint.config.js
export default { extends: ['@commitlint/config-conventional'] }
面试官可能追问
老项目接入 ESLint 报了几千个错误,怎么推进?
不要一次性全量修复。先把格式化单独做一次提交(只改格式、不改逻辑,方便评审),再把规则分级:可能导致 bug 的规则设为 error,风格类的先设为 warn。配合 lint-staged,只要求"改到的文件必须通过",存量问题随日常修改逐步消化。ESLint 也支持批量抑制:eslint --suppress-all 把现有的 error 记录到 eslint-suppressions.json,之后只拦截新增的问题。
为什么 CI 里还要再检查一遍?
本地钩子可以被 git commit --no-verify 跳过,有人没执行 npm install 钩子就不会安装,直接在网页上改代码也不会触发钩子。lint-staged 只检查改动的文件,修改一个公共函数可能让别的文件出现问题。CI 对整个项目运行 lint 和类型检查,是合并前唯一可靠的关卡。
pre-commit 里要不要跑单元测试和类型检查?
看耗时。pre-commit 应该在几秒内完成,否则大家会习惯性地用 --no-verify。类型检查需要分析整个项目,单元测试可能更慢,一般放到 pre-push 钩子或 CI 里。只有测试很快时,才在 pre-commit 里运行和改动相关的测试。
易错点
eslint-config-prettier没有放在配置数组的最后,后面的配置又把格式类规则打开了- 照着旧教程写
.eslintrc.js,ESLint 9 默认不会读取它,ESLint 10 已不再支持 - 在 lint-staged 的命令里自己写
git add,新版本会自动把修改加回暂存区,不需要手动添加 - 以为有了 Git Hooks 就万无一失,没有在 CI 里设置检查
AI 模拟面试官
用自己的话回答,AI 对照参考答案打分、指出遗漏,再追问,最多 3 轮
这道题你掌握了吗?
选一个最接近的状态,没掌握的题会出现在"我的进度 · 待复习"里。
学习记录暂存在本机浏览器。登录后自动同步到账号,换设备也能看到。