资讯动态

从lint到产物体积:构建不可绕过的代码质量门禁

发布时间:2026/10/15 8:59:34 来源:尧图企业网站定制
有一段时间我一听到代码质量这四个字就头大。不是不重视而是每次都在重复同样的事情评审前提醒自己跑一遍 lint合并前叮嘱同事补个测试发布前祈祷构建别炸。直到有一次一个小改动因为漏跑类型检查直接把测试环境的部署搞挂了。那天我坐在工位上盯着红屏脑子里的想法只有一个与其靠人提醒不如把检查做成一道绕不过去的门。这个想法后来变成了一个内部工具我给它取名impeccable英文里无可挑剔的意思。它不是什么惊天动地的大项目就是一个把 lint、格式、类型、单测、构建、产物体积全部串起来的 CLI 检查器跑一条命令给一份报告不达标就直接阻断。这篇文章想跟你聊聊这个工具的完整设计思路、实现细节以及我在真实项目里踩过的那些坑。1. 一切源于一次让我社死的 code review那次线上事故排查到最后根因是一个很简单的类型错误某个接口字段从字符串改成了数字但调用方还在拿它做字符串拼接。TypeScript 在编译期就能拦住的问题偏偏所有人都在以为别人检查过了的心理下放了过去。事后复盘大家都很尴尬因为 lint 规则在、测试框架在、构建流程也在但没有任何一把统一的尺子在提交之前告诉每个人你现在这笔改动不合格。1.1 那次事故不是偶然回顾那段时间的工作流你会发现一个典型困境开发机上的工具链和 CI 上的工具链并不是同一套。本地我用 VSCode 的 ESLint 插件看到波浪线就当检查过了CI 上跑了 lint但只针对增量文件测试覆盖率没有硬性指标低于阈值也能合并构建产物超过预算体积也没有警告机制。大家各自守着各自那一段流程切面太多反而没人对整体质量负责。更麻烦的是不同人判断合格的标准不一样。有人觉得警告可以忽略有人把警告一律当错误有人从来没打开过 lint 输出面板。你没法靠嘴去统一这些标准能统一标准的只有代码。所以我当时确定了一个原则所有质量检查必须由同一套命令、同一份配置、同一个阈值来执行不管在本地还是在 CI结果必须完全一致。1.2 我想要的不是再认真一点而是可执行的门禁再认真一点这种话没有任何工程价值。真正有价值的是设计一个所有人都无法绕过的门禁。这门禁要有三个特点确定性同样的代码在任何机器上跑出来的结果都一样不能依赖机器上装的插件版本。可解释性失败了必须告诉你是哪一步挂的、哪个文件、哪个规则、怎么改。可阻断性挂在 CI 上失败就不准合并挂在本地失败就阻止提交。这三点听起来简单但真正落地时会碰到大量细节问题。比如确定性要求你锁定所有工具的版本可解释性要求你统一报告格式可阻断性要求你能拿到所有步骤的退出码并动态决定整体是否通过。当时的流行方案不足以覆盖这些需求拆开拼装又太零散所以我决定自己写一个编排器把大家已经用得很熟的 ESLint、Prettier、tsc、Jest、构建命令整合到一条流水线里。这就是 impeccable 最初的形态。2. 取名 impeccable一个质量检查 CLI 的架构取舍起这个名字多少带点自我调侃。代码不可能真正无可挑剔但工具的目标是让每个可自动检查的维度都没有挑剔空间。也就是说人只负责写业务逻辑机器负责把所有能量化的问题挡在外面。确定目标后架构上我做了一个非常明确的取舍不追逐大而全的平台只做一个薄薄的胶水层。2.1 为什么不用现成的质量平台当时团队里有人提议用 SonarQube也有人建议把 CodeClimate 接进 GitHub。这些工具确实很强大但我最后放弃了。原因不复杂太重部署 SonarQube 要数据库、要 Java 运行时、要单独维护服务对一个几十人的前端团队来说是额外的基础设施负担。规则库与本项目语言契合度不够团队是 TypeScript React 技术栈但 SonarQube 里大量规则是通用语言分析的对前端工具链的特殊机制比如 JSX、CSS Modules、Vite 的按需编译理解不深容易产生噪音。反馈链路太长平台上给出的修复建议很难直接转成本地编辑器里的具体操作我想让开发者在提交前就看到同样的失败信息而不是等几秒后 CI 才告诉你。当然这些平台有它们的场景只是对我的团队而言最有效的方案是把大家都熟悉的命令行工具按顺序编排起来。这样开发者不用学新东西只看一份清晰的检查报告就能直接在本地修复。2.2 目录结构与核心模块项目结构大概长这样impeccable/ ├── bin/ │ └── impeccable.js # CLI 入口 ├── src/ │ ├── cli.js # 命令行参数解析 │ ├── runner.js # 步骤调度器 │ ├── steps/ │ │ ├── lint.js # ESLint 封装 │ │ ├── format.js # Prettier 检查 │ │ ├── types.js # tsc --noEmit │ │ ├── test.js # Jest 封装 │ │ ├── build.js # 构建封装 │ │ └── size.js # 产物体积检查 │ ├── reporter.js # 报告聚合 │ └── gate.js # 阻断决策 ├── templates/ │ └── report.markdown.ejs # Markdown 报告模板 ├── config/ │ └── impeccable.example.js └── package.json每个步骤都是一个独立函数输入是统一的上下文对象输出是标准结构的结果{ step: lint, status: passed | failed | skipped, durationMs: 1234, warnings: 5, errors: 2, detail: /path/to/report.json }这种统一结构让调度器变得非常简单依次执行每个步骤任何一个返回 failed最终门禁就 failed。但顺序不是随意排的lint 和格式检查最快放最前面可以秒级失败类型检查和单测次之构建和体积检查最贵放最后前面挂掉就不白跑后面。2.3 一个小巧但够用的调度模型调度模型没有用任何工作流引擎就是最朴素的for...of循环加Promise.all的组合。大部分步骤用串行因为构建依赖前面静态检查通过避免浪费时间但 lint、格式、类型检查这三个只读步骤可以并行。实测下来三者在本地各需要 3-8 秒并行能将这一步从十几秒压缩到八秒内。代码核心部分也不复杂类似这样const steps [ parallel([lint, format, types]), test, build, size ]; for (const stepOrGroup of steps) { const results Array.isArray(stepOrGroup) ? await Promise.all(stepOrGroup.map(runStep)) : [await runStep(stepOrGroup)]; for (const result of results) { if (result.status failed) { gate.fail(result); break; } } }这里有个非常关键的细节一旦某个阶段的步骤失败我不再继续执行后面的昂贵步骤。比如 lint 直接报错就不跑 Jest 和构建了。很多人在写 CI 脚本时习惯用连接命令效果一样但缺点是你拿不到结构化的进度报告也无法知道每一步的耗时和日志位置。用调度器把每一步包一层之后后续做超时控制、重试、跳过才方便。3. 七步自检链路从 git 提交到产物体积对比impeccable 的默认流程分成七个阶段每一段都解决一个具体问题。下面把这七步逐一说清楚包括最终选定的参数和为什么这么选。3.1 第一步环境与基线准备事情开始之前先确定两件事当前代码处于什么状态本轮检查有没有可用基线具体来说我会读取环境变量里的IMMACULATE_MODE值可能是local、ci或pre-commit。不同模式下后续步骤的行为不同local全量检查适合合并前主动跑一次。pre-commit只检查本次暂存区里的文件追求速度。ci全量检查并且把报告输出到artifacts/impeccable/目录供 CI 归档。基线信息也很关键。对于产物体积检查我会去读上次构建生成的dist/.impeccable-baseline.json里面记录了上个版本各产物 gzip 后的大小。如果文件不存在则当前构建结果只记录不阻断。这一步避免了新接入工具的第一天所有人都在失败的尴尬。3.2 第二步到第四步lint、格式、类型检查的串并联设计第二步是 ESLint。配置上我不建议在工具里强行覆盖项目规则而是直接读取项目根目录的.eslintrc这样 impeccable 只是执行器规则所有权仍然归各项目。唯一强制项是{ maxWarnings: 0 }这是最重要的一行配置。警告不处理就会成为背景噪音慢慢变得没人看。宁可在刚开始接入时把数量压到零也不要留着几十个警告当作历史债务。第三步是 Prettier。注意这里用prettier --check .而不是prettier --write .。--write会在检查的同时修改文件而 impeccable 的定位是判断是否合格不是格式化工具。如果开发者在本地被提示有格式问题应该自己执行npm run format而不是让检查工具偷偷改文件。否则在 pre-commit 模式里会出现一个更尴尬的局面你暂存了 A 版本工具改成了 B 版本然后提交的其实是未暂存的 B 版本很容易造成混乱。第四步是类型检查。命令是tsc --noEmit -p tsconfig.json。这里必须强调一个常见的坑不要在构建脚本里用tsc vite build做类型检查。因为tsc一旦启用了outDir配置就真的会输出文件容易污染到源码目录。更好的做法是单独维护一个tsconfig.typecheck.json用noEmit覆盖然后 impeccable 指向这份配置。这样类型检查和构建就完全解耦构建交给 Vite 或 Webpack 自己处理。{ extends: ./tsconfig.json, compilerOptions: { noEmit: true } }3.3 第五步到第六步测试覆盖率与构建产物校验第五步是 Jest。除了跑测试用例还需要输出覆盖率报告并在配置里设置硬性阈值。通常在package.json的 jest 配置里加上{ coverageThreshold: { global: { branches: 80, functions: 80, lines: 80, statements: 80 } } }这里要特别注意覆盖率阈值不能拍脑袋定。80% 是一个相对合理的起步值但如果项目之前完全没有覆盖率基础建议分阶段设定。我在另一个项目里就是这么做的第一个月只要求 60%并且只统计新增文件等团队写测试的习惯养成后再逐步拉高到 80%。如果一开始就定 90%只会让大家为了凑数字写一堆不疼不痒的单元测试反而失去意义。第六步是构建。这一步没什么神奇之处就是调用项目的npm run build但有个细节值得提构建产物不能直接覆盖正式输出目录。我会在命令里拼一个临时目录比如vite build --outDir .impeccable-dist检查完再清理。这样即使构建过程产生意外也不会破坏你已经部署好的静态文件。3.4 第七步报告聚合与阻断决策最后一步做两件事对比产物体积同时把前面所有步骤的结果汇总成一份报告。产物体积对比的规则很简单读取dist下所有 js/css 文件压缩成 gzip 后记录大小与基线文件对比。我定的默认阈值是单个文件超过基线 5% 就警告超过 15% 直接失败。这里用 gzip 后体积而不是原始体积是因为线上服务器几乎都开了 gzip 压缩它对用户感知更有参考价值。首屏性能真正关心的是传输体积不是磁盘体积。报告格式我同时输出三种JSON 方便机器读取Markdown 方便贴在 PR 描述里HTML 方便可视化查看。并提供断言语义信息比如lint: 未通过发现 2 个错误test: 未通过覆盖率 72%低于阈值 80%size: 未通过index.js gzip 体积比基线增加 19.2%这些文案是给人看的不是给机器看的。一份报告如果不能让开发者一眼找到问题文件那它就算不上合格的门禁。为了让这一点成立我在每个步骤里都保留了原始工具的输出文件链接比如 ESLint 的 json 输出、Jest 的 json-summary这样点开就能看到具体位置。4. 速度与噪音把完美变得可维护工具做得再漂亮如果每次跑要三分钟任何人都会想办法绕过它。快和准必须同时满足才能让团队成员愿意主动使用。这一节聊聊我在提速和降噪上做的具体事情以及实测数据。4.1 增量策略的巧妙骗局本地开发时的检查完全没必要全量跑一遍。我的处理方式是把检查范围缩小到 git 暂存区类似 lint-staged 的思路。但单纯只检查暂存区文件会有个漏洞如果一个文件引用了另一个被改动但未提交的文件类型检查时可能通过因为当前工作区里的内容已经变了而暂存区还是旧版本。这就会造成本地过了CI 挂了的怪现象。为了平衡速度和准确我做了个妥协类型检查始终跑全量因为tsc本身没有精确到单个文件检查的模式它必须基于整个项目图来判断类型lint 和格式则只跑暂存区利用 ESLint 的 cache 加快二次检查。这样做的结果是一次 pre-commit 检查本地耗时通常控制在 10 秒左右其中大半时间花在 tsc 上而它可以接受。还有个技巧是用eslint --cache。第一次跑完后会在.eslintcache里记录每个文件的哈希值第二次开始只检查变动的文件。这个文件记得加进.gitignore避免每个开发者在本地生成不同的缓存造成无谓的合并冲突。4.2 规则分级与误报处理任何一个检查工具的长期维护难点都是误报。误报太多大家就会对报告失去信任最后干脆不看。impeccable 的规则分级策略是ESLint 里只保留两类规则——error会阻断warn只记录。默认配置里如果项目历史上有太多 error我不会一次性开启而是把它们降到 warn跑一段时间积累数据再逐步转成 error。另一个常被团队吐槽的规则是no-nested-ternary。这个规则禁止嵌套三元表达式出发点是代码可读性但某些场景下嵌套三元写起来比 if 分支更清晰。我的处理方式是在配置文件里增加 overrides排除测试文件同时允许开发者用eslint-disable-next-line注释来标明这里故意这么写。重点在于允许例外但必须显式声明。不显式声明的一律默认进 error。Jest 覆盖率也有类似问题。比如一些幂等函数、边界工具函数分支特别多测试起来收益低。impeccable 会读取项目的coveragePathIgnorePatterns配置但我会在接入时专门审查一次确保被 ignore 掉的文件不是核心逻辑。否则覆盖率数字会变得很好看但实际保护力接近于零。4.3 性能实录一次全量检查到底有多慢我拿一个中等规模的项目做了实测项目大概是 120 个 TypeScript 文件、800 个测试用例、纯前端构建。机器是 MacBook Pro M1 Pro 16GB。结果如下阶段冷缓存耗时热缓存耗时lint format tsc 并行约 11 秒约 3 秒jest 全量测试 覆盖率约 24 秒约 12 秒vite build 到临时目录约 18 秒约 15 秒产物体积对比与报告约 1 秒约 1 秒总计约 54 秒约 31 秒冷缓存就是刚 clone 完仓库、还没有任何缓存的情况热缓存是日常开发、跑过一次后有缓存的情况。CI 上速度会稍慢大约一分半钟完成一次全量检查。这个时间完全可以接受毕竟它是一个合并前的强制门禁不是每个 commit 都全量跑。但 pre-commit 模式不能跑全量否则会被骂死。所以最终规则就是提交时跑增量合并前跑全量CI 永远跑全量。这条规则后来写进了团队文档效果非常好。5. 从个人脚本到团队协作落地配置与防绕过设计自己写脚本自己用很容易但要推广到整个团队最大的问题不是技术而是信任和习惯。如果工具设计得不透明开发者不知道它为什么失败也不知道怎么豁免很快就会产生对抗情绪。所以配置的灵活性和 CI 侧的强制力要一起考虑。5.1 一份可复用的配置文件impeccable 支持每个项目根目录放一份impeccable.config.js。默认配置不用动项目可以根据自己的情况覆盖单一维度。一个最小示例module.exports { mode: ci, steps: { lint: { enabled: true, maxWarnings: 0, }, format: { enabled: true, }, types: { enabled: true, tsconfigPath: tsconfig.typecheck.json, }, test: { enabled: true, coverageThreshold: { lines: 80, statements: 80, functions: 80, branches: 75, }, }, build: { enabled: true, outDir: .impeccable-dist, }, size: { enabled: true, warnPercent: 5, failPercent: 15, baselinePath: dist/.impeccable-baseline.json, }, }, };这份配置的核心价值在于项目团队拥有最终决定权。工具本身不是规则制定者而是规则执行者。质量策略是 80% 覆盖率还是 90%是团队自己定的工具只负责不让大家忘记这个约定。5.2 CI 侧接入与报告归档CI 侧的接入方式主要是通过一个 artifact 目录来保留证据。以 GitLab CI 为例代码块里的脚本大概是impeccable: stage: test script: - npm ci - npm run impeccable:ci artifacts: paths: - artifacts/impeccable/ expire_in: 30 days when: alwayswhen: always很关键即使检查失败报告文件也要保留下来这样开发者可以在流水线记录里翻到具体问题而不仅仅是看到一个红叉。GitHub 那边我没有单独写集成插件而是用了一个取巧的办法让 impeccable 在 CI 模式下往GITHUB_STEP_SUMMARY环境变量对应的文件里写一段 Markdown 总结。这样 GitHub Actions 会自动把这份内容渲染到 workflow 的 summary 页面不需要额外授权也不涉及第三方插件。如果你用的是 Jenkins那就更简单了直接把 Markdown 报告作为 HTML 发布在 job 页面里即可。5.3 团队推广的节奏与防绕过设计最后聊一个很多人不会明说但普遍关心的话题怎么防止团队成员绕过门禁。我的做法是本地检查永远可绕过CI 检查永远不可绕过。本地你可以不装 hook、不跑脚本我不搞任何强制手段因为开发环境是大家的自由。但合并请求必须通过 CI而 CI 上的 impeccable 是 pipeline 的硬性关卡没有绿色状态就不能合并。这就把管人的负担从离开发者最近的环节挪到了离代码仓库最近的环节反而减少了对抗感。接入节奏上我建议分三步走第一周只做报告不阻断。所有结果只输出到 CI artifacts让大家看看这个工具体验如何有没有误报。第二周开始阻断 error但允许项目经理和 tech lead 在群里申请临时豁免。第三周之后完全阻断所有不通过项必须修复或显式豁免。这里有个容易犯的错一上来就全局强制把历史债务全部暴露在门禁上。相信我第一周就会出现大量与本次改动无关的 lint error开发者会直接愤怒地找你理论。正确做法是新增的历史问题全部进eslint-disable注释但新代码必须达标。也就是说别妄想一次清理干净要保留一条可赊账的路然后逐步收紧。我在这个项目上最大的体会是工具的价值不在于完美的状态而在于让每一次不完美都立刻变得显眼。一个按钮、一条命令、一份报告它逼着每个人在自己最方便修改代码的时间点面对问题而不是等代码进了主干才来来回回地修补。如果你也在维护一套差不多的前端工程化流程我强烈建议你从最小闭环开始先串 lint 和 tsc再加测试最后再上体积对比。每一步都别贪多跑给团队看收到反馈后再迭代。这样出来的工具才会真的有人用也才配得上 impeccable 这个名字。

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

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

免费获取报价 →
↑