代码质量与规范体系
代码写一次,读无数次。一个人写代码可以随心所欲,但团队协作中,风格不一致、提交混乱、低级错误频发会极大拖慢效率。代码质量不是靠"约定"维护的——人会忘、会偷懒、会赶工——必须靠工具自动化。本章搭建一套"提交即合规"的质量保障体系:ESLint 检查逻辑问题,Prettier 统一格式,Husky 在提交前拦截,Conventional Commits 规范提交信息,最终形成从编码到发布的全链路质量闭环。
1. 代码质量的三个层次
在讨论具体工具之前,先理解代码质量包含哪些维度:
1.1 三层模型
| 层次 | 关注点 | 工具 | 示例 |
|---|---|---|---|
| 格式层 | 缩进、引号、分号、换行 | Prettier | 单引号 vs 双引号 |
| 逻辑层 | 潜在 bug、未使用变量、类型安全 | ESLint + TypeScript | 未 await 的 Promise |
| 流程层 | 提交规范、分支策略、发布流程 | Husky + Commitlint + Changesets | feat: add login page |
三层各司其职,不要混用。常见的反模式是让 ESLint 管格式(如 indent 规则)——这会导致 ESLint 和 Prettier 冲突。正确的做法:格式归 Prettier,逻辑归 ESLint。
1.2 自动化程度金字塔
/\
/ \ CI/CD 门禁(PR 必须通过检查才能合并)
/----\
/ \ Git Hook(提交前自动检查)
/--------\
/ \ 编辑器实时提示(保存时自动格式化)
/------------\
/ \ 手动检查(代码 Review)
越往上自动化程度越高、拦截越早。最佳实践是四层全部配置——编辑器实时反馈 → 提交前 Hook 拦截 → CI 门禁兜底 → 人工 Review 补充。
2. ESLint:代码逻辑守卫
2.1 ESLint 的定位
ESLint 不是格式化工具——它是静态分析工具,核心价值是发现代码中的潜在问题:
- 未使用的变量和 import
- 未
await的异步函数 - React Hook 的依赖数组遗漏
- 可访问性问题(img 缺少 alt)
- 类型不安全的操作
2.2 Flat Config:ESLint 的新时代
ESLint v9 默认使用 Flat Config(eslint.config.mjs),取代了旧的 .eslintrc 系列配置。这是 ESLint 历史上最大的架构变更。
为什么要迁移到 Flat Config?
| 维度 | 旧 .eslintrc | 新 Flat Config |
|---|---|---|
| 配置格式 | JSON/YAML/JS,多种格式混用 | 纯 JS/TS,类型安全 |
| 插件引用 | 字符串名称("plugin:react/recommended") | 直接 import 对象 |
| 配置合并 | extends + overrides,隐式合并规则复杂 | 数组展开,所见即所得 |
| 性能 | 每次 lint 都要解析配置 | 配置只解析一次 |
| 全局忽略 | 需要 .eslintignore 文件 | 直接在配置中声明 ignores |
Flat Config 的核心思想:配置就是一个 JavaScript 数组,每个元素是一个配置对象,按顺序合并。没有隐式继承,没有魔法字符串。
2.3 Next.js 项目的 ESLint 配置
// eslint.config.mjs
import js from '@eslint/js'
import tseslint from 'typescript-eslint'
import react from 'eslint-plugin-react'
import reactHooks from 'eslint-plugin-react-hooks'
import jsxA11y from 'eslint-plugin-jsx-a11y'
import importX from 'eslint-plugin-import-x'
export default tseslint.config(
{ ignores: ['.next/', 'node_modules/', 'dist/'] },
js.configs.recommended,
...tseslint.configs.recommended,
{
plugins: { react, 'react-hooks': reactHooks, 'jsx-a11y': jsxA11y, 'import-x': importX },
rules: {
...reactHooks.configs.recommended.rules,
'react/react-in-jsx-scope': 'off',
'jsx-a11y/alt-text': 'error',
'import-x/order': ['error', {
groups: ['builtin', 'external', 'internal', 'parent', 'sibling'],
'newlines-between': 'always',
}],
},
},
)2.4 关键插件选型
| 插件 | 用途 | 必要性 |
|---|---|---|
@eslint/js | ESLint 内置推荐规则 | ✅ 必装 |
typescript-eslint | TypeScript 类型感知规则 | ✅ 必装 |
eslint-plugin-react-hooks | Hook 规则(依赖数组检查) | ✅ 必装 |
eslint-plugin-jsx-a11y | 可访问性检查 | ✅ 推荐 |
eslint-plugin-import-x | import 排序和检查 | ✅ 推荐 |
eslint-plugin-react | React 通用规则 | 可选(Next.js 场景用处有限) |
@next/eslint-plugin-next | Next.js 特定规则 | 可选(如 Image 组件检查) |
选型建议:不要装太多插件。每个插件都有学习成本和维护成本。核心五个(js + ts + hooks + a11y + import)足以覆盖 95% 的场景。
2.5 自定义规则策略
团队应该对规则有明确的分级策略:
| 级别 | 含义 | 示例 |
|---|---|---|
error | 必须修复,CI 不通过 | 未使用变量、Hook 规则违反 |
warn | 应该修复,但不阻塞 | console.log、过长的函数 |
off | 明确关闭 | 与 Prettier 冲突的格式规则 |
原则:宁可少开规则,也不要开了不遵守。一个总是被 // eslint-disable 的规则不如直接关掉。
2.6 TypeScript 类型感知规则
typescript-eslint 提供两组规则集:
recommended:基础规则,不需要类型信息,速度快recommendedTypeChecked:高级规则,需要 tsconfig 提供类型信息,能发现更深层的问题
// 类型感知规则示例
// ❌ 这个 bug 只有 type-checked 规则能发现
async function getData() {
const result = fetch('/api/data') // 忘了 await!
return result.json() // result 是 Promise,没有 .json()
}类型感知规则虽然强大,但会让 ESLint 变慢(因为需要加载完整的 TypeScript 类型系统)。建议:编辑器用 recommended(快速反馈),CI 用 recommendedTypeChecked(深度检查)。
3. Prettier:格式的终结者
3.1 Prettier 的哲学
Prettier 的核心理念是有主见(opinionated)——它不给你太多选项,而是帮你做决定。这正是它的价值:团队不再需要争论缩进是 2 个还是 4 个空格、是否加分号、用单引号还是双引号。Prettier 说了算,讨论结束。
3.2 Prettier vs ESLint 格式规则
| 维度 | Prettier | ESLint 格式规则 |
|---|---|---|
| 定位 | 代码格式化器 | 代码检查器(格式只是副业) |
| 工作方式 | 解析 AST → 重新打印(完全重写) | 逐条规则检查 + 修复 |
| 配置项 | ~20 个(故意少) | 数百个格式规则 |
| 一致性 | 100%(相同输入永远相同输出) | 可能遗漏边角情况 |
| 冲突 | 无(只管格式) | 可能与 Prettier 冲突 |
结论:格式全部交给 Prettier,ESLint 只管逻辑。安装 eslint-config-prettier 来关闭所有与 Prettier 冲突的 ESLint 规则。
3.3 配置
Prettier 的配置项很少,这是故意的:
// .prettierrc
{
"semi": false,
"singleQuote": true,
"tabWidth": 2,
"trailingComma": "all",
"printWidth": 100,
"plugins": ["prettier-plugin-tailwindcss"]
}每个配置项的考量:
semi: false:JavaScript 中分号几乎不必要(ASI 机制),去掉减少噪音singleQuote: true:单引号比双引号少按一个 Shift,且与 JSX 的双引号区分明确trailingComma: "all":尾逗号让 git diff 更干净(新增一行不会改动上一行)printWidth: 100:默认 80 太窄,120 太宽,100 是平衡点prettier-plugin-tailwindcss:自动排序 Tailwind 类名,这是 Tailwind 官方推荐的
3.4 忽略配置
# .prettierignore
.next/
node_modules/
pnpm-lock.yaml
dist/
coverage/
3.5 编辑器集成
在 VS Code / Cursor 中,设置保存时自动格式化:
// .vscode/settings.json
{
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.formatOnSave": true,
"editor.codeActionsOnSave": {
"source.fixAll.eslint": "explicit"
}
}这样每次保存文件时:1)Prettier 格式化代码 → 2)ESLint 自动修复可修复的问题。开发者几乎不需要手动关心格式。
4. Husky + lint-staged:提交前的最后防线
4.1 为什么需要 Git Hook
即使编辑器配置了自动格式化,仍然会有人:
- 用不同的编辑器(没装 Prettier 插件)
- 关闭了自动格式化
- 通过命令行直接编辑文件
- 复制粘贴了未格式化的代码
Git Hook 是最后一道防线——代码在进入仓库之前必须通过检查。
4.2 Husky:管理 Git Hook
Husky 让 Git Hook 的管理变得简单——Hook 脚本存储在仓库中,pnpm install 后自动激活,团队所有人共享同一套 Hook。
pnpm add -D husky lint-staged
npx husky inithusky init 会:
- 在
package.json中添加prepare脚本 - 创建
.husky/pre-commit文件
4.3 lint-staged:只检查暂存文件
关键概念:lint-staged 只对 git add 后的暂存文件运行检查,而不是整个项目。这意味着:
- 速度极快(只检查你改动的文件)
- 不会因为历史遗留问题阻塞你的提交
- 渐进式改善代码质量
// package.json
{
"lint-staged": {
"*.{ts,tsx}": ["eslint --fix", "prettier --write"],
"*.{json,md,css}": ["prettier --write"]
}
}# .husky/pre-commit
npx lint-staged4.4 工作流程
开发者修改代码
↓
git add . ← 暂存修改的文件
↓
git commit -m "feat: ..." ← 触发 pre-commit Hook
↓
lint-staged 启动
↓
对暂存的 .ts/.tsx 文件:
1. eslint --fix ← 自动修复可修复的问题
2. prettier --write ← 格式化
↓
对暂存的 .json/.md/.css 文件:
1. prettier --write ← 格式化
↓
检查通过?→ 提交成功
检查失败?→ 提交被拒绝,显示错误信息
4.5 常见问题与解决
问题 1:大型项目 lint-staged 太慢
// 方案:限制 ESLint 只检查关键规则
{
"lint-staged": {
"*.{ts,tsx}": ["eslint --fix --quiet", "prettier --write"]
}
}--quiet 只报 error 级别的问题,忽略 warn,显著减少输出和处理时间。
问题 2:新人加入项目后 Hook 不生效
确保 package.json 中有 prepare 脚本:
{
"scripts": {
"prepare": "husky"
}
}pnpm install 会自动执行 prepare,激活 Husky。
问题 3:紧急情况需要跳过 Hook
git commit --no-verify -m "hotfix: critical bug"--no-verify 跳过所有 Git Hook。但这只能用于紧急情况——CI 门禁仍然会拦截不合规的代码。
5. Conventional Commits:提交信息的语义化
5.1 为什么需要规范提交信息
看看这些真实的提交信息:
fix bug
update
wip
asdf
修了一下
三个月后没人知道这些提交改了什么。Conventional Commits 规范化提交信息,让每条提交都有明确的语义:
feat(auth): add OAuth2 login with Google
fix(api): handle null response in user endpoint
docs(readme): update deployment instructions
5.2 格式规范
<type>(<scope>): <description>
[optional body]
[optional footer]
Type 分类:
| Type | 含义 | 示例 |
|---|---|---|
feat | 新功能 | feat(auth): add magic link login |
fix | 修复 bug | fix(api): handle timeout error |
docs | 文档 | docs: update API reference |
style | 格式(不影响逻辑) | style: format with prettier |
refactor | 重构(不改功能) | refactor(db): extract query builder |
perf | 性能优化 | perf(image): add lazy loading |
test | 测试 | test(auth): add login e2e test |
chore | 构建/工具 | chore(deps): update eslint to v9 |
ci | CI 配置 | ci: add playwright to GitHub Actions |
Scope:可选,标识改动的模块(如 auth、api、ui)。团队应该约定统一的 scope 列表。
Breaking Change:用 ! 标记或在 footer 中声明:
feat(api)!: change response format to JSON:API
BREAKING CHANGE: API response format changed from custom to JSON:API.
All clients need to update their response parsing logic.
5.3 Commitlint:自动验证提交信息
pnpm add -D @commitlint/cli @commitlint/config-conventional// commitlint.config.js
export default {
extends: ['@commitlint/config-conventional'],
rules: {
'type-enum': [2, 'always', [
'feat', 'fix', 'docs', 'style', 'refactor',
'perf', 'test', 'chore', 'ci', 'revert',
]],
'subject-max-length': [2, 'always', 72], // 标题不超过 72 字符
'body-max-line-length': [2, 'always', 100],
},
}# .husky/commit-msg
npx --no -- commitlint --edit $1现在 git commit -m "asdf" 会被直接拒绝,必须遵循 type(scope): description 格式。
5.4 Conventional Commits 的衍生价值
规范化提交信息不只是为了好看——它支撑了整个发布自动化链条:
自动生成 Changelog:
工具(如 changesets、semantic-release)可以从提交信息中自动提取变更:
## v2.1.0
### Features
- **auth**: add OAuth2 login with Google (#123)
- **dashboard**: add real-time analytics widget (#456)
### Bug Fixes
- **api**: handle null response in user endpoint (#789)自动版本号:
根据提交类型自动决定版本号递增(遵循 SemVer):
| 提交类型 | 版本变化 | 示例 |
|---|---|---|
fix | Patch(x.x.1 → x.x.2) | 1.2.3 → 1.2.4 |
feat | Minor(x.1.x → x.2.0) | 1.2.3 → 1.3.0 |
feat!(Breaking) | Major(1.x.x → 2.0.0) | 1.2.3 → 2.0.0 |
CI 触发条件:
根据提交类型决定 CI 行为——docs 类型不需要跑测试,feat/fix 需要完整流水线。
6. 完整质量体系架构
6.1 工具协作图
开发者写代码
↓
编辑器 ← Prettier(保存时格式化)+ ESLint(实时提示错误)
↓
git add + git commit
↓
pre-commit Hook (Husky)
↓
lint-staged → ESLint --fix + Prettier --write(只检查暂存文件)
↓
commit-msg Hook (Husky)
↓
Commitlint → 验证提交信息格式
↓
Push to Remote
↓
CI/CD Pipeline (GitHub Actions)
↓
├── ESLint (full project, type-checked)
├── TypeScript tsc --noEmit(类型检查)
├── Vitest(单元测试)
├── Playwright(E2E 测试)
└── Build(构建检查)
↓
所有检查通过 → 允许合并 PR
任何检查失败 → 阻塞合并
6.2 CI 门禁配置
Git Hook 是"软拦截"(可以 --no-verify 跳过),CI 是"硬拦截"(无法绕过):
# .github/workflows/quality.yml
name: Code Quality
on: [pull_request]
jobs:
quality:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v2
- uses: actions/setup-node@v4
with: { node-version: 20, cache: 'pnpm' }
- run: pnpm install
- run: pnpm eslint . # ESLint 全量检查
- run: pnpm tsc --noEmit # TypeScript 类型检查
- run: pnpm test -- --run # 单元测试
- run: pnpm build # 构建检查在 GitHub 仓库设置中启用 Branch Protection:
- Require status checks to pass → 勾选
qualityJob - Require pull request reviews → 至少 1 人 Review
- Do not allow bypassing → 即使管理员也不能跳过
6.3 渐进式采用策略
如果项目已有大量代码但没有质量工具,不要一次性全部开启——会产生成百上千个报错。推荐的渐进策略:
| 阶段 | 动作 | 耗时 |
|---|---|---|
| Week 1 | 安装 Prettier,全量格式化一次 | 1 天 |
| Week 2 | 安装 ESLint(只开 recommended),修复 error 级别 | 2-3 天 |
| Week 3 | 配置 Husky + lint-staged(只对新代码生效) | 半天 |
| Week 4 | 添加 Commitlint | 半天 |
| Week 5+ | 逐步开启更多 ESLint 规则 | 持续 |
关键原则:存量代码不改(或一次性批量格式化),增量代码必须合规。lint-staged 天然支持这个策略——只检查你改动的文件。
7. 进阶实践
7.1 Monorepo 中的质量配置
在 Turborepo 等 Monorepo 中,每个包可以有独立的 ESLint 配置,但共享基础规则:
packages/
├── eslint-config/ ← 共享的 ESLint 基础配置
│ └── index.mjs
├── web/ ← Next.js 应用
│ └── eslint.config.mjs ← 继承基础 + 添加 React 规则
├── api/ ← API 服务
│ └── eslint.config.mjs ← 继承基础 + 添加 Node 规则
└── shared/ ← 共享库
└── eslint.config.mjs ← 只用基础规则
7.2 自定义 ESLint 规则
当内置规则不满足需求时,可以编写自定义规则。常见场景:
- 禁止在 Server Component 中使用特定的 Client API
- 强制 API Route 必须有错误处理
- 检查环境变量命名是否符合约定
7.3 代码质量度量
除了工具检查,还可以用度量指标追踪质量趋势:
| 指标 | 工具 | 目标 |
|---|---|---|
| 测试覆盖率 | Vitest Coverage | > 80% |
| 类型覆盖率 | TypeScript strict mode | 100% |
| ESLint 违规数 | ESLint + CI Dashboard | 趋势下降 |
| 代码复杂度 | ESLint complexity 规则 | 单函数 < 15 |
| 重复代码率 | jscpd | < 5% |
| Bundle 大小 | @next/bundle-analyzer | 逐版本追踪 |
7.4 Code Review 的补充作用
工具能发现的问题有限——命名是否清晰、架构是否合理、业务逻辑是否正确,这些仍然需要人工 Review。好的 Code Review 文化应该:
- 关注设计而非格式(格式已经被 Prettier 处理了)
- 提出问题而非指令("这里用 Map 会不会更好?" vs "改成 Map")
- 限制 PR 大小(< 400 行改动,大 PR 没人认真看)
- 自动化能做的不要人做(不要在 Review 中指出缩进问题)
本章小结
- 三层分离:格式归 Prettier、逻辑归 ESLint、流程归 Husky + Commitlint,三者各司其职不混用
- ESLint Flat Config:v9 的新配置系统,纯 JS 数组,所见即所得,告别
extends的隐式合并 - Prettier 的哲学:有主见的格式化器,配置项越少越好,团队不再争论格式问题
- Git Hook 链:
pre-commit(lint-staged)+commit-msg(Commitlint),提交前自动检查 - lint-staged 精髓:只检查暂存文件,增量改善,不被历史债务阻塞
- Conventional Commits:语义化提交信息,支撑自动 Changelog 和版本号管理
- 四层防线:编辑器实时反馈 → Git Hook 拦截 → CI 门禁兜底 → 人工 Review 补充
- 渐进式采用:先 Prettier 格式化,再 ESLint,再 Hook,存量不改增量合规