资讯动态

从提交前到CI:打造无可挑剔的前端工程质量防线

发布时间:2026/10/14 7:40:06 来源:尧图企业网站定制
事情是这样的。上个月朋友拉我去看他们组的一个老项目业务逻辑挺完整功能也都在跑可代码一打开就是一股“能跑就行”的味道有人用单引号有人用双引号ESLint 的 warn 堆了两千多条没人清TypeScript 里到处是 anygit 提交信息清一色“fix”和“update”。谁也不敢动那些文件因为一动就可能踩到别人埋的雷。这不是个例。几乎所有团队在从“小作坊”往“正规军”过渡的时候都会撞上这堵墙功能没问题质量没保障。我给自己的一套工程质量基线方案起了一个代号叫 impeccable。它不依赖具体业务框架核心是把 ESLint、Prettier、TypeScript 严格模式、Husky、lint-staged、commitlint 和 CI 流水线整合成一套自动化的质量守门机制目标是让“无可挑剔”从愿望变成可执行的工程约束。这篇文章就是我从零搭建 impeccable 的完整记录里面有方案选型、具体配置、排坑经验也有一段关于“质量检查如何改变团队习惯”的思考。适合正在建设工程规范的前端工程师也适合被存量代码债折磨得想重构又不敢动手的团队参考。1. 为什么做 impeccable一次对“差不多就行”的反击1.1 这个项目到底想解决什么问题代码规范这件事听起来是“审美问题”做起来全是“成本问题”。没有统一规范时每个人打开别人的代码都要先经历一次心智转换这个人喜欢把类型定义放哪这个文件为什么不带分号这里的 any 是故意还是偷懒别小看这些琐碎它们会让 code review 变成无休止的风格争论让重构变成一场赌博让新人入职的第一周全耗在“猜规矩”上。impeccable 想解决三个具体问题第一提交前就把低级问题挡住比如未使用变量、隐式 any、明显格式错误第二让提交信息有迹可循以后翻 git log 能一眼看出每个提交的意图第三让 CI 成为第二道质量守门员就算有人本地跳过了检查合并之前也会被拦下来。核心思路是分层设防本地快速反馈层负责日常开发CI 全量检查层负责兜底两边跑同一套规则不给“漏网之鱼”留空间。这里要强调一个观点自动化工具不是要替代 code review而是要替代 code review 里最无聊的那部分。人眼应该去看“这个接口设计是否合理”“这段逻辑有没有边界漏洞”而不是盯着“你这里多了个空格”。机器能做的检查全部交给机器review 才有时间做真正有价值的事。1.2 方案选型为什么是这六个工具的组合定方案时我对比过不少工具最后敲定的组合是 ESLint Prettier TypeScript strict Husky lint-staged commitlint。每个工具都有明确分工也有明确的替代方案但组合起来最顺。ESLint 是 JS/TS 生态里事实标准的静态检查工具TSLint 早已停止维护没有悬念。Prettier 负责代码格式化它跟 ESLint 的分工是风格问题交给 Prettier逻辑质量问题交给 ESLint。TypeScript 的 strict 模式不是额外插件而是编译器自带的一组严格开关它能把整个类型系统的安全系数拉上来。Husky 用来挂载 git hooks比手动配置.git/hooks更优雅也方便团队共享。lint-staged 只对暂存区的文件做检查避免每次提交全量扫描拖慢速度。commitlint 配合 conventional commits 规范让提交信息格式统一。这几个工具有一个共同特点它们都是“声明式配置”驱动规则写进配置文件后团队所有人共享同一套标准。相比口头约定配置文件是防呆的——新人不需要问“我们项目用什么风格”跑一遍命令就知道哪里不合格。1.3 为什么叫“impeccable”这个词在英文里的意思是“无可挑剔的、无瑕疵的”。给工具链起个名字听上去有点中二但实际好处是团队有了一个共同语言。你说“走一遍 impeccable”大家知道是跑 lint、格式化和类型检查你说“PR 没过 impeccable”大家知道是质量门槛没过。命名让“质量”从一个抽象概念变成了一个具体的、可引用的东西。更重要的是“无可挑剔”这个目标本身就是一种工程态度。代码不是写给机器看的机器只要能跑就行代码是写给下一个维护者看的那个维护者可能是三个月后的你自己。impeccable 这个名字每天都在提醒我质量不是靠某一次大扫除而是靠每一次提交都不过线。2. 核心工具链拆解每一条质量防线都在拦什么2.1 ESLint 9用扁平配置告别历史包袱ESLint 9 之后默认走 flat config也就是eslint.config.js这个配置文件。老版本的.eslintrc那套层级继承、env、overrides的写法还在过渡期但新项目没必要再学旧东西。扁平配置最直观的变化是一个数组搞定所有规则从前到后依次生效心智负担小很多。接入 TypeScript 时我直接用了typescript-eslint这个官方推荐的工具链它内部把 parser、plugin、配置项都封装好了不需要自己手动拼接typescript-eslint/parser和typescript-eslint/eslint-plugin。tseslint.config()这个工厂函数还能合并多套配置代码写起来很干净。规则配置上我默认只开recommended集合里最核心的几条再补充几个团队痛点规则。比如typescript-eslint/no-explicit-any我设成 error就是为了逼团队处理那些any泛滥的问题。实际写代码时any意味着告诉编译器“你别管我了”可程序崩溃时编译器不会替你说“我提醒过你”。换成unknown加类型收窄虽然要多写几行但至少你对自己的数据结构有交代。// eslint.config.js import js from eslint/js; import tseslint from typescript-eslint; import prettier from eslint-config-prettier; import globals from globals; export default tseslint.config( { ignores: [dist, node_modules, coverage], }, js.configs.recommended, ...tseslint.configs.recommended, { files: [**/*.{js,ts,tsx}], languageOptions: { ecmaVersion: 2022, sourceType: module, globals: { ...globals.browser, ...globals.node }, }, rules: { typescript-eslint/no-explicit-any: error, typescript-eslint/no-unused-vars: [error, { argsIgnorePattern: ^_ }], eqeqeq: [error, always], }, }, prettier, );注意最后一行prettier我引入的是eslint-config-prettier它的作用是把 ESLint 里跟格式化相关的规则全部关掉。否则 ESLint 和 Prettier 会在缩进、换行、引号这些事上重复管理冲突只会让你头疼。2.2 Prettier把格式争论从 code review 里彻底踢出去你可以把 Prettier 理解成一个“不讲道理的排版员”它不管你的代码逻辑对不对只管把代码按统一风格重新排好。为什么需要它因为人是不可靠的手写的缩进在十个人的协作下一定会漂移而机器格式化是确定性的同一个配置全世界跑出来都一样。配置 Prettier 时要注意一个心态别花太多时间调参数。printWidth、tabWidth、singleQuote这些选一套顺手的就锁死团队里任何人提出“我想用 120 列”请他用数据证明这个改动带来的收益而不是凭审美投票。// prettier.config.js export default { semi: true, singleQuote: true, trailingComma: all, tabWidth: 2, printWidth: 100, endOfLine: lf, };我这里用了endOfLine: lf主要是避免 Windows 和 macOS 之间因为 CRLF/LF 差异产生大量无意义的 diff。这个问题在多人协作时特别隐蔽两个系统各自格式化后git 会显示整个文件都被改了实际上只是换行符不同。Prettier 还有个大用途历史存量代码可以先全体跑一遍prettier --write瞬间统一风格。这一步看着声势浩大但它是纯格式变更不触碰业务逻辑review 起来只需要确认没有夹带私货。2.3 TypeScript strict让类型系统替你兜底TypeScript 的strict: true一次性打开了一大批严格检查开关包括strictNullChecks、noImplicitAny、strictFunctionTypes、strictBindCallApply等。如果你接触过strict: false的项目再切到true第一反应通常是“怎么到处报错”这很正常因为过去编译器对很多可疑操作都睁一只眼闭一只眼。除了strict我还会额外开几个个人非常推荐的开关noUncheckedIndexedAccess是最容易被低估的一个。它会让数组和对象的索引访问结果带上undefined逼你面对“取不到值”这种现实问题。举个例子// 非严格模式编译器不吭声运行时可能崩溃 const list: string[] []; const first list[0].toUpperCase(); // 开启 noUncheckedIndexedAccess 后编译器直接报错 // list[0] is possibly undefined const first list[0]?.toUpperCase() ?? ;生活化一点普通模式像是“成年人的自觉”编译器默认你不会访问越界严格模式则是把安全绳挂在栏杆上就算你一时手滑它也能拉住你。对生产代码来说我愿意多写几个?.和??也不想在凌晨两点接到“线上白屏”的电话。2.4 Husky lint-staged commitlint把检查绑死在提交前本地检查要真正落地必须把它和 git 操作绑定而不是指望每个人自觉跑一遍脚本。Husky 干的就是这件事它让你能优雅地配置 git hooks。安装并初始化后你会在.husky/目录下看到可执行的 hook 脚本这些脚本会跟随仓库分发给所有人。lint-staged 的价值在于“只检查暂存区”。设想一个中型项目有几千个文件每次提交都全量跑一遍 ESLint 可能要十几秒开发者很快就烦了。lint-staged 会把git add过的文件筛出来只对这些文件执行检查命令提交动作能控制在两三秒内完成反馈足够快人才愿意守规矩。commitlint 则负责规范提交信息。它要求提交信息符合 conventional commits 格式比如feat: 增加登录接口、fix: 修复空指针异常。这条规则看似繁文缛节但当你需要回滚某个功能、生成 changelog、或者用git bisect定位问题时规范的提交信息能省下大量时间。工具不贵习惯才贵。3. 实操从零搭建 impeccable 质量基线3.1 初始化工程与安装依赖先准备一个空目录用 npm 初始化项目。如果你已经有工程直接跳到依赖安装步骤就行。Node 版本建议 20 以上ESLint 9 要求 Node 至少 18.18太老的版本会出现兼容问题。mkdir impeccable-demo cd impeccable-demo npm init -y然后安装核心依赖npm install --save-dev eslint eslint/js typescript typescript-eslint globals npm install --save-dev prettier eslint-config-prettier npm install --save-dev husky lint-staged npm install --save-dev commitlint/cli commitlint/config-conventional这里我特意没有装eslint-plugin-prettier因为现代化的推荐做法是Prettier 单独做格式化ESLint 只做代码质量检查两者通过eslint-config-prettier交接。eslint-plugin-prettier是把 Prettier 当成 ESLint 规则跑效率低而且容易出现“双重格式化”的闹剧。安装完成后在package.json里补几个脚本方便后续统一使用{ scripts: { lint: eslint ., lint:fix: eslint . --fix, format: prettier --write ., format:check: prettier --check ., typecheck: tsc --noEmit } }把命令收敛到统一脚本里很重要。团队协作时所有人跑的都是npm run lint而不是各自记忆一串长长的 npx 命令。3.2 配置 ESLint 与 PrettierESLint 9 的配置文件是eslint.config.js我用的是 ESM 写法所以记得在package.json里加上type: module或者把配置文件改成eslint.config.mjs。之前给的配置可以直接抄但有两个点要展开说。第一globals不能漏。浏览器环境有自己的全局变量比如window、documentNode 环境有process、module。如果不声明ESLint 会把它们当成未定义变量报错。用globals包一次性引入比旧版的env直观。第二ignores里的dist、node_modules必须保留否则 lint 自己生成的产品代码就是浪费性能。如果你还用了 Vite、Next.js 这类框架记得把它们的构建产物目录也加进去。Prettier 配置文件如果也用 ESM 格式prettier.config.js里写export default即可。注意Prettier 默认会读取package.json里的prettier字段或者.prettierrc多个配置源同时存在时会让人迷惑。我的习惯是只保留一个prettier.config.js删掉其他可能的配置来源保证单一事实来源。3.3 接入 TypeScript 严格模式如果你是从零开始建议直接开满严格配置如果是老项目可以分阶段开启这个后面会讲。先看推荐配置{ compilerOptions: { target: ES2022, module: ESNext, moduleResolution: bundler, strict: true, noUncheckedIndexedAccess: true, exactOptionalPropertyTypes: true, noImplicitOverride: true, noFallthroughCasesInSwitch: true, noUnusedLocals: true, noUnusedParameters: true, verbatimModuleSyntax: true, skipLibCheck: true, esModuleInterop: true, forceConsistentCasingInFileNames: true }, include: [src] }重点解释几个容易被忽略的开关。exactOptionalPropertyTypes会严格区分“属性被设置为 undefined”和“属性未定义”这两种情况它能揪出一批“看似可选实则偷偷传 undefined”的调用。verbatimModuleSyntax强制你使用import type导入纯类型这能让构建工具更好地做 tree-shaking同时减少运行时依赖。noUnusedLocals和noUnusedParameters初看有点烦人但它们在帮你保持代码整洁大量死代码就是从这里开始藏污纳垢的。如果担心开发时调试不便可以把它们先设成 warn推进度时再升级为 error。3.4 配置 Husky、lint-staged 和 commitlintHusky v9 的初始化命令很简单npx husky init这条命令会在.husky/目录生成一个pre-commit脚本。我们把它改成# .husky/pre-commit npx lint-staged再创建一个commit-msg钩子用来校验提交信息# .husky/commit-msg npx commitlint --edit $1lint-staged的配置我放在package.json里集中管理{ lint-staged: { *.{js,ts,tsx}: [ prettier --write, eslint --fix ], *.{json,css,md}: [ prettier --write ] } }顺序上我刻意让prettier先跑、eslint后跑。因为 eslint-config-prettier 已经把格式类规则关掉了理论上两者不冲突但先统一格式再检查逻辑心智上更顺畅ESLint 报的错误也更纯粹。commitlint 的配置// commitlint.config.js export default { extends: [commitlint/config-conventional], };这里直接继承约定式提交标准不用自己造轮子。它允许的 type 包括feat、fix、docs、style、refactor、test、chore等覆盖了几乎所有常规提交场景。3.5 流程演示一次完整的提交是一次质量关卡配置完成后整个提交流程是这样的。开发完一个功能git add暂存文件然后执行git commit。此时pre-commit钩子触发lint-staged 只对暂存文件跑 prettier 和 eslint。如果有问题提交会直接失败终端会告诉你哪个文件哪一行违反了什么规则。修复后再 add、再 commit。提交信息起手式如果写成update xxxxxcommit-msg 钩子会立刻拦下提示你 type 必须符合规范。正确写法是git commit -m feat: 增加用户注册接口这套流程的精妙之处在于“阻塞式检查”。很多人担心这样会打断开发节奏但实际体验是早失败永远比晚失败便宜。提交前的检查失败成本只有几秒合并到主分支后才发现问题成本是回滚、修复、重新走发布流程。把质量关卡前置到每一次提交就是把成本控制在最低点。4. 落地过程中的真实问题与排查手册4.1 ESLint 和 Prettier 互相打架这是接入时最常见的问题。症状是格式化完代码后ESLint 依然报错报错内容全是缩进、换行、引号这类格式问题。原因也简单ESLint 内置的indent、quotes、semi等规则跟 Prettier 的格式化结果不一致两边都在管同一件事。解决思路是明确分工格式问题只归 Prettier逻辑问题只归 ESLint。eslint-config-prettier的作用就是把 ESLint 里的格式规则全部关闭。如果你确实想保留一些风格约束也请只选 Prettier 配置里能表达的维度不要两边各设一套。我踩过的坑是早期为了“少装一个依赖”试着只靠 ESLint 的indent和quotes维持格式。结果团队里有人用 eslint --fix 自动格式化有人手写最后仍然一团乱。后来老实接上 Prettier世界安静了。提示如果 lint-staged 里同时跑 prettier --write 和 eslint --fix 后依然报错检查一下 eslint.config.js 里prettier是不是放在了配置数组的最后一层顺序错误会导致规则覆盖失效。4.2 Git hooks 不生效Husky 初始化完成后有时候会发现提交时钩子压根不执行。先查一个东西git 的core.hooksPath。有些全局配置比如某些 dotfiles 管理工具会把 hooks 指向别的地方导致.husky/下的脚本被忽略。git config core.hooksPath如果输出不是.husky就用下面的命令修正git config core.hooksPath .husky另一个容易踩的坑是 Windows 环境。Husky v9 在 Windows 上默认能工作但如果你之前手动改过.git/hooks或者用了旧版 husky可能残留一些冲突文件。最省事的办法是删掉.git/hooks下同名的 hook 文件再跑一次npx husky init。还有一类问题是团队协作里的某个人换了新电脑clone 仓库后node_modules还没装或者忘了跑npm installhooks 自然不存在。所以在项目 README 里写清楚“安装后必须执行npm install npx husky init”这类引导很有必要。4.3 提交信息不规范commitlint 报错的文案一开始会让人摸不着头脑最常见的是subject may not be empty这通常是因为提交信息里没有写好 type 前缀。另一种常见报错是type must be one of [feat, fix, ...]说明你写了个规范之外的 type。为了方便团队查阅我把 commit 规范整理成一张速查表type适用场景feat新功能fix修复 bugdocs文档变更style不影响逻辑的格式调整refactor重构行为不变test测试相关chore构建、依赖、工具链变更记住一个原则提交信息不是写给 git 看的是写给三个月后的自己看的。当你需要定位“登录模块的报错是哪个提交引入的”规范的提交信息能让你用git log --grep快速锁定范围而不用在“update”和“fix”之间大海捞针。4.4 存量代码改造如何把历史债务一次结清老项目接入 impeccable 最怕的是“平地一声雷”全量打开严格规则结果几千条错误刷屏团队直接放弃。我的做法是分三步走。第一步先全量跑一遍prettier --write把格式统一了。这一步风险最小纯格式化不改变行为。第二步ESLint 先以 warn 级别接入不阻断构建让团队在开发时看到警告但可以暂时放行。第三步设定一个债务清零期限比如一个迭代内把 warn 降到 0然后把规则从 warn 升级成 error。这个过程看着慢实际上比一步到位快得多因为团队没有“破罐破摔”的挫败感。针对无法短期内清完的历史文件可以在 eslint.config.js 里用files单独开一个老目录的降级配置比如对src/legacy/**关闭typescript-eslint/no-explicit-any。但这里有一个我强烈建议的约束豁免必须设时限。你可以加一张“技术债清单”每一条豁免都对应一个 issueCI 里再做一道检查——如果 legacy 目录的体积在增长就说明有人又把豁免范围扩大了这时候应该亮红灯。豁免是权宜之计不是长期方案。5. 从工具到文化impeccable 的团队落地心得5.1 质量检查不是审批而是安全网很多人一听到“质量门槛”就觉得是给自己的活找麻烦实际上自动化检查真正改变的是 code review 的气质。我的亲身体验是接入 impeccable 之后review 的评论从“你这里有个 any 忘改了”“为什么用双引号”变成了“这个接口在边界条件下会不会崩溃”“这段逻辑要不要补个单元测试”。机械问题由机器守住之后人脑才有余力关注真正重要的事。质量门槛不是审批流程不是管理者拿来卡人的工具它是一张安全网。它让团队里每个人都有底气说“我来改这个文件”因为你知道规则是公开的、自动的、不会因为谁心情好就放水。5.2 在 CI 里跑同样的检查本地 hooks 再完善也挡不住某些“聪明人”绕过。有人可能用了git commit --no-verify有人可能改动了没有 hook 的机器上的配置。所以 CI 必须做第二道防线而且 CI 里的检查一定要是全量的不能像 lint-staged 那样只查暂存区。用 GitHub Actions 举一个简单例子name: quality on: push: branches: [main] pull_request: jobs: quality: runs-on: ubuntu-latest steps: - uses: actions/checkoutv4 - uses: actions/setup-nodev4 with: node-version: 20 cache: npm - run: npm ci - run: npm run lint - run: npm run typecheck - run: npm run build这套流水线把 lint、类型检查、构建全部串起来任何一步失败PR 都不允许合并。CI 的价值在于“凡是在我机器上能过”的借口彻底失效质量验证变成了仓库级的事实。5.3 后续还能怎么扩展impeccable 是基线不是终点。等团队适应了这套流程后还可以往三个方向扩展测试覆盖率门禁比如全项目覆盖率不低于 80%新增代码覆盖率不低于 90%复杂度检查用 ESLint 的complexity规则限制单个函数的分支数量依赖安全审计npm 生态里就有现成工具可以扫描漏洞依赖。我曾经试过再接入一个死代码检测工具把那些“从来没被 import 过的导出”全部找出来。清理完之后项目体积和心智负担一起降了下去。这些扩展都遵循同一个原则自动化、可量化、不靠自觉。5.4 我个人的一点体会做 impeccable 这个过程让我对“工程质量”有了新的理解。过去我以为质量好是靠几个高手把关后来发现高手也会累也会疏忽也会在周五下午的紧急需求里妥协。真正可靠的质量不是靠英雄主义而是靠让“正确的事情方便做错误的事情不方便做”的制度设计。团队里推行这套机制时阻力最大的往往不是写代码的人而是那些总说“这些规则太严格了我们这是业务项目不是开源库”的人。我的回答一直是业务项目才最需要质量基线因为业务代码会不断生长没有约束的长线代码最终会变成所有人都不愿意碰的沼泽。给代码装上刹车不是为了限制速度是为了让你敢开快车。

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

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

免费获取报价 →
↑