上个月我们线上出了一个不算严重但足够丢人的事故:一个看起来人畜无害的配置项改动,因为拼写错误和类型不匹配,在灰度阶段把所有用户的偏好设置全重置了。事故复盘时发现,这个改动是同事在周五下午趁大家不备提的,review的同事草草扫了两眼就点了通过,压根没注意到那行问题。我后来在组里说了一句话:代码审查这种烂事,靠自觉是靠不住的。于是我开始认真琢磨,怎么把代码审查做成一套标准的、开放的、任何人装上就能用的工作流。这就是open-code-review这个项目的由来。
open-code-review本质上不是某个单一工具,而是一套把代码审查规则、自动化检查、CI流程、团队规范全部串起来的开源工作流方案。它能解决的核心问题很简单:让每次代码提交在合并前就被机器和人双重把关,把低级错误、规范问题、设计隐患提前拦在门口,而不是等上线后靠用户来报bug。适合谁用?适合被“review只是走过场”困扰的技术团队,适合想在不增加人力负担的前提下把代码质量量化、流程化的工程效能负责人,也适合个人项目想建立一点质量底线的独立开发者。这套方案不需要一次性全量照搬,可以从最轻量的一两个环节开始落地,跑顺了再逐步加码。
1. 为什么需要一套开放的代码审查工作流
1.1 代码审查被严重低估的价值
很多人觉得代码审查就是找茬、挑刺,甚至觉得它是流程负担,拖慢了发布节奏。但如果你认真跑过一段时间的规范审查,你会发现它真正解决的问题远不止“少几个bug”。
代码审查能拦住那种“当时看着没问题、上线就出事故”的低级错误。人类reviewer的注意力是有限的,尤其在周一早上或周五下午,你不可能指望一个同事用10分钟把1000行diff里的每一行都仔细读一遍。机器不会累,规则不会看走眼,把这一类问题交给自动化,等于给每次提交上了一份底线保险。
代码审查还有一个经常被忽视的作用:知识传递。组里新人写的代码,老手在review时给出修改建议,这是在真实代码上做师徒教学,比任何培训文档都有效。但人肉review天然不稳定——遇到忙的时候、急的时候,审查质量直线下降。用一套开放、统一的工作流,把大家都该遵守的底线固化下来,剩下的人力再去关注逻辑架构这类机器判断不了的层面,效率和质量能同时往上走。
1.2 “开放”这两个字意味着什么
我给这套工作流取名叫open-code-review,强调的就是“开放”。技术选型要开放,全部使用社区成熟的开源方案,不依赖某家商业公司闭源产品;规则配置要开放,每个规则、每个阈值都明文写在仓库里,团队成员随时能看、能讨论、能改;流程要开放,不搞“某某大佬说了算”的黑盒评审,任何人在任何时间都能通过PR评论提出意见。
这套理念实际上借鉴了开源社区运作模式。想想Linux内核或React的代码评审流程:提交的patch要过CI、过静态检查、过reviewer的逐行阅读,所有讨论都公开留痕。开源项目能保持长期高质量,靠的不是几个天才,而是这套“开放的硬约束”。open-code-review就是要把这种模式移植到普通业务团队,不需要每个人都是专家,只要流程是公开透明的,质量问题就藏不住。
1.3 人工审查和自动化检查怎么分工
这是搭建open-code-review时最需要想清楚的问题,也是踩坑最多的地方。我的判断是这样的:能用规则定义清楚的问题交给机器,需要上下文理解、架构判断、产品语义分析的问题交给人。
举个例子,缩进是4个空格还是2个、变量命名用驼峰还是下划线、import语句有没有排序,这类纯格式和规范类问题,机器应该一秒钟搞定,人来看纯粹浪费生命。还有一层是“一眼就能看出但人容易看漏”的问题,比如把一个可选链写成了直接属性访问,在某个边界场景会抛TypeError,这类问题ESLint规则完全可以覆盖。
但机器永远替代不了人做这几种判断:某个函数是不是被拆得太碎或者太臃肿、这个模块的接口设计面对将来的需求变化够不够灵活、用这个第三方库的代价是不是太大、这段业务逻辑的理解和产品预期是否一致。所以合理的分工是:机器做第一道硬性过滤,人做第二道深度review。两道关卡都有明确标准和可见的产出物,审查这件事才算真正闭环。
2. 方案选型:构建open-code-review的四个关键决策
2.1 规则引擎:ESLint作为底座
静态检查规则引擎,我没有犹豫就选了ESLint。原因不复杂:生态最成熟,社区规则包几乎覆盖了JavaScript生态里所有好东西,TypeScript支持通过@typescript-eslint插件也算稳定的第一梯队方案。这几年虽然冒出来Biome、oxlint这类主打性能的新秀,速度确实快得离谱,但规则生态和自定义能力还是差ESLint一大截,在团队内推广时免不了遇到“这条规则我想调一下”的需求,ESLint的配置灵活度是无可替代的。
ESLint的配置这两年也在经历从.eslintrc到eslint.config.js的迁移,新项目建议直接用后者。再说一个我实际对比测试后的结论:ESLint虽然全量扫描大项目时速度一般,但配合lint-staged做增量扫描后,单次执行时间能压到一两秒内,用户体验是完全可以接受的。性能问题根本不用提前焦虑。
2.2 格式与样式:Prettier加Stylelint组合
格式问题我强烈建议直接交给Prettier,它最核心的价值是让团队不再争论“代码该长什么样”。凡是格式争议,一律用prettier来解决,不需要人在review里去点评“这里多了个空格”。
CSS/SCSS/Less这层样式规则用Stylelint来管,它和ESLint的思路很像,规则集可以覆盖属性顺序、颜色值是否用变量、选择器嵌套深度这类问题。如果你项目里用CSS Modules或Tailwind,Stylelint同样能适配。这里有个常见误区:Prettier只管格式,不管代码质量规则。格式统一是Prettier的活,样式规范是Stylelint的活,两者不冲突,也不该互相替代。
2.3 CI管道与机器人:Reviewdog打通评论闭环
工具输出检查结果只是第一步,关键是结果怎么呈现给提交者。把几百行日志丢在CI输出里让人自己翻,和直接在PR代码行上挂一条精确的评论,体验差距是本质性的。我选Reviewdog来做这件事。
Reviewdog是一个能把任意linter输出翻译成代码评审评论的开源工具,支持GitHub、GitLab、Bitbucket等主流平台。它做的事情可以用一句话概括:把ESLint、Stylelint、甚至自定义脚本的输出解析出来,然后在对应的PR代码行上添加评论,标记error级别的问题时同时让检查任务失败,阻止合并。这样审查意见就长在了代码上,提交者打开PR一眼就能看到“这个问题出在这一行、是什么问题、怎么改”,不需要再到处找日志。
2.4 要不要上SonarQube
SonarQube是个好东西,覆盖面广,能检测重复代码、圈复杂度、潜在bug模式,甚至维护一个“技术债”指标。但我给大多数团队的诚恳建议是:初期先不要上,等基础流程跑顺了再考虑。
原因有两个。第一,SonarQube是重组件,跑起来要一个服务端,需要数据库、磁盘存储、持续更新规则库,对于中小团队来说是额外运维负担。第二,规则多不代表效果好,它的海量规则集对老项目来说会产生大量历史告警,处理这堆存量告警本身就是一件劝退新人的事。如果项目对代码质量要求很高、团队有专门的工程效能人力,SonarQube值得研究;否则,先用ESLint加Reviewdog把流程跑起来,效果立竿见影。
下面是我最终采用的工具组合和替代方案汇总:
| 工具 | 职责 | 选型理由 | 替代方案 |
|---|---|---|---|
| ESLint | JavaScript/TypeScript静态规则 | 生态最全、自定义能力强 | Biome、oxlint |
| Prettier | 代码格式化 | 团队零争议格式化方案 | 内置格式化工具 |
| Stylelint | CSS/SCSS样式规范 | 规则精细、适配主流CSS方案 | 无成熟替代 |
| Reviewdog | CI评论机器人 | 打通检查结果与PR评论 | CodeRabbit、GitHub Action bot |
| Husky + lint-staged | 本地提交前检查 | 把问题拦在push之前,节省CI排队时间 | pre-commit框架 |
3. 实操过程与核心环节实现
3.1 本地环境的规则初始化
先说本地这套怎么搭。我建议先用npm初始化项目,然后一次性安装全部依赖:
npm init -y npm install --save-dev eslint @eslint/js typescript typescript-eslint prettier stylelint stylelint-config-standard lint-staged husky装完之后创建eslint.config.js。这里给一份兼容TypeScript项目的基础配置,可以直接抄:
import js from '@eslint/js'; import tseslint from 'typescript-eslint'; export default tseslint.config( js.configs.recommended, ...tseslint.configs.recommended, { files: ['**/*.{ts,tsx}'], rules: { '@typescript-eslint/no-explicit-any': 'error', '@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }] } }, { ignores: ['dist/', 'node_modules/', 'coverage/'] } );这个配置里最关键的一行是no-explicit-any,设置成error后禁止任何地方出现裸any类型。项目里总有人图省事写个any让类型检查闭眼,这条规则必须卡死,不然TypeScript的意义直接腰斩。
接下来把Prettier和Stylelint配置也立起来。Prettier可以在package.json里加一个prettier字段,也可以单独建.prettierrc文件,我习惯单独建文件,便于和ESLint的规则统一审查:
{ "semi": true, "singleQuote": true, "trailingComma": "es5", "printWidth": 100 }Stylelint的配置比较简单,可以直接继承标准集:
{ "extends": "stylelint-config-standard", "rules": { "color-hex-length": "long", "declaration-empty-line-before": null } }3.2 用lint-staged做提交前增量检查
直接全量跑ESLint在团队里不现实,项目一大就是几十秒甚至几分钟,没人愿意每次提交都等。正确做法是用lint-staged,只对暂存区里变动的文件做检查:
在package.json里加这段配置:
{ "lint-staged": { "*.{js,ts,tsx}": ["eslint --fix", "prettier --write"], "*.{css,scss}": ["stylelint --fix", "prettier --write"] } }然后执行这行命令初始化Husky钩子:
npx husky init echo "npx lint-staged" > .husky/pre-commit这样团队每个人在本地提交时,变动的文件都会被自动格式化并做静态检查,如果有error级别的违规,提交会直接被拦截。这一步的意义比CI里的检查更大:越早发现问题,修复成本越低。试想一个错误如果在本地就拦住了,连提交都不会发生,更不会出现在PR里被大家围观。
3.3 把Reviewdog接进CI
本地检查只是第一道关,防止的是“自己看不见自己的问题”。CI里的检查才是所有变更的必经之路,因为它不受个人本地环境的影响,任何绕过本地钩子的操作在里面都会现出原形。
我用GitHub Actions来演示,在项目里创建.github/workflows/code-review.yml:
name: open-code-review on: pull_request: types: [opened, synchronize, reopened] jobs: review: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: 20 - run: npm ci - name: Run ESLint with Reviewdog uses: reviewdog/action-eslint@v1 with: github_token: ${{ secrets.GITHUB_TOKEN }} reporter: github-pr-review fail_on_error: true eslint_flags: 'src/**/*.{ts,tsx}'这里有两个参数值得细说。reporter: github-pr-review表示Reviewdog会把评论直接以review记录的形式挂在PR上,而不是在Actions日志里输出。fail_on_error: true表示只要存在error级别的问题,这个Action就返回失败,配合GitHub分支保护规则,可以做到“有error级别问题就不允许合并”。团队里总有人想钻空子,这个硬门禁直接把路堵死了。
如果你用的是GitLab,流程也类似,用reviewdog/action-eslint的GitLab版本或者调用reviewdog CLI,配置变量的写法不同,但逻辑完全一致。
3.4 规则配置的版本化管理
这是我踩过很多坑之后特别想强调的一环。规则配置绝不能散落在各个人本地,或者只存在于某个CI脚本里,必须作为独立文件纳入git版本控制,最好是放在一个独立的config仓库或项目根目录的.config/目录下,然后通过包管理器或脚本统一引用。
我们团队现在的做法是维护一个叫code-review-config的内部npm包,里面包含eslint配置、stylelint配置、prettier配置,以及对应的依赖版本锁定。各个业务项目通过npm安装这个包,然后在自己项目里re-export或者引用。这样调整规则时只需要改动一个仓库,发一个新版本,各业务项目升级依赖即可生效。
这个做法初期有点费事,但一旦业务项目超过三五个,收益会非常明显。否则你会在每个项目里手工改规则,改到第三个的时候就会彻底崩溃,而且改完之后各项目行为还不一致,所谓“统一标准”就成了一句空话。open-code-review的“open”理念也体现在这里:规则不是某个人拍脑袋定的,而是全团队能审查、能提议修改的开放资产。
4. 常见问题与排查技巧实录
4.1 规则误报和噪声太多,团队直接当成“狼来了”
这是所有引入静态检查的团队遇到最多的第一个问题,没有之一。刚开始配置规则太激进,把几十个rules全开成error,结果就是每个PR几十条评论,开发提一次PR被机器喷得体无完肤,气得直接摆烂不看评论了。
正确做法是分级治理。把规则分成两级:error级别只保留会明显影响功能正确性的规则,比如no-undef、no-unreachable、no-explicit-any;warn级别用来提示代码风格问题,比如prefer-const、no-var。Reviewdog默认对warn只评论不失败,这样开发者在PR里看到warn时会知道“这里可以更好”,但不至于被卡住。
还有一种很有效的技巧:新规则的推进要“渐进式”。不要一次性把所有规则都打开,而是每一两周引入几条新规则,并在PR评论中说明为什么加这条规则、能解决什么具体问题。让团队成员感觉规则的增加是有逻辑的,不是被工具绑架。
4.2 审查速度越来越慢,CI排队时间暴涨
项目跑几个月后,另一个痛点浮出水面:CI上的审查任务越来越慢。原因多半是检查范围失控,ESLint和Stylelint把整个项目的文件全扫了一遍,包括那些几年没动过的老文件。
有两个解法。第一,配合之前提到的lint-staged思想,在CI里也优先做增量检查,只针对本次PR改动的文件跑ESLint,全量检查可以放到定时任务里执行。第二,给ESLint配置合理的ignore文件,把dist、构建产物、第三方目录都排除掉。经过这样调整后,我们组CI的审查时间从平均5分钟降到了40秒左右,效果非常显著。
4.3 老项目历史存量问题太多,新规则一开就告警上千
引入新规则的时候最容易踩的坑:一个运行多年的老项目,规则一开,ESLint瞬间报出上千个问题,一半以上都是历史代码留下的。这时候如果把fail_on_error开成true,等于让整个团队在第一个月什么都别干,天天改历史债,是典型的“好心干坏事”。
这类问题我的处理思路是“新旧有别”。在ESLint配置中通过files字段对旧目录做豁免,或者用overrides把规则只作用在src/new-feature这类新编写目录上,同时建立一条定时任务,每周自动扫描一次历史遗留问题的数量,指标只要求不增长,不要求清零。等技术债被一项项修掉,再逐步收窄豁免范围,而不是一上来就搞一刀切。这样做的好处是团队可以用很平和的心态持续改进,而不是被海量历史告警整到焦虑。
4.4 “工具再强也挡不住人不想看”的团队阻力
技术方案写得再完善,如果团队里有人就是不配合,闭环也撑不起来。有段时间我们组有人每到提PR时就想着绕过CI,把检查失败强行跳过合并。后来我复盘了一下,发现根本原因是他们觉得这套流程是“被管控”,而不是“帮他们兜底”。
破局的关键是转化视角。我们在周会上不评价“谁的代码又没过检查”,而是挑几条典型的Reviewdog评论,当众感谢那位写了差错的同事——因为他的代码代表了团队踩过的坑,改完之后其他人就不会再犯。这种正向反馈很管用。另外,你们可以在CI里加一个“审查记录”展示看板,统计每人每月拦截了多少潜在问题,用数字告诉团队这套流程不是什么监督工具,而是大家的共同防线。
4.5 冷启动项目时的最小落地清单
如果团队或项目是全新的,千万不要一上来就把全套东西铺开。建议按这个顺序小步快跑:
- 先上Prettier + Husky钩子,统一格式,立竿见影。
- 第二周加ESLint基础规则,开启error硬门禁。
- 第三周接入Reviewdog,让检查结果变成PR评论。
- 等到所有人都习惯了上面的流程,再考虑SonarQube、圈复杂度、测试覆盖率这类进阶指标。
这个节奏的核心思路是:每一步带来的摩擦都小于收益,让团队成员感觉到流程在帮他们,而不是烦他们。工具是用来减少摩擦的,不是用来制造恐惧的。
根据我个人这段时间的体会,open-code-review这套工作流真正跑起来之后,团队审查效率肉眼可见地上了一个台阶:原来一天最多能认真review的PR数量有限,现在机器把简单问题全扫光了,人类reviewer只需要专注架构和业务逻辑,讨论质量完全不一样。现在我每次打开PR,看到机器评论加人类讨论的组合,心里会安定很多,因为知道这些代码上线之前,至少被两双以上不同维度的眼睛认真看过了。