☰
构建代码质量门禁:用ESLint与Prettier守护CI的实践指南
2026/10/13 10:38:48 网站建设 项目流程

1. 从一次线上事故说起:为什么我要写"impeccable"这个项目

事情得从我们团队一次凌晨两点的线上事故讲起。当时新版本发布后,有个接口在特定参数组合下直接抛了类型错误,用户端看到的是白屏和 500 错误。排查到最后,根因让人哭笑不得——某个同事在改业务代码时,把原本的userId字段误传成了user_id,而因为那次改动绕过了本地的类型检查,加上 CI 里根本没有强制门禁,这个问题一直潜伏到线上才爆发。

那次事故之后,团队花了一整周做复盘,结论就一句话:我们缺的不是"检查工具",而是一条从代码提交到合并全程自动生效的质量门禁。市面上已有的 ESLint、Prettier、TypeScript 类型检查各自为战,公司也没有统一的规范层,于是我做了一个叫impeccable的项目——把所有静态质量检查能力打包成一个命令行工具,用一套配置跑完全部流程,并把结果以评分、报告、CI 阻断的形式呈现出来。

"impeccable"这个名字就是我对这套工具的核心期望:不是"差不多能用",而是让代码在进入主干之前,就已经达到接近无瑕的状态。如果你也在维护一个多人协作的前端/Node.js 仓库,或者正被"代码风格不统一、类型错误反复出现、CI 形同虚设"这些问题困扰,这篇文章就是我完整还原这个项目从设计到落地的全过程,包括各种踩坑记录。它不是一个玩具 Demo,而是一个已经在我们团队跑了半年、每天服务几十次提交的准生产级工具。

2. "impeccable"到底在管什么:功能定位与边界

2.1 先搞清楚需求:我们要的不是"多一个 lint 工具"

动手写代码之前,我先把团队过去三个月的 code review 记录翻了一遍,把出现频率最高的几类问题做了个排序:

问题类型出现频次当前防线
类型错误(TS 报错被忽略)极高本地 IDE 提示,无强制门禁
代码风格不一致(引号、缩进、命名)高Prettier 但未接入 pre-commit
无意义变量、死代码、未使用导入中ESLint 但规则太少
简单逻辑写复杂(圈复杂度超标)中无
API 调用参数错误中无

这里的关键洞察是:真正的问题不是"没有工具",而是工具没有形成一个完整的闭环。本地跑 ESLint 和 CI 里强制跑 ESLint,完全是两回事——前者是自觉,后者是规则。所以我对 impeccable 的定义就一句话:

一条在合并代码前自动执行的"质量安检通道",覆盖静态检查、格式验证、复杂度评估、类型校验,最后输出一份可读的报告和一个分数。

2.2 三大主流程:检查、验证、评分

把需求抽象之后,impeccable 正式形成了三条主流程,后续所有模块都围绕这三条线展开:

  1. 静态检查(Lint):内置 ESLint 核心规则集 + 团队定制的规则覆盖,对 JavaScript/TypeScript 代码做语法、风格、潜在 bug 检测。这是整个工具的地基。
  2. 格式验证(Format):用 Prettier 作为格式化事实标准,验证提交的代码是否完全符合格式要求。这块我特意设计成"先校验再格式化"而不是"自动帮你格式化"——原因后面细说。
  3. 质量评分(Score):根据检查结果按模块维度计算质量分,低于阈值的代码直接阻断合并。这一步是团队从"看报告"到"强制执行"的关键转折。

这三条线不是独立跑的,是有严格顺序的:先做 diff 扫描确认改动范围,再跑静态检查,然后做格式校验,最后汇总所有结果计算评分。任何一步失败,后续步骤直接短路,减少无谓的耗时。

3. 技术选型:在一些容易忽视的岔路口,我为什么这么选

3.1 核心引擎:为什么坚持用 ESLint 的底层能力,而不是重新造轮子

impeccable 的第一个也是最重要的技术决策,就是不自己写规则解析器,而是复用 ESLint 的底层 AST 和规则体系。刚开始我确实动过"自己写一套简洁解析器"的念头,但很快被现实劝退了——JavaScript/TypeScript 的语法复杂度远超想象,光是处理新版 ES 语法和装饰器提案就能让人绝望。复用 ESLint(配合@typescript-eslint解析器)意味着我可以把精力全部放在编排、报告、增量扫描这些真正有价值的部分。

选型时的几个关键决定:

  • Lint 引擎:直接调用 ESLintLinter类,不走 CLI 进程,减少每次启动的 200-300ms 开销。虽然初期麻烦一点,但换来了热启动的流畅体验。
  • 解析器:@typescript-eslint/parser+@typescript-eslint/eslint-plugin,这是目前 TS 静态检查的事实标准,社区规则非常丰富,足以支撑团队定制。
  • 格式化工具:Prettier 2.x/3.x,用一个prettier.check只读模式做格式校验,绝不自动改写文件。
  • 代码克隆/重复检测(后期加的):用了jscpd,这在后面处理"复制粘贴代码"的场景里派上了大用。

3.2 为什么不用现成的全家桶方案

团队里当时有人建议直接用 Nx 或者 Turborepo 这类 monorepo 工具自带的检查能力,也有人建议直接氧化 CIEHUSKY 之类的插件组合。我的理由是:

第一,我们当时的主仓库并不是 monorepo,强行引入这类工具反而增加心智负担。第二,这些方案的"检查结果"基本是终端日志,没有历史记录、没有评分趋势,团队没法回答"这周代码质量比上周好了还是坏了"这个问题。第三,我要的增量 diff 检查和按模块评分,在现成方案里都需要二次开发,自己做一个反而架构更干净。

最终我选择的技术栈是:Node.js + TypeScript 开发 CLI,ESLint 底层 + Prettier 只读校验 + jscpd 查重,通过 pnpm 打包成单一可执行命令impeccable。整个项目大约 4000 行代码,维护成本完全可控。

4. 核心模块实现:质量门禁的四个关键环节

4.1 检查编排器:串行短路,还是并行快跑?

先说第一个核心模块——编排器(Orchestrator)。它负责调度所有下游流程,设计上我定了一个原则:默认串行 + 关键步骤短路,而不是一上来就并行。

为什么?静态检查本身就是 CPU 密集任务,如果并行去跑 lint、format、clone check,在 CI 的 2 核容器里反而会因为资源争抢导致整体变慢。我的经验是:在 CI 环境里,串行跑完整个流程通常比"粗暴并行"只慢 10-15%,但稳定性和日志可读性高很多。更重要的是,一旦 lint 发现 50 个错误,我根本不需要再花时间跑 format 和查重——直接 return 非零退出码,让开发者先修最基础的问题。

// orchestrator.ts 核心流程简化版 export async function runPipeline(options: CliOptions) { const changedFiles = await scanChangedFiles(options.diffBase ?? "origin/main"); if (changedFiles.length === 0) return exit(0, "无变更文件,跳过检查"); // 第一步:静态检查(含类型感知) const lintResult = await runLint(changedFiles, options.configPath); if (lintResult.errorCount > 0) return exit(1, formatLintError(lintResult)); // 第二步:格式验证(只读模式) const formatResult = await runFormatCheck(changedFiles); if (formatResult.unformatted.length > 0) return exit(1, formatReport(formatResult)); // 第三步:查重与复杂度检查 const qualityResult = await runQualityChecks(changedFiles); if (qualityResult.blockingIssues.length > 0) return exit(1, formatBlockingIssue(qualityResult)); // 第四步:评分并输出报告 const score = computeScore({ lintResult, formatResult, qualityResult }); await writeReport({ score, ... }); return exit(score >= options.threshold ? 0 : 2, `质量分 ${score}/100`); }

你注意看最后一步:exit(0)表示通过,exit(1)表示有硬性错误必须修,exit(2)表示有质量问题但非阻塞——这个三等分的设计很快被证明是必要的。最开始的版本只有通过/失败两态,结果团队频繁出现"为了合代码而强行绕过检查"的情况。引入exit(2)之后,"质量分低于阈值但允许合并(需人工确认)"给了团队一个缓冲,反而让大家更愿意正视问题了。

4.2 增量 diff 扫描:为什么不能每次都全量检查

第二个关键模块是 diff 扫描。初期版本我跑的就是全量检查——一个几千文件的大仓库,每次检查要花 4-5 分钟,几乎没人愿意等。后来对比了一下数据,一次普通的提交通常只涉及 5-20 个文件,全量检查里 95% 的时间都花在了没改过的文件上。

于是我用git diff --name-only配合--diff-filter=ACMR拿到了变更文件列表,再结合.impeccableignore排除构建产物和第三方目录。但这块有个大坑:只检查变更文件会导致"上下文类型错误"漏掉。举个例子,你只改了一个工具函数formatDate()的入参类型,但调用它的五个文件都没动——如果你只 lint 变更文件,那五个调用处可能就会炸出类型不匹配的问题,可它们根本不在 diff 列表里。

我们的解法是两层结构:

  1. 精确扫描层:只 lint diff 文件,跑语法/风格规则,速度快,覆盖"新代码引入的问题"。
  2. 类型关联层:如果变更文件涉及导出函数/组件的签名变化,额外触发对"引用该模块的其他文件"的类型检查。这块用 TypeScript 的project references能力做了个保守实现:先解析出变更文件的所有直接引用方,再对这些文件单独跑一次tsc --noEmit,扫到类型错误才真正减少漏报。

这份增量逻辑最终把平均检查时长从 4 分 20 秒压到了 38 秒,团队接受度直接提升了一个档次。

4.3 质量评分算法:分数不能拍脑袋,要可解释

第三个关键模块是评分系统,这也是"impeccable"最出彩的地方。评分绝不是"问题少就分高"这么简单,我定的是一个加权矩阵:

  • 基础分:100 分起步。
  • 错误扣分:每个 error 扣 5 分(上限 40 分);每个 warning 扣 1 分(上限 15 分)。
  • 格式问题扣分:每个格式不一致文件扣 3 分(上限 15 分)。
  • 复杂度扣分:圈复杂度超过 15 的函数,每个扣 4 分(上限 12 分);超过 20 直接记为一个 blocking issue。
  • 重复代码扣分:重复率超过 5% 的部分每 1% 扣 2 分(上限 10 分)。

这个设计的核心原则是:让每一个扣分都能回溯到具体文件、具体规则、具体行号。报告里不仅给总分数,还会按模块给出分数明细,比如src/utils/date.ts: 78 分,展开后能看到扣分原因列表。假如一个模块的分数是 78,开发者想知道为什么,点进去就能看到"3 个 warning:no-console、prefer-const、max-lines-per-function",再点一下就能跳到对应代码行。

提示:打分规则要跟随团队目标动态调整。初期我们把"格式一致"权重拉得很高,因为当时代码风格确实五花八门;等风格问题基本绝迹之后,就把这个权重降下来,把"复杂度"和"重复代码"的权重提了上去。评分规则一定不是写死就完事的,每季度都应该根据团队实际痛点做一次微调。

4.4 结果报告与 CI 阻断:怎么报告才能让人愿意看

最后一个环节是报告与集成的设计。很多检查工具死掉的真正原因不是功能不够,而是输出太难读。终端刷出几百行密密麻麻的错误列表,开发者第一反应就是CTRL+C然后骂一句"垃圾工具"。

所以我们做了三件事:

  1. 分级展示:终端里默认只显示前 10 个 error 和 5 个 warning 的摘要,完整报告写入.impeccable/report.json和report.md。想看完整列表就按--verbose。
  2. CI 注释:在 GitHub Actions 或 GitLab CI 里,通过reviewdog这种工具把错误逐条贴到对应的 PR 代码行上。开发者直接在代码审查界面就能看到"这里有个类型错误",不用切回终端。
  3. 趋势曲线:报告里附带最近 30 次提交的质量分趋势。这个设计意外地有效——人都有好胜心,看到自己负责的模块分数连续三天走低,不用催就会主动去修问题。

CI 接入就是这么一段简单的配置:

# .github/workflows/impeccable.yml name: impeccable quality gate on: pull_request: branches: [main] jobs: impeccable: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 with: fetch-depth: 0 # 必须完整拉取 git 历史,diff 扫描需要 - uses: pnpm/action-setup@v2 - uses: actions/setup-node@v4 with: node-version: 20 cache: pnpm - run: pnpm install --frozen-lockfile - run: pnpm impeccable --diff-base origin/main --threshold 85

5. 踩过的坑:diff 扫描的"误报雪崩"与完整排查链路

这个部分我想花大篇幅单独讲,因为这是整个项目开发过程中最折磨人的一段经历,也是最能体现"工具落地远比想象中复杂"的地方。

5.1 现象:突然冒出来的几百个"格式错误"

impeccable 上线第三周,团队开始批量接入新仓库。那天下午前端负责人找我,语气有点冲:"你那个工具是不是有 bug?我一行代码没改,就改了 README,它给我报 400 多个格式错误,全是prettier/prettier规则。"

我第一反应是不可能。我的逻辑是:diff 扫描只 lint 变更文件,README 根本不在 lint 列表里。但按他给的仓库试了一遍,居然完美复现——大量报错文件确实不在 diff 列表里。

5.2 第一步排查:先确认 diff 列表本身是否正常

我先把scanChangedFiles的输出打印出来,发现文件列表没问题,确实只有README.md和一个.gitlab-ci.yml。那问题就出在 lint 环节——它为什么会对全量文件做检查?

看了一下代码,突然意识到一个低级错误:在runLint函数里,我虽然接收了changedFiles参数,但 ESLint 的lintFiles被调用时传入的是options.include模式匹配。当时的配置写的是["src/**/*.{ts,tsx,js,jsx}"],这导致 ESLint 在拿到文件列表后,又按 include 模式重新展开了一遍索引,把整个src目录的文件全拉进来了,diff 过滤完全失效。

5.3 第二步排查:为什么报错内容这么整齐划一

修复了 include 逻辑之后(改成直接将文件数组传给lintText/lintFiles),又出现了第二个更隐蔽的问题:报错文件虽然只剩变更文件了,但几百个"格式错误"依然存在——因为新仓库第一次接入 impeccable 时,历史上积累的代码本来就完全没有按 Prettier 格式写过。

这个"存量格式债务"是任何 lint 工具落地时最经典的坑。直接跑格式校验,新手仓库瞬间几百个错误,评分直接跌到个位数,开发者根本不知道从何改起。

5.4 根治方案:分三步走,把"存量债务"和"增量管控"分开

这次踩坑之后,我彻底重构了 diff 扫描与格式检查之间的交互,核心思路是三分法:

  1. 建立基线(baseline):仓库首次接入 impeccable 时,先把当前所有文件的检查结果快照到.impeccable/baseline.json,记录每个文件当前有多少 lint 错误、格式问题。
  2. 增量判定:后续每次检查,只对 diff 涉及的文件做全量质量评估;对未变更文件,只要错误数量没有比基线增加,就不报错。这个"数量和严重程度双维度对比"的算法,把存量问题对评分的拖累完全隔绝。
  3. 设置 grace period(宽限期):对新接入的仓库自动开启 30 天宽限期,宽限期内评分只展示不阻断。但一旦过了宽限期,新引入的问题立刻恢复阻断能力。

这套方案上线后,新仓库接入的体验完全变了。开发者在第一次跑的时候看到的不是几百条红字,而是"基线已建立,当前存量问题 327 个,将在 30 天后纳入评分",配合一份"存量问题清单"逐步清理。团队里不再有人把我们当"找茬工具",反而是每周例会会主动看基线清单的清理进度。

5.5 另外两个不是 bug 但同样坑的细节

除了 diff 扫描的大坑,还有两个小问题我必须提醒你:

  • .gitignore与 diff 扫描的权限问题:在 CI 里如果你只 checkout 了 PR 分支,fetch-depth: 0没设置的话,git diff origin/main会直接失败甚至张冠李戴。这个问题我们线上踩过一次,后来在所有 CI 示例和文档里都强制标注了fetch-depth: 0。
  • 跨平台换行符(CRLF/LF):团队有 Windows 和 macOS 混用,Git 的core.autocrlf设置不同会导致"文件没改但也报格式错误"。我们最后在 impeccable 内部对源码做 lint 前先执行一次git config core.autocrlf input的校验,并且把 Prettier 的endOfLine显式固定为"lf",才算彻底解决。

6. 从零到一接入一个新仓库:完整的实操流程

6.1 初始化与配置文件

有了前面的架构设计,接入一个新仓库就变成了一件很轻松的事。impeccable 提供了一条init命令,它会自动完成下面这些事情:

npx impeccable init

执行过程:

  1. 自动探测仓库里的框架类型(Vue / React / 纯 TS / Node 库),生成对应的.impeccable/config.json和.impeccableignore。
  2. 自动写入package.json的 scripts("quality": "impeccable")。
  3. 自动生成第一个基线快照。
  4. 如果在 Git 仓库里,自动往.git/hooks/pre-push追加一个本地钩子(可选)。

生成的配置文件长这样:

{ "include": ["src/**/*.{ts,tsx,js,jsx,vue}"], "ignore": ["src/generated/**", "dist/**", "coverage/**"], "diffBase": "origin/main", "threshold": 85, "rules": { "complexity-max": 15, "complexity-blocking": 20 }, "format": { "semi": true, "singleQuote": false, "endOfLine": "lf" }, "score": { "errorWeight": 5, "warningWeight": 1, "formatPerFile": 3, "duplicationThreshold": 0.05 }, "gracePeriodDays": 30 }

6.2 日常开发使用方式

接入之后,日常使用只有几个动作:

  1. 提交本地代码之前,跑pnpm quality做一次自查。本地一般 10-30 秒内出结果,发现的新问题通常都是几秒钟能修完的小问题。
  2. 提交后 CI 自动执行同样的检查,如果被exit(2)阻断,去 PR 页面看评论里的逐行提示,改完重新提交即可。
  3. 到月底看一眼质量趋势图,单独拉低分的模块出来复盘。

这套流程我们已经连续运行半年,我统计过数据:接入前平均每次 PR 的 review 往返次数是 4.2 次,接入后降到 1.8 次。虽然不能全归功于 impeccable(团队也有成长),但它的"强制前置检查"确实把大量低级问题挡在了 code review 之前,让 review 讨论的焦点回到了设计层面。

6.3 一条被反复验证过的经验:阈值设置要"跳一跳够得着"

最后这条经验我想单独讲,因为它对工具有效性的影响极大。我们最开始把threshold设成 95,以为"标准高一点,质量就高一点"。结果跑了一周,发现三分之一的 PR 被阻断在 90-94 分这个区间,开发者怨声载道,开始有人绕过检查,更有甚者直接在配置里把 threshold 改成 60 再提交。

后来我换了个策略:先按各模块当前真实分数的中位数 -2 作为初始阈值,保证 80% 的合法提交都能一次通过,剩下 20% 需要微调代码质量。等团队适应之后每两周把阈值抬高 2-3 分。这个方法在行为学上叫"最近发展区",具体到工具上就是:标准要存在感够强,但必须让大多数人在正常努力范围内够得着。太松了形同虚设,太紧了必然被绕过,只有贴着团队真实水平走,才能持续推动大家变好。

7. 实测体验:性能数据、团队反馈与后续演进

7.1 一组真实的性能数据

性能这块我贴一组我们仓库(约 1800 个 TS/TSX 文件)的实测数据:

场景全量检查增量检查(平均 12 个文件)
冷启动(无缓存)4 分 22 秒52 秒
热启动(有缓存)2 分 03 秒21 秒
内存峰值1.4 GB420 MB

这个数据是在 CI 的 2 核 4G 容器里测的。增量扫描的收益一目了然,这也是我为什么极力推荐后来接入的团队把 diff 机制用起来——全量检查的 90 秒如果每天都跑十几次,开发者很快会变得麻木,然后开始无视检查结果。

7.2 团队反馈里最有价值的三个改进方向

半年运行下来,团队成员给的建议里我采纳了三个方向:

  1. 问题分级不够细:早期只分 error 和 warning,现在增加了hint级别——只提示不扣分,用于推荐更优写法但不过度打扰。
  2. 规则覆盖率不足:针对 Vue 单文件组件和 styled-components 的自定义规则需求强烈,我们陆续扩充了一组业务规则,比如"禁止在 render 函数里直接写内联新对象"这类对性能敏感的场景。
  3. 缺少自动修复指引:现在--fix参数能自动修复格式问题,但类型问题不会自动改。我们下一步计划让报告直接关联到具体的 ESLint 文档页,让开发者每次报错都能顺便学到规则背后的原因。

7.3 后续演进:从"检查工具"到"质量平台"

如果继续往下走,我最想做的其实是三件事:第一,把评分趋势做成一目了然的图表看板,让技术负责人一眼看清各模块质量走势;第二,把 pr 级别的质量门禁升级成支持灰度策略——比如核心模块阈值 90,边缘业务 75;第三,把规则集做成可发布的 npm 包,让多个仓库共享同一个团队质量规范,而不是每个仓库复制一份 config。

我自己在这半年过程中最大的体会是:一个代码质量工具的成败,七分在流程设计,三分在技术实现。规则引擎可以用现成的,diff 逻辑可以反复调优,但真正难的,是让工具的目标和团队的真实处境对齐——不妖魔化存量问题、不放过增量问题、用分数趋势代替一刀切阻断、用可解释的报告代替冰冷的红字。这才是"impeccable"这个名字背后真正的分量。

最后分享一个小技巧:如果你也想在公司里推类似的工具,千万别一上来就强制所有人接入。先在自己负责的仓库跑两个星期,录一段"从混乱到规范"的对比视频,拿真实的数据和时间成本说话,等团队主动问了再推广。工具本身确实有用,但让人愿意用它,永远是更关键的那一步。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询