代码质量与规范体系

代码写一次,读无数次。一个人写代码可以随心所欲,但团队协作中,风格不一致、提交混乱、低级错误频发会极大拖慢效率。代码质量不是靠"约定"维护的——人会忘、会偷懒、会赶工——必须靠工具自动化。本章搭建一套"提交即合规"的质量保障体系:ESLint 检查逻辑问题,Prettier 统一格式,Husky 在提交前拦截,Conventional Commits 规范提交信息,最终形成从编码到发布的全链路质量闭环。

1. 代码质量的三个层次

在讨论具体工具之前,先理解代码质量包含哪些维度:

1.1 三层模型

层次关注点工具示例
格式层缩进、引号、分号、换行Prettier单引号 vs 双引号
逻辑层潜在 bug、未使用变量、类型安全ESLint + TypeScript未 await 的 Promise
流程层提交规范、分支策略、发布流程Husky + Commitlint + Changesetsfeat: 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/jsESLint 内置推荐规则✅ 必装
typescript-eslintTypeScript 类型感知规则✅ 必装
eslint-plugin-react-hooksHook 规则(依赖数组检查)✅ 必装
eslint-plugin-jsx-a11y可访问性检查✅ 推荐
eslint-plugin-import-ximport 排序和检查✅ 推荐
eslint-plugin-reactReact 通用规则可选(Next.js 场景用处有限)
@next/eslint-plugin-nextNext.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 格式规则

维度PrettierESLint 格式规则
定位代码格式化器代码检查器(格式只是副业)
工作方式解析 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 init

husky init 会:

  1. package.json 中添加 prepare 脚本
  2. 创建 .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-staged

4.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修复 bugfix(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
ciCI 配置ci: add playwright to GitHub Actions

Scope:可选,标识改动的模块(如 authapiui)。团队应该约定统一的 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

工具(如 changesetssemantic-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):

提交类型版本变化示例
fixPatch(x.x.1 → x.x.21.2.3 → 1.2.4
featMinor(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 → 勾选 quality Job
  • 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 mode100%
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,存量不改增量合规