资讯动态

构建代码质量门禁:用ESLint与Prettier守护CI的实践指南

发布时间:2026/10/13 10:38:57 来源:尧图企业网站定制
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 正式形成了三条主流程后续所有模块都围绕这三条线展开静态检查Lint内置 ESLint 核心规则集 团队定制的规则覆盖对 JavaScript/TypeScript 代码做语法、风格、潜在 bug 检测。这是整个工具的地基。格式验证Format用 Prettier 作为格式化事实标准验证提交的代码是否完全符合格式要求。这块我特意设计成先校验再格式化而不是自动帮你格式化——原因后面细说。质量评分Score根据检查结果按模块维度计算质量分低于阈值的代码直接阻断合并。这一步是团队从看报告到强制执行的关键转折。这三条线不是独立跑的是有严格顺序的先做 diff 扫描确认改动范围再跑静态检查然后做格式校验最后汇总所有结果计算评分。任何一步失败后续步骤直接短路减少无谓的耗时。3. 技术选型在一些容易忽视的岔路口我为什么这么选3.1 核心引擎为什么坚持用 ESLint 的底层能力而不是重新造轮子impeccable 的第一个也是最重要的技术决策就是不自己写规则解析器而是复用 ESLint 的底层 AST 和规则体系。刚开始我确实动过自己写一套简洁解析器的念头但很快被现实劝退了——JavaScript/TypeScript 的语法复杂度远超想象光是处理新版 ES 语法和装饰器提案就能让人绝望。复用 ESLint配合typescript-eslint解析器意味着我可以把精力全部放在编排、报告、增量扫描这些真正有价值的部分。选型时的几个关键决定Lint 引擎直接调用 ESLintLinter类不走 CLI 进程减少每次启动的 200-300ms 开销。虽然初期麻烦一点但换来了热启动的流畅体验。解析器typescript-eslint/parsertypescript-eslint/eslint-plugin这是目前 TS 静态检查的事实标准社区规则非常丰富足以支撑团队定制。格式化工具Prettier 2.x/3.x用一个prettier.check只读模式做格式校验绝不自动改写文件。代码克隆/重复检测后期加的用了jscpd这在后面处理复制粘贴代码的场景里派上了大用。3.2 为什么不用现成的全家桶方案团队里当时有人建议直接用 Nx 或者 Turborepo 这类 monorepo 工具自带的检查能力也有人建议直接氧化 CIEHUSKY 之类的插件组合。我的理由是第一我们当时的主仓库并不是 monorepo强行引入这类工具反而增加心智负担。第二这些方案的检查结果基本是终端日志没有历史记录、没有评分趋势团队没法回答这周代码质量比上周好了还是坏了这个问题。第三我要的增量 diff 检查和按模块评分在现成方案里都需要二次开发自己做一个反而架构更干净。最终我选择的技术栈是Node.js TypeScript 开发 CLIESLint 底层 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-filterACMR拿到了变更文件列表再结合.impeccableignore排除构建产物和第三方目录。但这块有个大坑只检查变更文件会导致上下文类型错误漏掉。举个例子你只改了一个工具函数formatDate()的入参类型但调用它的五个文件都没动——如果你只 lint 变更文件那五个调用处可能就会炸出类型不匹配的问题可它们根本不在 diff 列表里。我们的解法是两层结构精确扫描层只 lint diff 文件跑语法/风格规则速度快覆盖新代码引入的问题。类型关联层如果变更文件涉及导出函数/组件的签名变化额外触发对引用该模块的其他文件的类型检查。这块用 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 个 warningno-console、prefer-const、max-lines-per-function再点一下就能跳到对应代码行。提示打分规则要跟随团队目标动态调整。初期我们把格式一致权重拉得很高因为当时代码风格确实五花八门等风格问题基本绝迹之后就把这个权重降下来把复杂度和重复代码的权重提了上去。评分规则一定不是写死就完事的每季度都应该根据团队实际痛点做一次微调。4.4 结果报告与 CI 阻断怎么报告才能让人愿意看最后一个环节是报告与集成的设计。很多检查工具死掉的真正原因不是功能不够而是输出太难读。终端刷出几百行密密麻麻的错误列表开发者第一反应就是CTRLC然后骂一句垃圾工具。所以我们做了三件事分级展示终端里默认只显示前 10 个 error 和 5 个 warning 的摘要完整报告写入.impeccable/report.json和report.md。想看完整列表就按--verbose。CI 注释在 GitHub Actions 或 GitLab CI 里通过reviewdog这种工具把错误逐条贴到对应的 PR 代码行上。开发者直接在代码审查界面就能看到这里有个类型错误不用切回终端。趋势曲线报告里附带最近 30 次提交的质量分趋势。这个设计意外地有效——人都有好胜心看到自己负责的模块分数连续三天走低不用催就会主动去修问题。CI 接入就是这么一段简单的配置# .github/workflows/impeccable.yml name: impeccable quality gate on: pull_request: branches: [main] jobs: impeccable: runs-on: ubuntu-latest steps: - uses: actions/checkoutv4 with: fetch-depth: 0 # 必须完整拉取 git 历史diff 扫描需要 - uses: pnpm/action-setupv2 - uses: actions/setup-nodev4 with: node-version: 20 cache: pnpm - run: pnpm install --frozen-lockfile - run: pnpm impeccable --diff-base origin/main --threshold 855. 踩过的坑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 扫描与格式检查之间的交互核心思路是三分法建立基线baseline仓库首次接入 impeccable 时先把当前所有文件的检查结果快照到.impeccable/baseline.json记录每个文件当前有多少 lint 错误、格式问题。增量判定后续每次检查只对 diff 涉及的文件做全量质量评估对未变更文件只要错误数量没有比基线增加就不报错。这个数量和严重程度双维度对比的算法把存量问题对评分的拖累完全隔绝。设置 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执行过程自动探测仓库里的框架类型Vue / React / 纯 TS / Node 库生成对应的.impeccable/config.json和.impeccableignore。自动写入package.json的 scriptsquality: impeccable。自动生成第一个基线快照。如果在 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 日常开发使用方式接入之后日常使用只有几个动作提交本地代码之前跑pnpm quality做一次自查。本地一般 10-30 秒内出结果发现的新问题通常都是几秒钟能修完的小问题。提交后 CI 自动执行同样的检查如果被exit(2)阻断去 PR 页面看评论里的逐行提示改完重新提交即可。到月底看一眼质量趋势图单独拉低分的模块出来复盘。这套流程我们已经连续运行半年我统计过数据接入前平均每次 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 团队反馈里最有价值的三个改进方向半年运行下来团队成员给的建议里我采纳了三个方向问题分级不够细早期只分 error 和 warning现在增加了hint级别——只提示不扣分用于推荐更优写法但不过度打扰。规则覆盖率不足针对 Vue 单文件组件和 styled-components 的自定义规则需求强烈我们陆续扩充了一组业务规则比如禁止在 render 函数里直接写内联新对象这类对性能敏感的场景。缺少自动修复指引现在--fix参数能自动修复格式问题但类型问题不会自动改。我们下一步计划让报告直接关联到具体的 ESLint 文档页让开发者每次报错都能顺便学到规则背后的原因。7.3 后续演进从检查工具到质量平台如果继续往下走我最想做的其实是三件事第一把评分趋势做成一目了然的图表看板让技术负责人一眼看清各模块质量走势第二把 pr 级别的质量门禁升级成支持灰度策略——比如核心模块阈值 90边缘业务 75第三把规则集做成可发布的 npm 包让多个仓库共享同一个团队质量规范而不是每个仓库复制一份 config。我自己在这半年过程中最大的体会是一个代码质量工具的成败七分在流程设计三分在技术实现。规则引擎可以用现成的diff 逻辑可以反复调优但真正难的是让工具的目标和团队的真实处境对齐——不妖魔化存量问题、不放过增量问题、用分数趋势代替一刀切阻断、用可解释的报告代替冰冷的红字。这才是impeccable这个名字背后真正的分量。最后分享一个小技巧如果你也想在公司里推类似的工具千万别一上来就强制所有人接入。先在自己负责的仓库跑两个星期录一段从混乱到规范的对比视频拿真实的数据和时间成本说话等团队主动问了再推广。工具本身确实有用但让人愿意用它永远是更关键的那一步。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑