多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

工程化质量门禁实战:把代码评审从人肉纠错变成自动化拦截

工程化质量门禁实战:把代码评审从人肉纠错变成自动化拦截 如果你和我一样早年看到“impeccable”这个词多半会以为又是一个新的前端组件库或者某个代码风格工具。但后来我才意识到真正贴切的名字往往不是用来命名的而是用来描述团队对交付状态的一种执念。我参与维护模拟项目X的时候把仓库里的静态检查、单元测试、覆盖率收集、提交信息规范全部串了起来做成一套只要质量不达标就立即拦住合并请求的自动化门禁项目代号就叫 impeccable。这词的意思是“无可挑剔的”实际上它并不要求每一行代码都精美如诗它要求的是机器能检查的事情绝不留到人工评审阶段才暴露。这篇文章写给两类人一类是正在被“代码评审天天花半天、合并后发现低级问题”折磨的研发负责人另一类是刚接触工程质量自动化、想在团队里从零搭一套可落地门禁的一线开发者。我会把这套门禁内部的三个层次拆开讲清楚再给出一套可以直接抄的配置和命令最后分享我在实际推进过程中踩过的坑和处理的细节。如果你照着做即使不叫 impeccable也能让团队的质量反馈从“几天后的评审意见”变成“提交后十几分钟内的自动化结论”。1. 工程化质量门禁的整体思路与分层设计1.1 一个容易被低估的痛点评审的时间花在了哪里很多团队对代码评审的理解是“人看人”但现实是一个稍微活跃点的仓库每天会有十几个合并请求每个请求动辄改动几十个文件。评审者打开 diff 之后真正能用来判断设计合理性的时间其实很少因为大量注意力都被格式不规范、命名随意、明显可以靠工具查出来的低级问题吃掉了。我在模拟项目X上做过一次统计随机挑了最近两周的二十个合并请求把评审意见分类之后发现大概有四成左右的评论指向的是“可以自动发现”的问题比如未使用的变量、缺少边界判断、文件行数超标、提交信息不符合规范。人的时间和精力是有限的把这些东西消耗在机器能干的活上意味着真正需要人去判断的并发逻辑、数据一致性、接口语义反而没有足够的关注度。impeccable 这个名字看起来很严格但我对它的定位不是给团队增加负担而是替团队把低级负担自动消化掉让人只去看那些机器看不懂的部分。1.2 为什么叫“门禁”而不是“辅助工具”这里有一个很重要的思路转变。很多团队也接入了代码检查工具但工具是“辅助”性质的它在 CI 上跑一遍出了警告看一眼觉得不影响上线就点合并。时间一长工具就形同虚设。impeccable 做的事情完全不同它是门禁是安检。打个比方机场安检不会等你上了飞机再检查你有没有票而是在登机口前拦住你。质量门禁也一样——不是等代码合并进了主干、部署上了环境再去测试而是在合并请求进入主干之前把静态检查、测试覆盖、提交规范这三道关口全部过一遍。任何一道关口不达标就不允许合并。这个“不允许”听起来很硬但恰好是它能够长期生效的原因规则一旦可以被绕过团队的潜意识就会默认“可以不遵守”。我把这套门禁分成了三层。第一层是静态检查层负责代码风格、复杂度、类型安全这些“代码本身长什么样”的事情。第二层是测试层负责单元测试和覆盖率确保改动行为确实被验证过。第三层是流程层负责提交信息格式、分支命名、合并请求描述这些看起来琐碎但能影响追溯效率的事情。再加上托管平台上的 CI 流水线三层检查被打包成一个整体任何一层不过整个合并请求就停在门口。2. 静态检查层怎么配置才不“劝退”团队成员2.1 先做差量检查别急着全量开火静态检查层是整套门禁里最先上线的部分因为它见效最快。最常见的做法是引入 ESLint、Prettier 和 TypeScript 的类型检查分别对应逻辑质量、格式质量和类型安全。但这里大多数人犯的第一个错误就是一把梭直接对整个仓库开启全部规则然后发现存量代码的报错数量比天上的星星还多团队瞬间陷入改不完的历史债项目直接卡住。我在模拟项目X上用的方式是差量检查。所谓差量就是只检查本次改动涉及的代码而不管存量代码是否合规。注意这里有一个非常现实的理由存量问题不是这次改动引入的不应该成为阻碍这次上线的原因真正需要盯住的是“新增代码绝不引入新的问题”。实现上可以用 lint-staged 拦截暂存区的文件也可以在 CI 里用工具对比当前分支和主干分支的差异文件列表。// lint-staged.config.js module.exports { *.{js,ts,vue}: [eslint --fix, prettier --write], *.{json,md,yml}: [prettier --write] };加了--fix之后本地提交时 ESLint 会顺手修复一部分自动可修的问题比如多余空格、单引号换成双引号这类机械操作。这里有个细节不要让 Prettier 去处理逻辑规则也不要让 ESLint 去处理排版规则职责要分开。Prettier 管格式统一输出风格ESLint 管逻辑和潜在问题比如不允许 console 残留、不允许用any、复杂度超过阈值就报错。混在一起的结果是规则互相打架团队改起来很痛苦。2.2 规则分级error、warning、off 三档怎么说静态检查配置里最容易引起争议的就是规则级别。我的经验是在门禁刚上线的头两周不要把所有规则直接拉满成 error更不要把所有规则一刀切为 off。合理的做法是给规则分三个档位。off当前阶段不关注的规则或者和团队现状差距过大、短时间内无法收敛的规则。留着它只会制造噪音。warning希望团队逐步养成的习惯类规则比如代码复杂度上限、函数长度上限。warning 不会阻塞合并但会在报告里显示出来让团队有感知。error红线类规则比如禁止any、禁止使用eval、禁止出现调试日志、未使用的变量直接报错。error 会直接阻塞合并。为什么复杂度这类规则先设成 warning 而不是 error原因非常实际存量代码里那些五百行的大函数不是一天写出来的刚开局就让它们全部爆红团队必然反抗。但你可以把阈值设得比现状略低一点点让新代码写不了那么长同时给存量代码留出重构的时间窗口。{ rules: { complexity: [warn, { max: 12 }], max-lines: [warn, { max: 300 }], typescript-eslint/no-explicit-any: error, no-console: error } }2.3 规则配置文件本身要进评审这里分享一个可能很多人都没想到的经验规则文件本身也要代码评审。最早的时候我直接把一份网上抄来的 ESLint 配置扔进仓库结果团队里最资深的两位开发者因为一条规则吵了两天。后来我们约定任何修改eslint.config.js或lint-staged.config.js的合并请求必须附带两条信息这条规则是为了解决什么问题而加进来的以及如果某个场景合理可以通过什么方式临时豁免。定期做一次规则评审将那些实际误报率极高的规则降级或移除。静态检查层还有一个容易忽略的点类型检查要独立于构建过程。很多项目把tsc放在打包脚本里但打包时如果有类型错误可能只是 log 一行 warning然后继续执行。impeccable 的做法是把tsc --noEmit单独抽成一个 CI 检查步骤只要类型不通过流水线就停住。3. 测试覆盖率的正确打开方式别被数字骗了3.1 测试不是越多越好覆盖率也不是越高越好静态检查只是第一关第二关是测试层。很多团队一提到测试第一反应就是把覆盖率堆到 90% 以上。我承认这个数字听起来很有安全感但它容易把一个团队带进“凑数字”的陷阱。覆盖率实际上是“这次改动有没有被测试执行到”的度量而不是“这次测试有没有验证对结果”的度量。你把责任都寄托在覆盖率这一个数字上团队就会想方设法把那些已经执行过的分支数出来而不是思考关键路径有没有真正被断言。在模拟项目X的门禁里覆盖率阈值是分模块设置的而不是全仓库一个统一数字。全仓库全局的阈值定在 80%但核心模块单独定为 95%。所谓核心模块是指支付回调、状态机转换、权限校验这类一旦出问题就会造成线上事故的代码。非核心模块比如纯粹的数据展示页面覆盖率低一点也不阻塞合并。// jest.config.js module.exports { collectCoverage: true, coverageReporters: [text, lcov, html], collectCoverageFrom: [ src/**/*.{js,ts,vue}, !src/**/*.d.ts, !src/main.ts, !src/router/** ], coverageThreshold: { global: { lines: 80, functions: 80, branches: 75, statements: 80 }, src/core/**/*.js: { lines: 95, functions: 95, branches: 90, statements: 95 } } };这里要注意collectCoverageFrom这个配置决定了哪些文件纳入统计。不参与逻辑的入口文件、路由配置、类型声明最好都从统计范围里排除掉否则你的覆盖率数字会被这些“不需要测的文件”稀释。我看到过一些仓库覆盖率统计里塞了上百个纯类型声明文件数字虚高完全没有参考价值。3.2 新代码覆盖率与全量覆盖率要分开看全局覆盖率有个很大的缺陷存量代码覆盖率已经超了阈值之后新增代码即使一点没测全局数字可能只是从 80% 掉到 79.8%根本不会触发门禁。所以门禁里真正关键的是“新代码覆盖率”。每次合并请求会统计新增行的覆盖情况只有新代码也达到阈值本次改动才算过关。在模拟项目X上我们借助了覆盖率差的自动化工具把主干分支和当前分支的 lcov 报告做比对得到两个指标本次改动新增了多少行代码以及这些新增行里有多少行被执行测试触碰过。这个指标不直接跟随全局阈值而是单独设了 85% 的门槛。低于这个值合并请求被拦下来报告里会清楚地列出哪些新增行没有被覆盖。# 本地生成覆盖率报告 npx jest --coverage --coverageReporterslcov # 将当前分支报告与主干基线比较示意 npx diff-cover coverage/lcov.info --compare-branchorigin/main --fail-under853.3 关键路径要单独加“保险丝”除开覆盖率数字测试层里还有一个更实在的动作为核心用例单独设置失败门禁。这就是我常说的“保险丝”。比如状态机的状态流转、支付回调的重复通知处理、缓存击穿保护这类业务逻辑哪怕覆盖率已经达标了我也要求必须存在对应的测试用例并且这些用例必须跑到、必须断言结果而不是单纯执行了一遍。覆盖率是及格线不是 KPI。这句话我在团队里重复了不止三次。门禁的目的是保证团队不会在无人察觉的情况下引入回归至于测试质量本身应该靠用例评审、靠团队对业务的理解去保证。4. 把检查串成流水线从本地提交到合并请求的完整链路4.1 本地提交前先拦住最愚蠢的错误静态检查和测试配置好了之后接下来要做的就是把它串成一条完整的链路。链路的第一段在开发者本地它不负责发现深层次的问题只负责拦住最愚蠢的问题比如把 console.log 提交进仓库、改了十行代码不小心格式错乱、提交信息写成fix两个字。本地链路通常依赖 Husky 这类 Git Hooks 工具。在 pre-commit 阶段执行 lint-staged在 commit-msg 阶段执行 commitlint 校验提交信息格式。注意不同版本的 Husky 配置方式略有区别老版本在package.json里写 husky 字段新版本推荐用prepare脚本初始化我的建议是团队里锁死一个版本避免配置跟着文档来回换。// commitlint.config.js module.exports { extends: [commitlint/config-conventional], rules: { type-enum: [2, always, [feat, fix, docs, style, refactor, test, chore, perf]], subject-empty: [2, never], type-case: [2, always, lower-case] } };// package.json 中的 husky 配置以 Husky v9 的 .husky 目录结构为例 // .husky/pre-commit npx lint-staged // .husky/commit-msg npx commitlint --edit $1为什么提交信息规范值得做进门禁因为版本发布的时候changelog 是依靠提交信息自动生成的分支溯源也要靠它。如果提交信息乱写后面所有依赖提交历史的自动化工具都会一起遭殃。这一步不需要条条框框特别多够用即可。4.2 CI 流水线的三个阶段与执行策略本地检查之后CI 是真正的最后一道防线。模拟项目X的 CI 流水线分为三个阶段安装依赖、质量检查、构建验证。三个阶段顺序执行任何一个阶段失败流水线整体失败合并请求显示为未通过。# 一个不绑定具体 CI 平台的通用流水线结构示例 stages: - install - check - build install: stage: install script: - npm ci cache: key: npm-cache paths: - node_modules/ lint: stage: check script: - npm run lint:diff - npm run type-check - node scripts/check-diff-coverage.js unit-test: stage: check script: - npm run test -- --coverage artifacts: paths: - coverage/ expire_in: 7 days build: stage: build script: - npm run build dependencies: - install有几个细节值得专门提一下。第一安装阶段要用npm ci而不是npm install因为前者会严格按照锁文件安装依赖保证本地和 CI 的环境依赖完全一致避免“我本地能过、CI 挂掉”的经典问题。第二测试报告和覆盖率报告要作为构建产物归档哪怕是流水线成功也要把报告留存下来方便日后追溯。第三流水线尽量做并行和缓存不要每轮合并请求都重装全部依赖。4.3 失败通知与反馈速度门禁是否好用就看这里门禁做得再严格如果开发者在几个小时后才知道失败了愤怒值会直线上升。所以我个人非常看重反馈速度。让检查在合并请求创建后的最短时间内跑起来一旦失败要在第一时间把报错链接和修复建议直接贴到对应的讨论流里。CI 平台自带的机器人通知功能就能做到关键是不要忽略这个配置步骤。另一个经验是同一个合并请求反复提交代码只测最后一次即可不要每次 push 都排队跑全量。CI 的增量触发和自动取消旧任务功能能解决这个问题。我早年吃过亏一个合并请求连续提交了五次每次 push 都触发一次全量流水线CI 队列积压到需要等二十分钟才开始跑团队直接开骂。后来我们把“自动取消旧任务”打开等待时间从二十分钟降到了三分钟以内。反馈速度快开发者才愿意等门禁才可持续。5. 常见问题排查与避坑实录5.1 本地过关、CI 挂掉的版本指纹问题这是团队接触门禁之后最常遇到的一个问题。开发者在本地跑npm run lint通过了推到远端之后 CI 却报了完全不相关的规则错误。定位下来大多数情况是本地 Node 版本和 CI 上的 Node 版本不一致或者依赖没有锁定导致依赖解析出了不同版本的工具链最终规则判断结果不同。解决办法分两步。第一步统一 Node 版本在仓库里放.nvmrc文件CI 安装依赖前切换版本。第二步安装依赖统一用npm ci锁文件必须提交进仓库。如果还有人问“为什么 lock 文件变更要进评审”那说明他还没有经历过“昨晚还好好的今天 CI 突然挂掉”的绝望。5.2 lint 规则太严如何避免团队集体反抗这一节完全是用一次真实经历换来的。最早期我把 ESLint 的复杂度规则直接设成了 error阈值 10结果团队里一个老项目模块瞬间产生了上百个 error。开发者的第一反应不是去重构而是问我“能不能跳过这个检查”。我没有松口但做了一个很务实的调整把复杂度规则降为 warning同时把“新增代码”的复杂度控制在线级规则里。这样存量代码不受影响新提交的代码一旦复杂度超标会自动被拦下来。另外越是严格的规则越要能讲出道理。团队不是不接受规则而是不接受“说不清楚为什么”的规则。后来我们约定新增一条 error 级规则必须在周会上同步一次并且给出实际案例说明这条规则曾经帮我们拦住过什么样的问题。有了这一步规则的接受度明显提升。5.3 覆盖率达标但核心逻辑裸奔怎么补覆盖率数字达标了线上还是出事故这种经历最能让人冷静下来。回看事故现场通常会发现那个函数确实被测试执行过但测试只跑了一遍正常路径没有覆盖到边界分支或者测试执行了但没有断言最终结果只满足了“代码被执行到”的条件。对策有三个第一把核心模块单独列入白名单单独拉高覆盖率阈值到 95% 以上第二看分支覆盖率而不只看行覆盖率分支覆盖率能体现逻辑分支有没有都被测到第三如果团队有余力可以在核心模块上引入变异测试——简单说就是故意改坏一小段源码再跑测试如果测试没失败说明测试并没有真正保护这段代码。变异测试不可能全量做但用在学校或者核心服务的重点模块上效果非常好。5.4 流水线越来越慢怎么把等待时间打下来门禁上线一个月之后新的抱怨出现了流水线太慢。原本合并请求等个三四分钟后来业务范围扩大单测数量翻倍流水线稳定在十五分钟以上。一个简单的合并请求也要等这么久团队的抵抗情绪又回来了。我做的调整有三个层面。第一差量检查继续做只对改动文件执行 lint 和类型检查不全量跑。第二单测加--onlyChanged之类的相关测试运行模式只跑和本次改动相关的用例核心模块的保险丝用例仍然全量跑保证安全性不被牺牲。第三把安装层、静态检查层、单元测试层拆成并行阶段各自独立跑最终汇集结果。三层减少到一层的等待时间整体反馈速度从十五分钟降到了六分钟左右。如果你在 monorepo 场景下还可以进一步按子项目拆分独立的流水线避免一个子项目出问题阻塞所有项目的合并。5.5 新人总是漏掉门禁失败的信息靠通知模板解决最后一个问题看着很琐碎但实际影响很大。新加入的开发者第一次被门禁拦下来往往不知道该怎么看报告覆盖率报告是一大堆 HTML 文件lint 输出是一长串 javaScript 错误circle 里的 log 要翻半天。我们的做法是把失败的输出封装成固定模板说明是哪一步失败、失败原因关键词、对应的修复命令、相关文档链接。模板固化到 CI 的失败通知里新人在最开始的几周几乎不需要问别人就能自己搞定。6. 最后分享一点个人体会整套 impeccable 门禁上线到现在我最有感触的一件事是自动化并不会自动变好它的价值取决于你有多勤快地去维护它。护栏可以帮你拦住低级错误但最终代码质量的上限还是来自团队对自身标准的认同。对照模拟项目X的数据合并请求的平均反馈时间从两天的人工评审缩短到十五分钟以内的自动化结论主干分支的构建失败率也大幅下降但真正让我觉得值得的不是这些数字而是团队终于有精力在评审里讨论设计、边界和长期演进而不是一遍遍纠正分号。如果你也想做类似的东西我的建议是先挑一个仓库试点不要一开始就搞一套全公司通用的平台。从小仓库把门禁跑顺、规则调稳再逐步复制出去。规则文件是活物它需要每个季度跟团队一起复盘一次把误报多的规则降级把已经形成习惯的规则再变严一点。只要门禁一直在那里它就会默默帮你兜住一万次不小心。
返回列表