代码质量与规范
代码质量与规范
代码质量和规范是前端工程化的重要组成部分,直接影响代码的可维护性、可读性和可靠性。可以把一个代码库想象成一座持续扩建的城市:如果每个人都随意搭建、没有统一的规划标准,城市很快就会变成无法维护的贫民窟;而一套良好的代码规范和质量门禁,就相当于城市的建筑法规和验收流程,保证每一栋新楼都符合承重、消防、水电标准,从而让整座城市可以持续、安全地生长。
本文将围绕"代码质量如何定义与衡量、如何用工具体系(ESLint / Prettier / Stylelint / Commitlint / TypeScript / husky / SonarQube)落地、如何设计代码审查流程、如何把质量门禁接入 CI"这几条主线,配合大量可直接使用的配置与代码示例展开。
代码质量概念
定义
代码质量不是一个单一维度的指标,而是多个属性的综合体现:
为什么重要
低质量代码的成本往往是隐性的、被推迟的。一个"能跑就行"的实现,可能在三个月后成为整个团队的噩梦。行业里常用"技术债"来类比:像信用卡透支一样,短期借来的开发速度,未来要连本带息偿还。
有一个被广泛引用的经验数据:修复缺陷的成本随发现阶段呈数量级上升。
| 缺陷发现阶段 | 相对修复成本 | 典型说明 |
| --- | --- | --- |
| 编码时(IDE/lint) | 1x | 保存文件即报错,几秒钟修复 |
| 代码审查阶段 | 5x | 审查者发现,需要来回沟通 |
| 测试阶段 | 10x | QA 报 bug,需重新排期修复 |
| 生产环境 | 30x - 100x | 影响用户,可能伴随事故复盘与回滚 |
这张表解释了为什么把质量门禁前移(shift-left)如此重要:越早拦截,成本越低。
衡量指标
代码质量需要可量化才能被管理。常见的核心指标如下:
| 指标 | 含义 | 参考基准 | 工具 |
| --- | --- | --- | --- |
| 代码覆盖率 | 测试执行到的代码占比 | 核心模块 80%+,整体 60%+ | Jest / Vitest / c8 |
| 圈复杂度 | 函数中独立执行路径数量 | 单函数 < 10,超过 15 需拆分 | ESLint complexity / SonarQube |
| 代码重复率 | 重复代码块占总量比例 | < 5% | SonarQube / jscpd |
| 认知复杂度 | 人类理解代码的难度 | 单函数 < 15 | SonarQube |
| 技术债比率 | 修复债务时间 / 开发总时间 | < 5%(A 级) | SonarQube |
| 圈内可维护性指数 | 综合复杂度/体积/注释的评分 | > 20 为可维护 | 各类静态分析工具 |
##### 圈复杂度(Cyclomatic Complexity)原理
圈复杂度衡量的是一个函数中"独立路径"的数量,等价于代码里判定节点(if / for / while / case / && / ||)的个数加一。复杂度越高,需要的测试用例越多,出错概率也越大。
下面这个函数复杂度过高,可读性和可测试性都很差:
// 圈复杂度 ≈ 11,难以维护
function getDiscount(user, order) {
let discount = 0;
if (user.level === 'gold') {
if (order.amount > 1000) {
discount = 0.2;
} else if (order.amount > 500) {
discount = 0.15;
} else {
discount = 0.1;
}
} else if (user.level === 'silver') {
if (order.amount > 1000) {
discount = 0.15;
} else {
discount = 0.08;
}
} else {
if (order.amount > 1000) {
discount = 0.05;
} else {
discount = 0;
}
}
return discount;
}用"配置表 + 查表"的方式重构后,复杂度大幅下降,规则一目了然:
// 圈复杂度 ≈ 3,规则可读、易扩展
const DISCOUNT_RULES = {
gold: [
{ min: 1000, rate: 0.2 },
{ min: 500, rate: 0.15 },
{ min: 0, rate: 0.1 },
],
silver: [
{ min: 1000, rate: 0.15 },
{ min: 0, rate: 0.08 },
],
normal: [
{ min: 1000, rate: 0.05 },
{ min: 0, rate: 0 },
],
};
function getDiscount(user, order) {
const rules = DISCOUNT_RULES[user.level] ?? DISCOUNT_RULES.normal;
const matched = rules.find((rule) => order.amount >= rule.min);
return matched ? matched.rate : 0;
}为什么更好:新增一个会员等级只需加一条配置,不需要改动分支逻辑;每条规则都能独立测试;圈复杂度从 11 降到 3,认知负担显著减轻。
代码规范工具体系
工具是代码质量落地的抓手。不同工具各司其职,组合使用才能形成完整闭环。下面先给出职责对比,再逐个展开配置。
| 工具 | 核心职责 | 关注点 | 是否修改代码 |
| --- | --- | --- | --- |
| ESLint | 代码质量与潜在错误检查 | 逻辑、语法、最佳实践 | 部分可自动修复 |
| Prettier | 代码格式化 | 缩进、引号、换行、分号 | 是 |
| Stylelint | 样式代码检查 | CSS/SCSS 规范与错误 | 部分可自动修复 |
| Commitlint | 提交信息规范校验 | commit message 格式 | 否 |
| TypeScript | 类型检查 | 类型安全、契约 | 否 |
| EditorConfig | 跨编辑器基础风格统一 | 换行符、缩进、编码 | 编辑器行为 |
| SonarQube | 静态分析与质量门禁 | 复杂度、重复率、坏味道、安全 | 否 |
一个常见的误区是把 ESLint 和 Prettier 当成竞争关系。实际上它们是互补的:ESLint 关注"代码对不对"(catch bugs),Prettier 关注"代码好不好看"(formatting)。让 Prettier 负责格式化,ESLint 关闭所有格式相关规则,两者井水不犯河水,才是现代前端的标准做法。
ESLint:代码质量与错误检查
ESLint 是 JavaScript/TypeScript 生态最核心的静态检查工具,通过可插拔的规则和插件,在编码阶段发现潜在问题。
##### 传统配置(.eslintrc)
对于仍在使用旧版配置格式的项目,一份典型的 TypeScript + React 配置如下:
{
"root": true,
"env": {
"browser": true,
"es2022": true,
"node": true
},
"parser": "@typescript-eslint/parser",
"parserOptions": {
"ecmaVersion": "latest",
"sourceType": "module",
"ecmaFeatures": { "jsx": true },
"project": "./tsconfig.json"
},
"plugins": ["@typescript-eslint", "react", "react-hooks", "import"],
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/recommended",
"plugin:react/recommended",
"plugin:react-hooks/recommended",
"plugin:import/recommended",
"plugin:import/typescript",
"prettier"
],
"settings": {
"react": { "version": "detect" }
},
"rules": {
"no-console": ["warn", { "allow": ["warn", "error"] }],
"no-debugger": "error",
"no-unused-vars": "off",
"@typescript-eslint/no-unused-vars": [
"error",
{ "argsIgnorePattern": "^_", "varsIgnorePattern": "^_" }
],
"@typescript-eslint/no-explicit-any": "warn",
"@typescript-eslint/explicit-function-return-type": "off",
"complexity": ["warn", 10],
"max-lines-per-function": ["warn", { "max": 50, "skipComments": true }],
"max-depth": ["warn", 4],
"react/react-in-jsx-scope": "off",
"react-hooks/rules-of-hooks": "error",
"react-hooks/exhaustive-deps": "warn",
"import/order": [
"error",
{
"groups": ["builtin", "external", "internal", "parent", "sibling", "index"],
"newlines-between": "always",
"alphabetize": { "order": "asc", "caseInsensitive": true }
}
]
}
}配套的 `.eslintignore` 用于跳过无需检查的产物目录:
node_modules
dist
build
coverage
*.min.js
public
next-env.d.ts##### 扁平配置(Flat Config,eslint.config.js)
ESLint 9 起默认采用扁平配置。它用一个数组表达配置层叠,比传统 `extends` 更直观、更易组合:
// eslint.config.js
import js from '@eslint/js';
import tseslint from 'typescript-eslint';
import react from 'eslint-plugin-react';
import reactHooks from 'eslint-plugin-react-hooks';
import prettier from 'eslint-config-prettier';
export default [
js.configs.recommended,
...tseslint.configs.recommended,
{
files: ['**/*.{ts,tsx}'],
plugins: {
react,
'react-hooks': reactHooks,
},
languageOptions: {
parserOptions: {
ecmaFeatures: { jsx: true },
project: './tsconfig.json',
},
},
settings: {
react: { version: 'detect' },
},
rules: {
'no-console': ['warn', { allow: ['warn', 'error'] }],
'@typescript-eslint/no-explicit-any': 'warn',
'@typescript-eslint/no-unused-vars': [
'error',
{ argsIgnorePattern: '^_' },
],
complexity: ['warn', 10],
'react-hooks/rules-of-hooks': 'error',
'react-hooks/exhaustive-deps': 'warn',
},
},
{
ignores: ['dist/**', 'build/**', 'coverage/**', '**/*.min.js'],
},
prettier,
];##### ESLint 能拦下什么样的真实 bug
ESLint 不只是"风格警察",它能拦截真正会导致线上事故的问题。例如 React 的 `exhaustive-deps` 规则:
// ❌ ESLint 会警告:缺少依赖 userId,导致闭包捕获旧值
function UserProfile({ userId }) {
const [data, setData] = useState(null);
useEffect(() => {
fetchUser(userId).then(setData);
}, []); // 依赖数组为空,userId 变化时不会重新请求
return <div>{data?.name}</div>;
}
// ✅ 补全依赖后,userId 变化会正确触发重新请求
function UserProfile({ userId }) {
const [data, setData] = useState(null);
useEffect(() => {
fetchUser(userId).then(setData);
}, [userId]);
return <div>{data?.name}</div>;
}常见坑:为了消除警告而随手加 `// eslint-disable-next-line`,等于把安全气囊拆掉。正确做法是理解规则背后的原因,用 `useCallback` / `useMemo` 稳定引用,而不是禁用规则。
Prettier:统一代码格式
Prettier 是"意见强硬"(opinionated)的格式化工具,它几乎不提供风格选择,从而彻底终结团队内的格式争论。
{
"printWidth": 100,
"tabWidth": 2,
"useTabs": false,
"semi": true,
"singleQuote": true,
"quoteProps": "as-needed",
"jsxSingleQuote": false,
"trailingComma": "all",
"bracketSpacing": true,
"arrowParens": "always",
"endOfLine": "lf"
}为什么让 Prettier 接管格式:格式差异会污染 Git diff,让代码审查者在一堆空格改动里找不到真正的逻辑变更。统一格式后,diff 只反映有意义的修改。
要让 Prettier 和 ESLint 和平共处,需要安装 `eslint-config-prettier` 并放在 extends 数组的最后,它会关闭所有与格式相关的 ESLint 规则,避免两者互相打架。
Stylelint:样式规范检查
Stylelint 之于 CSS/SCSS,等价于 ESLint 之于 JS。它能发现无效属性、重复选择器、颜色格式不一致等问题。
// stylelint.config.js
export default {
extends: [
'stylelint-config-standard',
'stylelint-config-standard-scss',
'stylelint-config-recess-order',
],
plugins: ['stylelint-order'],
rules: {
'color-hex-length': 'short',
'color-no-invalid-hex': true,
'declaration-block-no-duplicate-properties': true,
'no-duplicate-selectors': true,
'selector-max-id': 0,
'selector-class-pattern': '^[a-z][a-zA-Z0-9]+$',
'unit-allowed-list': ['px', 'rem', 'em', '%', 'vh', 'vw', 's', 'ms', 'deg', 'fr'],
'max-nesting-depth': 3,
},
};坏味道示例与修复:
/* ❌ 无效颜色、重复属性、过深嵌套 */
.card {
color: #ff;
color: red;
.header .title .icon .badge { font-size: 12px; }
}
/* ✅ 合法颜色、无重复、扁平结构 */
.card {
color: #f00;
}
.card__badge {
font-size: 12px;
}Commitlint:提交信息规范
统一的提交信息不仅让 git log 可读,还能自动生成 changelog、自动推断版本号(结合 semantic-release)。业界事实标准是 Conventional Commits。
// commitlint.config.js
export default {
extends: ['@commitlint/config-conventional'],
rules: {
'type-enum': [
2,
'always',
['feat', 'fix', 'docs', 'style', 'refactor', 'perf', 'test', 'build', 'ci', 'chore', 'revert'],
],
'subject-max-length': [2, 'always', 72],
'subject-empty': [2, 'never'],
'type-empty': [2, 'never'],
},
};提交格式为 `type(scope): subject`,各类型含义如下:
| type | 用途 | 是否触发版本变更(semver) |
| --- | --- | --- |
| feat | 新功能 | minor |
| fix | 修复 bug | patch |
| perf | 性能优化 | patch |
| refactor | 重构(不改变外部行为) | 无 |
| docs | 文档 | 无 |
| style | 格式(不影响逻辑) | 无 |
| test | 测试 | 无 |
| build | 构建系统或依赖 | 无 |
| ci | CI 配置 | 无 |
| chore | 杂项 | 无 |
规范与不规范的对比:
❌ 不规范
update
fix bug
改了点东西
修复登录问题以及顺便调整了首页样式还加了个埋点
✅ 规范
feat(auth): 支持手机号验证码登录
fix(cart): 修复优惠券叠加计算错误
perf(list): 虚拟滚动优化长列表首屏渲染TypeScript 类型约束
类型系统是最强大、最早期的静态检查手段。它在编译期就能拦截大量运行时才会暴露的错误。可以把类型理解为"编译器帮你写的、永远不会过期的文档和断言"。
##### 严格模式配置
开启严格模式是发挥 TypeScript 价值的前提。推荐的 `tsconfig.json` 关键选项:
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"strict": true,
"noImplicitAny": true,
"strictNullChecks": true,
"strictFunctionTypes": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedIndexedAccess": true,
"exactOptionalPropertyTypes": true,
"forceConsistentCasingInFileNames": true,
"skipLibCheck": true
}
}##### 类型如何拦下缺陷
`strictNullChecks` 能在编译期阻止最常见的运行时错误之一——读取 undefined 的属性:
interface User {
id: string;
profile?: { avatar: string };
}
function getAvatar(user: User): string {
// ❌ 未开启严格模式时能编译,运行时可能抛 "Cannot read properties of undefined"
// return user.profile.avatar;
// ✅ 严格模式强制处理可空分支
return user.profile?.avatar ?? '/default-avatar.png';
}用可辨识联合(discriminated union)建模状态机,让"非法状态无法被表达":
// 请求状态用联合类型精确建模,避免 isLoading + error 同时为真的非法组合
type RequestState<T> =
| { status: 'idle' }
| { status: 'loading' }
| { status: 'success'; data: T }
| { status: 'error'; message: string };
function render(state: RequestState<string[]>): string {
switch (state.status) {
case 'idle':
return '等待中';
case 'loading':
return '加载中...';
case 'success':
return state.data.join(', '); // 此分支才能访问 data,类型安全
case 'error':
return state.message;
}
}常见坑:滥用 `any` 会让类型检查形同虚设,它像病毒一样会污染整条调用链。团队应通过 `@typescript-eslint/no-explicit-any` 把 `any` 标为警告,优先使用 `unknown` 并配合类型收窄。
EditorConfig:跨编辑器的基础统一
不同成员使用 VS Code、WebStorm、Vim 等不同编辑器,缩进和换行符很容易不一致。EditorConfig 用一个跨编辑器都识别的文件统一最底层的格式约定:
# .editorconfig
root = true
[*]
charset = utf-8
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
indent_style = space
indent_size = 2
[*.md]
trim_trailing_whitespace = false
[Makefile]
indent_style = tab它和 Prettier 有职责重叠,但层级更低、更基础:EditorConfig 在"输入时"影响编辑器行为,Prettier 在"保存/提交时"重新格式化。两者配合使用。
Git 钩子:husky + lint-staged
再完善的规范,如果依赖人自觉执行,就一定会有人忘记。Git 钩子的作用是把检查变成"强制发生"的自动动作。husky 负责管理钩子,lint-staged 负责只检查本次暂存的文件(而不是全量扫描,速度快很多)。
##### 安装与初始化
npm install -D husky lint-staged @commitlint/cli @commitlint/config-conventional
npx husky init##### pre-commit 钩子
# .husky/pre-commit
npx lint-staged##### commit-msg 钩子
# .husky/commit-msg
npx --no-install commitlint --edit "${1}"##### lint-staged 配置
{
"lint-staged": {
"*.{js,jsx,ts,tsx}": [
"eslint --fix",
"prettier --write"
],
"*.{css,scss,less}": [
"stylelint --fix",
"prettier --write"
],
"*.{json,md,yml,yaml}": [
"prettier --write"
]
}
}##### package.json 完整 scripts 示例
{
"scripts": {
"lint": "eslint . --ext .js,.jsx,.ts,.tsx",
"lint:fix": "eslint . --ext .js,.jsx,.ts,.tsx --fix",
"lint:style": "stylelint \"src/**/*.{css,scss}\"",
"format": "prettier --write \"src/**/*.{js,jsx,ts,tsx,css,scss,json,md}\"",
"format:check": "prettier --check \"src/**/*.{js,jsx,ts,tsx,css,scss,json,md}\"",
"typecheck": "tsc --noEmit",
"test": "jest --coverage",
"quality": "npm run typecheck && npm run lint && npm run format:check && npm run test",
"prepare": "husky"
}
}为什么用 lint-staged 而非全量检查:一个中大型项目全量 lint 可能耗时数十秒到几分钟,会严重拖慢提交体验,导致开发者绕过钩子(`git commit --no-verify`)。只检查暂存文件通常能在 1-3 秒内完成,兼顾了强制性和体验。
常见坑:钩子里做太重的操作(比如跑全量测试),会让每次提交都很痛苦。建议 pre-commit 只做快速的 lint + format,把耗时的完整测试放到 pre-push 或 CI 阶段。
SonarQube 静态分析
ESLint 关注单文件级别的规则,SonarQube 则从项目全局视角做深度分析:坏味道、重复代码、认知复杂度、安全漏洞、以及"质量门禁(Quality Gate)"。它常被用作团队级的代码健康度仪表盘。
# sonar-project.properties
sonar.projectKey=my-frontend-app
sonar.projectName=My Frontend App
sonar.sources=src
sonar.tests=src
sonar.test.inclusions=**/*.test.ts,**/*.test.tsx,**/*.spec.ts
sonar.exclusions=**/node_modules/**,**/dist/**,**/*.d.ts
sonar.javascript.lcov.reportPaths=coverage/lcov.info
sonar.typescript.tsconfigPath=tsconfig.json
sonar.qualitygate.wait=trueSonarQube 的核心概念是"质量门禁":对新增代码设定必须满足的阈值,不满足则阻断合并。一个典型的门禁标准:
| 门禁条件(针对新增代码) | 阈值 |
| --- | --- |
| 新代码覆盖率 | ≥ 80% |
| 新代码重复率 | ≤ 3% |
| 可维护性评级 | A |
| 可靠性评级 | A |
| 安全性评级 | A |
| 阻断级 / 严重级问题数 | 0 |
为什么盯"新增代码"而非"整体":老项目存量债务巨大,要求整体达标不现实,容易让团队直接放弃。而"新代码质量门禁"(Clean as You Code 理念)保证债务不再增长,存量则逐步偿还,是更务实的落地策略。
命名、函数与错误处理规范
工具能拦下机械性问题,但"好代码"的很多特质需要靠人写出来。这一节给出几条最能提升可读性的编码规范。
##### 命名:让名字自解释
// ❌ 含义不明的命名
const d = new Date();
const list = users.filter((u) => u.a > 18);
function calc(x, y) { return x * 0.9 + y; }
// ✅ 语义化命名,代码即文档
const currentDate = new Date();
const adultUsers = users.filter((user) => user.age > 18);
function calcPriceWithDiscount(basePrice, shippingFee) {
return basePrice * 0.9 + shippingFee;
}命名约定速查:
| 场景 | 约定 | 示例 |
| --- | --- | --- |
| 变量/函数 | camelCase | userName, fetchList |
| 常量 | UPPER_SNAKE_CASE | MAX_RETRY_COUNT |
| 类/组件/类型 | PascalCase | UserProfile, OrderItem |
| 布尔值 | is/has/can 前缀 | isLoading, hasPermission |
| 事件处理 | handle/on 前缀 | handleClick, onSubmit |
| 私有成员 | 下划线或 # 前缀 | _cache, #internal |
##### 函数:短小、单一职责
一个函数应该只做一件事。下面这个函数混杂了校验、计算、格式化、副作用,很难测试和复用:
// ❌ 一个函数承担了太多职责
function processOrder(order) {
if (!order.items || order.items.length === 0) {
console.log('订单为空');
return null;
}
let total = 0;
for (const item of order.items) {
total += item.price * item.quantity;
}
if (total > 1000) total *= 0.9;
const formatted = '¥' + total.toFixed(2);
saveToDatabase(order, total);
sendEmail(order.userEmail, formatted);
return formatted;
}拆分为职责单一的小函数后,每一步都可独立测试与复用:
// ✅ 职责拆分,纯函数与副作用分离
function validateOrder(order) {
return Array.isArray(order.items) && order.items.length > 0;
}
function calcTotal(items) {
return items.reduce((sum, item) => sum + item.price * item.quantity, 0);
}
function applyBulkDiscount(total) {
return total > 1000 ? total * 0.9 : total;
}
function formatPrice(amount) {
return `¥${amount.toFixed(2)}`;
}
async function processOrder(order) {
if (!validateOrder(order)) {
throw new EmptyOrderError('订单不能为空');
}
const total = applyBulkDiscount(calcTotal(order.items));
await saveToDatabase(order, total);
await sendEmail(order.userEmail, formatPrice(total));
return formatPrice(total);
}注意上面模板字符串里出现的 `formatPrice` 用到了 JS 模板插值,这类计算与展示的分离让核心逻辑(`calcTotal`、`applyBulkDiscount`)成为无副作用的纯函数,测试时无需 mock 数据库和邮件服务。
##### 错误处理:不要吞掉异常
// ❌ 静默吞掉错误,问题被隐藏,排查无从下手
async function loadConfig() {
try {
return await fetch('/api/config').then((r) => r.json());
} catch (e) {
return {}; // 出错了却当作正常,返回空对象
}
}
// ✅ 分类处理、保留上下文、向上传播
class ConfigLoadError extends Error {
constructor(message, cause) {
super(message);
this.name = 'ConfigLoadError';
this.cause = cause;
}
}
async function loadConfig() {
try {
const res = await fetch('/api/config');
if (!res.ok) {
throw new ConfigLoadError(`配置接口返回 ${res.status}`);
}
return await res.json();
} catch (err) {
// 记录日志并上报监控,再抛出让调用方决定降级策略
logger.error('加载配置失败', err);
throw new ConfigLoadError('无法加载应用配置', err);
}
}错误处理最佳实践:区分"可恢复"与"不可恢复"错误;保留原始错误的 `cause`;不要用 `catch` 吞掉后返回空值伪装成功;在边界处(如全局兜底、路由层)统一上报。
代码审查流程与要点
代码审查(Code Review)是质量保障中"人"的一环,工具无法替代它对设计、可读性、业务正确性的判断。
##### 审查流程
开发者提交代码 → 创建 Pull Request
↓
CI 自动检查(lint / typecheck / test / SonarQube 门禁)
↓
自动检查通过 → 指派 Reviewer 人工审查
↓
Reviewer 评论 / 提问 → 开发者回应或修改
↓
审查通过(Approve)→ 合并(Squash / Merge)→ 触发部署关键原则是:人工审查发生在自动检查之后。让机器先把格式、lint、类型这些机械问题挡掉,人才能把宝贵注意力集中在逻辑、设计和边界上。
##### 审查要点清单
| 维度 | 审查问题 |
| --- | --- |
| 正确性 | 逻辑是否符合需求?边界条件是否处理? |
| 可读性 | 命名是否清晰?是否需要注释解释意图? |
| 设计 | 抽象是否合理?是否有更简单的实现? |
| 复用 | 是否重复造轮子?能否复用已有工具函数? |
| 性能 | 是否有明显的性能问题(如循环内请求)? |
| 安全 | 是否有 XSS / 注入 / 越权风险?敏感信息是否泄露? |
| 测试 | 关键逻辑是否有测试覆盖? |
| 兼容 | 是否影响现有功能?是否有破坏性变更? |
##### 审查的沟通礼仪
CI 集成质量门禁
把所有检查接入 CI,是让规范真正"长牙齿"的最后一步。任何绕过本地钩子的提交,都会在 CI 这道防线被拦下。
# .github/workflows/quality.yml
name: Code Quality
on:
pull_request:
branches: [main, develop]
push:
branches: [main]
jobs:
quality:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: 20
cache: npm
- name: Install dependencies
run: npm ci
- name: Type check
run: npm run typecheck
- name: Lint
run: npm run lint
- name: Format check
run: npm run format:check
- name: Test with coverage
run: npm run test -- --coverage
- name: Upload coverage
uses: actions/upload-artifact@v4
with:
name: coverage-report
path: coverage/
- name: SonarQube Scan
uses: sonarsource/sonarqube-scan-action@v3
env:
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
SONAR_HOST_URL: ${{ secrets.SONAR_HOST_URL }}配合分支保护规则(Branch Protection Rules),可以强制要求:所有 CI 检查通过 + 至少 1 位 Reviewer 批准,才允许合并到 `main`。这样质量门禁就不再是"建议",而是"硬约束"。
真实案例分析
##### 案例一:某电商团队引入 lint + CI 门禁
一个约 20 人的电商前端团队,早期没有统一规范,代码审查大量时间耗在讨论格式,线上因低级错误(空指针、未处理的 Promise rejection)导致的缺陷频发。团队分三步引入质量体系:先落地 ESLint + Prettier + husky,再接入 CI 门禁,最后引入 SonarQube 新代码门禁。半年后的对比数据:
| 指标 | 引入前 | 引入后 | 变化 |
| --- | --- | --- | --- |
| 线上缺陷率(每千行/月) | 2.1 | 0.6 | 下降约 71% |
| 单次代码审查平均耗时 | 45 分钟 | 18 分钟 | 下降约 60% |
| 因格式问题产生的评论占比 | 40% | 3% | 大幅下降 |
| 新成员上手周期 | 3 周 | 1.5 周 | 缩短约 50% |
| CI 拦截的缺陷(每月) | 0 | 约 35 | 前移拦截 |
其中审查耗时下降的主因,是 Prettier 消除了格式争论,审查者得以专注逻辑。而线上缺陷率下降,则主要归功于 TypeScript 严格模式 + ESLint 拦下了大量空值与异步错误。
##### 案例二:大型项目复杂度治理
某中后台系统随迭代累积,核心模块出现多个圈复杂度超过 30 的"巨型函数",改动频繁引发回归。团队用 SonarQube 定位复杂度热点,制定"新代码复杂度 < 10"的门禁,并对存量 Top 20 复杂函数做专项重构。一个季度后,平均圈复杂度从 14 降到 7,回归缺陷减少约 55%,相关模块的改动评审时间下降约 40%。
##### 业界规范参考
大型公司都有成熟的公开规范,可作为团队制定自身规范的起点:
| 来源 | 特点 |
| --- | --- |
| Airbnb JavaScript Style Guide | 最流行的社区规范,规则详尽、有解释 |
| Google JavaScript Style Guide | 偏保守、强调一致性 |
| 阿里巴巴前端规范 | 中文友好,贴近国内团队实践 |
| TypeScript ESLint 官方推荐集 | TS 项目的规则基线 |
落地建议:不要照搬任何一份规范,而应基于团队现状裁剪,逐步收紧。一开始就把所有规则设成 error,只会让团队陷入"红海"而放弃;更好的做法是先 warn 后 error,渐进式提升门禁。
常见坑总结
| 坑 | 后果 | 正确做法 |
| --- | --- | --- |
| ESLint 和 Prettier 规则冲突 | 保存和 lint 互相打架 | 用 eslint-config-prettier 关闭格式规则 |
| 用 disable 注释绕过警告 | 隐患被掩盖 | 理解规则原因并真正修复 |
| pre-commit 跑全量测试 | 提交极慢,被 --no-verify 绕过 | 只在钩子里做 lint-staged,测试放 CI |
| 滥用 any | 类型系统失效 | 用 unknown + 类型收窄 |
| 一次性把规则全设 error | 团队抵触、放弃 | 先 warn 再逐步 error |
| 只检查存量代码 | 老项目无法达标而放弃 | 盯新增代码门禁(Clean as You Code) |
| 只靠本地钩子 | --no-verify 可绕过 | CI 门禁 + 分支保护双保险 |
总结
代码质量与规范的本质,是把"依赖个人自觉和记忆"的软约束,转化为"工具自动执行、门禁强制拦截"的硬约束,从而让整个团队的代码基线稳定、可预期。落地路径通常是:先统一格式(Prettier + EditorConfig),再引入静态检查(ESLint + Stylelint + TypeScript),接着用 Git 钩子(husky + lint-staged)把检查前移到提交,最后用 CI 门禁 + SonarQube + 代码审查形成完整闭环。
最后用一张表串起本文涉及的各环节,帮助建立整体心智模型:
| 环节 | 代表工具 | 拦截阶段 | 解决的核心问题 | 落地优先级 |
| --- | --- | --- | --- | --- |
| 格式统一 | Prettier / EditorConfig | 编码/保存 | 风格不一致、diff 噪音 | 高(最易见效) |
| 静态检查 | ESLint / Stylelint | 编码/保存 | 潜在错误、坏习惯 | 高 |
| 类型约束 | TypeScript | 编译 | 类型错误、空值 | 高 |
| 提交规范 | Commitlint | 提交 | commit 混乱、无法自动化 | 中 |
| 本地门禁 | husky + lint-staged | 提交/推送 | 忘记检查 | 中 |
| 深度分析 | SonarQube | CI | 复杂度、重复、债务 | 中 |
| 流水线门禁 | GitHub Actions 等 | CI | 绕过本地检查 | 高 |
| 人工审查 | Pull Request | 合并前 | 设计、逻辑、可读性 | 高 |
需要牢记的一句话:质量门禁不是为了限制开发者,而是为了让开发者能够长期、快速、有信心地迭代。 好的规范体系,最终会以"更少的线上事故、更短的审查时间、更快的新人上手"回报团队。