代码质量与规范体系

代码质量不是"多花时间写好代码"这么简单——它是一套工具链 + 流程 + 团队共识的系统工程。好的规范体系让"做对的事"成为阻力最小的路径:格式化自动完成、低质量代码提交不了、变更历史清晰可追溯。本章从 Nuxt4 项目的 ESLint 配置讲起,覆盖格式化、Git 钩子、TypeScript 严格模式、提交规范到自动化 Changelog 的完整链路。

1. ESLint:从"查错"到"统一风格"

1.1 ESLint 在现代项目中的角色

ESLint 已经不仅仅是"检查语法错误"的工具。在 2024+ 的前端项目中,ESLint 承担三重职责:

  • 错误检测:未使用的变量、未处理的 Promise、意外的全局变量
  • 风格统一:缩进、分号、引号、import 排序——消除团队中的"格式圣战"
  • 最佳实践:Vue 组件命名规范、TypeScript 类型安全、Nuxt 特有规则

1.2 @nuxt/eslint-config:一行配置

Nuxt4 推荐使用 @nuxt/eslint-config,它是 Anthony Fu 的 @antfu/eslint-config 的 Nuxt 定制版,基于 ESLint Flat Config 格式:

// eslint.config.mjs
import { createConfigForNuxt } from '@nuxt/eslint-config/flat'
 
export default createConfigForNuxt({
  features: {
    tooling: true,    // 启用 Prettier 集成
    stylistic: true,  // 启用代码风格规则
  },
})

这一行配置自动包含了:

  • TypeScript 规则:类型安全、未使用变量、any 限制
  • Vue 规则:组件命名、模板语法、编译器宏使用
  • Nuxt 规则:auto-import 兼容、server/app/ 分区规则
  • Import 排序:自动整理 import 语句顺序

1.3 Flat Config vs Legacy Config

ESLint 9+ 默认使用 Flat Configeslint.config.mjs),取代了旧的 .eslintrc.* 格式。关键区别:

特性Flat ConfigLegacy Config
文件格式eslint.config.mjs(JS 模块).eslintrc.json / .eslintrc.js
配置合并数组拍平,显式覆盖extends 链式继承,隐式合并
插件加载显式 import字符串引用,自动 resolve
类型支持原生 TypeScript 类型需要额外类型包

Flat Config 的优势在于透明性——你能清楚看到每条规则的来源,不会出现"不知道哪个 extends 覆盖了我的配置"的问题。

1.4 自定义规则调整

// eslint.config.mjs
import { createConfigForNuxt } from '@nuxt/eslint-config/flat'
 
export default createConfigForNuxt({
  features: {
    stylistic: {
      semi: false,          // 不要分号
      quotes: 'single',     // 单引号
      indent: 2,            // 2 空格缩进
    },
  },
}).override('nuxt/vue/rules', {
  rules: {
    'vue/max-attributes-per-line': ['error', { singleline: 3 }],
    'vue/no-v-html': 'warn',  // v-html 警告而非报错
  },
}).override('nuxt/typescript/rules', {
  rules: {
    '@typescript-eslint/no-explicit-any': 'warn',
    '@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }],
  },
})

.override() 链式 API 让规则调整非常清晰——你知道自己在覆盖哪个配置块的哪条规则。

2. 格式化:ESLint Stylistic vs Prettier

2.1 争论的本质

"ESLint 还是 Prettier?"这个争论的本质是:格式化应该由 Linter 还是专门的格式化工具来做?

Prettier 阵营:格式化是独立关注点,应该用专门工具处理。Prettier 的"零配置"哲学(故意不提供太多选项)强制团队统一风格,减少争论。

ESLint Stylistic 阵营(Anthony Fu 主导):ESLint 已经在解析 AST 了,格式化规则可以直接复用 AST,不需要再跑一个工具。而且 ESLint 规则的粒度更细,可以做 Prettier 做不到的事(比如强制 import 排序)。

2.2 Nuxt4 的推荐方案

@nuxt/eslint-config 默认使用 ESLint Stylistic——不需要 Prettier。这是 Nuxt 生态的官方推荐。

如果你的团队已经在用 Prettier 且不想迁移,可以关闭 ESLint 的样式规则,让 Prettier 接管:

export default createConfigForNuxt({
  features: {
    stylistic: false,  // 关闭 ESLint 样式规则
  },
})

然后配置 Prettier:

// .prettierrc
{
  "semi": false,
  "singleQuote": true,
  "trailingComma": "all",
  "printWidth": 100,
  "vueIndentScriptAndStyle": true
}

建议:新项目用 ESLint Stylistic(少一个工具、少一套配置);已有项目如果 Prettier 运行良好就不必迁移。

3. Git 钩子:Husky + lint-staged

3.1 为什么需要 Git 钩子

ESLint 配置再好,如果开发者不跑 lint,等于没有。Git 钩子在 git commit 时自动执行检查——提交不合规的代码时直接拒绝,从源头保证代码质量。

3.2 工作原理

开发者执行 git commit
        ↓
Husky 拦截 pre-commit 钩子
        ↓
lint-staged 只对本次暂存的文件执行检查
        ↓
ESLint 检查 → 通过 → 提交成功
                 → 失败 → 提交被拒绝

关键设计:lint-staged 只检查暂存文件(staged files),而非整个项目。这有两个好处:

  1. 速度快——只检查你改动的文件,不受项目规模影响
  2. 渐进式修复——旧代码不会阻塞新代码的提交

3.3 配置

// package.json
{
  "scripts": {
    "lint": "eslint .",
    "lint:fix": "eslint . --fix",
    "prepare": "husky"
  },
  "lint-staged": {
    "*.{js,ts,vue,mjs,cjs}": [
      "eslint --fix"
    ]
  }
}
# .husky/pre-commit
npx lint-staged

eslint --fix 在 lint-staged 中运行时会自动修复格式问题(缩进、分号等),只有无法自动修复的真实错误才会阻止提交。这让开发者体验更顺畅——大部分格式问题静默修复,不会打断工作流。

3.4 commit-msg 钩子

除了代码检查,还可以在 commit-msg 钩子中验证提交信息格式(下文 Conventional Commits):

# .husky/commit-msg
npx commitlint --edit $1

4. TypeScript 严格模式

4.1 Nuxt4 的 TypeScript 支持

Nuxt4 内置 TypeScript 支持,零配置即可使用 .ts<script setup lang="ts">。但默认的类型检查并不严格——很多隐患会被放过。

4.2 严格模式的价值

TypeScript 的 strict 模式是一组编译选项的集合:

选项作用抓到的典型 bug
strictNullChecks不允许 null/undefined 隐式赋值user.name 在 user 为 null 时崩溃
noImplicitAny不允许隐式 any 类型函数参数没有类型,传什么都不报错
strictFunctionTypes函数参数逆变检查回调函数参数类型不匹配
strictPropertyInitializationclass 属性必须初始化构造函数遗漏赋值
noUncheckedIndexedAccess数组/对象索引访问返回 `Tundefined`

4.3 Nuxt4 配置

// nuxt.config.ts
export default defineNuxtConfig({
  typescript: {
    strict: true,
    typeCheck: true,   // 构建时类型检查(CI 中推荐开启)
  },
})
// tsconfig.json — Nuxt4 自动生成,可手动增强
{
  "extends": "./.nuxt/tsconfig.json",
  "compilerOptions": {
    "strict": true,
    "noUncheckedIndexedAccess": true,
    "exactOptionalPropertyTypes": true
  }
}

4.4 渐进式启用

在已有项目上直接开启 strict: true 可能产生上百个类型错误。推荐渐进策略:

  1. 先开启 strictNullChecks(最有价值,抓到的 bug 最多)
  2. 再开启 noImplicitAny
  3. 最后开启完整 strict
  4. 每步修复完当前错误再开启下一个

或者用 // @ts-expect-error 临时压制存量错误,新代码严格遵守,逐步消化历史债务。

5. Conventional Commits

5.1 为什么规范提交信息

不规范的提交历史:

fix bug
update
wip
asdfasdf
修了个问题

这种提交历史毫无信息量——三个月后你无法从中了解项目发生了什么。

规范的提交历史:

feat(video): add AI video generation endpoint
fix(auth): prevent token refresh race condition
perf(player): lazy load HLS.js to reduce initial bundle
docs(readme): update deployment instructions

5.2 Conventional Commits 格式

<type>(<scope>): <description>

[optional body]

[optional footer(s)]

type 决定了变更的性质:

type含义触发版本号
feat新功能minor(0.x.0)
fixBug 修复patch(0.0.x)
perf性能优化patch
refactor重构(不改变行为)
docs文档
style格式调整(不影响逻辑)
test测试
chore构建/工具/依赖
ciCI 配置

scope 标识变更影响的模块(authvideoplayerapi),让提交历史可按模块筛选。

BREAKING CHANGE:在 footer 或 type 后加 ! 表示破坏性变更,触发 major 版本号。

5.3 commitlint 强制执行

// commitlint.config.ts
export default {
  extends: ['@commitlint/config-conventional'],
  rules: {
    'scope-enum': [2, 'always', [
      'video', 'auth', 'player', 'ai', 'api', 'ui', 'config', 'deps',
    ]],
    'subject-max-length': [2, 'always', 72],
    'body-max-line-length': [2, 'always', 100],
  },
}

配合 Husky 的 commit-msg 钩子,不符合格式的提交信息会被拒绝,并给出修正建议。

5.4 工具辅助

手动记住格式很烦。用 commitizencz-conventional-changelog)提供交互式提交向导:

npx cz
# ? Select the type of change: feat
# ? What is the scope: video
# ? Write a short description: add thumbnail auto-generation

或者用 VS Code 的 Conventional Commits 插件,在 Source Control 面板中直接选择 type 和 scope。

6. 自动化 Changelog

6.1 从提交历史生成 Changelog

Conventional Commits 的真正威力在于自动化——规范的提交信息可以被工具解析,自动生成 Changelog 和版本号。

git log:
  feat(video): add batch upload
  fix(auth): session timeout handling
  feat(ai): support Stable Diffusion models
  fix(player): iOS fullscreen orientation

自动生成:
## v1.3.0 (2025-01-15)

### Features
- **video**: add batch upload
- **ai**: support Stable Diffusion models

### Bug Fixes
- **auth**: session timeout handling
- **player**: iOS fullscreen orientation

6.2 工具选型

工具特点适用场景
changelogenNuxt 团队出品,轻量Nuxt 项目(推荐)
changelogithub从 GitHub Release 生成开源项目
release-it全流程自动化(version + tag + changelog + publish)npm 包发布
semantic-release完全自动化,零手动干预CI/CD 驱动的项目

6.3 changelogen 配置

// package.json
{
  "scripts": {
    "release": "changelogen --release"
  }
}

执行 npm run release 后自动:

  1. 解析自上次发布以来的所有 commit
  2. 根据 type 计算新版本号(feat → minor,fix → patch)
  3. 生成 CHANGELOG.md
  4. 更新 package.json 的 version
  5. 创建 Git tag

6.4 发布流程

开发 → commit(Conventional Commits)
         ↓
PR Review → merge to main
         ↓
CI 运行 changelogen → 更新版本号 + CHANGELOG
         ↓
创建 Git Tag → 触发部署

7. 完整的质量门禁体系

7.1 质量门禁全景

将所有质量工具串联成一条自动化流水线:

阶段工具检查内容触发时机
编码时ESLint + IDE 插件实时错误提示每次保存
提交时Husky + lint-staged代码规范git commit
提交时commitlint提交信息格式git commit
PR 时GitHub Actions + ESLint全量代码检查Push / PR
PR 时Vitest单元测试 + 覆盖率Push / PR
PR 时TypeScript类型检查Push / PR
合并后changelogenChangelog + 版本号Merge to main

7.2 CI 工作流

# .github/workflows/quality.yml
name: Quality Gate
on: [push, pull_request]
 
jobs:
  lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
      - run: npm ci
      - run: npm run lint
 
  typecheck:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
      - run: npm ci
      - run: npx nuxi typecheck
 
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
      - run: npm ci
      - run: npx vitest run --coverage

三个 job 并行执行,任何一个失败都阻止 PR 合并。

8. 规范落地的心法

8.1 工具先行,文档后补

不要指望一份代码规范文档就能让团队统一风格。可执行的规则 > 写在文档里的规则。ESLint 报错比 Wiki 页面有效 100 倍。

8.2 自动修复优先

能自动修复的就自动修复(eslint --fix)。开发者应该把精力花在业务逻辑上,而不是手动调整缩进和分号。

8.3 渐进式推进

不要一次性引入所有规则。先从影响最大的开始(strictNullChecks、Conventional Commits),稳定后再逐步加严。一次改太多会让团队产生抵触情绪。

8.4 CI 是最后防线

本地钩子可以被 --no-verify 绕过。CI 中的检查无法绕过——它是质量门禁的最后防线,必须严格。

本章小结

  • ESLint@nuxt/eslint-config 一行配置覆盖 TS + Vue + Nuxt 规则,Flat Config 透明可控
  • 格式化:ESLint Stylistic(新项目推荐)或 Prettier(已有项目),二选一
  • Git 钩子:Husky + lint-staged 在提交时自动检查暂存文件,渐进式修复
  • TypeScript 严格模式strictNullChecks 价值最高,渐进式开启避免一次性爆炸
  • Conventional Commitstype(scope): description 格式,commitlint 强制执行
  • 自动化 Changelog:changelogen 从规范提交自动生成版本号和变更日志
  • 质量门禁:编码时 → 提交时 → PR 时 → 合并后,四层自动化保障