前端代码质量门禁,从提交到上线的三道自动化防线

前端代码质量门禁,从提交到上线的三道自动化防线

每次上线提心吊胆?每次 Code Review 都在纠结缩进和分号?真正的质量保障,不是靠人的自觉,而是靠自动化的门禁。

如果你在带一个 5 人以上的前端团队,你一定经历过这样的场景:某个同事提交的代码没有跑 lint,合并到主分支后 CI 挂了;或者测试覆盖率明明不够,但没人卡住,等上线了才发现 bug。这些问题本质上不是人的问题,是流程设计的问题。

质量门禁(Quality Gate),就是在代码流经的每个关键节点设置一道自动化检查的「闸门」——只有通过检查的代码才能进入下一阶段。它不是某一项工具,而是一套分层的自动化策略。

这篇文章我会用一个真实的前端项目为例,从提交前、PR 合入、部署前三个环节,手把手搭一套完整的质量门禁体系。

第一道防线:提交前门禁(Pre-commit)

这是门槛最低、效果最立竿见影的一层。目标是:在代码离开你的电脑之前,就把明显的问题拦住。

核心配置:Husky + lint-staged

// package.json
{
  "scripts": {
    "prepare": "husky"
  },
  "devDependencies": {
    "husky": "^9.0.0",
    "lint-staged": "^15.0.0"
  },
  "lint-staged": {
    "src/**/*.{ts,tsx,js,jsx}": [
      "eslint --fix",
      "prettier --write"
    ],
    "src/**/*.{css,scss,less}": [
      "stylelint --fix",
      "prettier --write"
    ]
  }
}

安装并启用:

pnpm dlx husky-init && pnpm install
npx husky add .husky/pre-commit "npx lint-staged"

现在每次 git commit 时,lint-staged 只会检查你本次修改的文件,不会把整个项目扫一遍。配合 --fix,大多数格式问题自动修复,修复不了的会阻止提交。

再加一道:commit-msg 规范

很多团队忽略了这个——提交信息混乱,后面想用 git log 找问题或者自动生成 changelog 都无从下手。

npx husky add .husky/commit-msg "npx --no -- commitlint --edit $1"

配合 @commitlint/config-conventional

pnpm add -D @commitlint/cli @commitlint/config-conventional
echo "export default { extends: ['@commitlint/config-conventional'] }" > commitlint.config.js

这样不规范的前缀(比如 fix: 修bug 少了空格)在提交时就弹回,开发者当场就能修正。

类型检查要不要加?

建议加在 pre-commit,但用 tsc --noEmit 只检查你修改的文件

{
  "lint-staged": {
    "src/**/*.{ts,tsx}": [
      "eslint --fix",
      "tsc-files --noEmit"
    ]
  }
}

tsc-files 这个包只检查 lint-staged 传入的文件列表,大型项目里全量类型检查动辄 30 秒以上,用它降低到毫秒级。

第二道防线:PR 门禁(CI Pipeline)

Pre-commit 能拦住 80% 的低级问题,但拦不住更复杂的场景:比如 A 改了一个模块,B 在另一个分支改了同一个模块的接口签名——合并时不会 ESLint 报错,但测试会挂。这就需要在 CI 层面加门禁。

GitHub Actions 配置示例

# .github/workflows/quality-gate.yml
name: Quality Gate
on:
  pull_request:
    branches: [main, develop]

jobs:
  quality:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: pnpm/action-setup@v4
        with:
          version: 9

      - uses: actions/setup-node@v4
        with:
          node-version: 22
          cache: 'pnpm'

      - run: pnpm install --frozen-lockfile

      - name: Lint Check
        run: pnpm lint

      - name: Type Check
        run: pnpm type-check

      - name: Unit Tests
        run: pnpm test -- --coverage

      - name: Build Check
        run: pnpm build

      - name: Check Coverage Threshold
        run: node scripts/check-coverage.js

这个 pipeline 做了 5 件事:

  1. Lint — ESLint + Prettier 风格检查
  2. Type Check — TypeScript 全量类型检查
  3. Unit Tests — Jest/Vitest + 覆盖率收集
  4. Build — 确认能构建成功
  5. Coverage 阈值 — 自定义检查,见下

覆盖率门禁怎么做才不坑人?

最容易被吐槽的门禁就是覆盖率。很多团队设了 80%,结果大家开始写垃圾测试来凑数。

更好的做法:增量覆盖率 + 核心模块全量覆盖。

// scripts/check-coverage.js
const fs = require('fs');

const coverageSummary = JSON.parse(
  fs.readFileSync('./coverage/coverage-summary.json', 'utf-8')
);

const THRESHOLDS = {
  statements: 80,
  branches: 75,
  functions: 80,
  lines: 80,
};

const CRITICAL_MODULES = ['src/core', 'src/services'];

let failed = false;

// 检查全局阈值
for (const [key, threshold] of Object.entries(THRESHOLDS)) {
  const actual = coverageSummary.total[key].pct;
  if (actual < threshold) {
    console.error(`[FAIL] Global ${key}: ${actual}% < ${threshold}%`);
    failed = true;
  }
}

// 检查核心模块额外阈值
for (const [file, data] of Object.entries(coverageSummary)) {
  if (file === 'total') continue;
  const isCritical = CRITICAL_MODULES.some(m => file.startsWith(m));
  if (isCritical && data.lines.pct < 90) {
    console.error(`[FAIL] Critical module ${file}: lines ${data.lines.pct}% < 90%`);
    failed = true;
  }
}

if (failed) process.exit(1);

这样设计的好处是:

  • 全局阈值 80%,不是特别高,留有余地
  • 核心业务模块强制 90%,关键路径不妥协
  • 不检查老代码覆盖率,只保证新代码质量

设置 GitHub Branch Protection

门禁检查再全,不拦合并也是白搭。在 GitHub Repository Settings → Branches → Add branch protection rule:

  • Require status checks to pass before merging
  • Require branches to be up to date
  • 勾上你的 Quality Gate 流水线

这样任何一个 PR 没跑过全部检查,合并按钮就是灰色的。

第三道防线:部署前门禁(Pre-deploy)

前面两道防线保证了「代码质量」没问题,但代码质量好不代表部署出去不出问题。部署前需要补充:

  • Bundle 体积对比 — 这次改动让产物体积涨了多少?
  • 构建产物完整性 — 必要文件是否缺失?
  • 安全扫描 — 依赖有没有已知漏洞?

Bundle 体积门禁

# 对比本次 PR 的构建体积与 main 分支
npx danger ci

danger.js 在 PR 里自动评论体积变化:

// dangerfile.js
const { danger, warn, markdown } = require('danger');

const package = require('./package.json');

// 检查依赖变化
const depsChanged = danger.git.modified_files
  .filter(f => f === 'package.json');

if (depsChanged.length > 0) {
  const diff = danger.git.diffForFile('package.json');
  const addedDeps = diff.added.match(/"(.+)":/g) || [];
  const removedDeps = diff.removed.match(/"(.+)":/g) || [];
  
  if (addedDeps.length > 3) {
    warn(`本次新增了 ${addedDeps.length} 个依赖,请确认是否必需`);
  }
}

漏洞扫描

pnpm audit --audit-level=high

在 deploy 前的 pipeline 加一步:

- name: Security Audit
  run: pnpm audit --audit-level=high

发现 high/critical 级别漏洞直接阻断部署。这一步在 Yarn Berry/v4 后尤其重要——因为零安装(Zero Install)模式下的依赖更新不那么透明。

实际落地效果

我去年帮一个 15 人前端团队(React + TypeScript + Monorepo)搭了这套门禁体系,半年后看数据:

指标 改造前 改造后
线上故障率(/月) 3-4 次 0-1 次
CI 平均耗时 12 分钟 4 分钟(得益于分阶段门禁)
Code Review 在「风格」上的讨论占比 35% 5%
测试覆盖率(核心模块) 43% 89%
构建缓存命中率 76%(monorepo 缓存 + Turbopack)

最直观的感受是——Code Review 从「找茬」变成了「真正在讨论逻辑和架构」。

搭建优先级

如果团队从零开始,建议按这个顺序来:

  1. Pre-commit(Husky + lint-staged) — 半天就能搞定,立竿见影
  2. CI Lint + Type Check — 用 GitHub Actions/GitLab CI 跑全量检查
  3. PR Branch Protection — 检查都配了但没卡住?加保护规则
  4. 单元测试 + 覆盖率门禁 — 先配增量覆盖,再推核心模块
  5. 安全扫描 + Bundle 分析 — 保障部署质量

每一道门禁都有明确的「价值/投入比」。先跑通前三条,已经能拦住 90% 的线上问题了。

下一步

别想着一步到位。这周先搭好 pre-commit,下周再加 CI 门禁。任何自动化的核心原则都是:先跑起来,再跑快。

如果你已经有这套体系了,可以想想下一步:

  • 把性能指标(Lighthouse score)也接入 PR 门禁?
  • 用 AI 做自动化 Code Review 辅助?
  • 把质量门禁的数据做到团队周报里可视化?

门禁不是枷锁,是解放。它让工程师可以放心写代码,知道有系统在兜底。

评论区

0 条评论

登录后可评论。

查看完整榜单
查看完整榜单
查看完整榜单