先声明一下这个标题里的 impeccable 不是穿搭博主嘴里的“无可挑剔”而是我折腾了差不多两个周末才理顺的一个前端工程化工具。它解决的问题非常具体每次 Code Review同事们的批注总有一半以上是格式问题、导入顺序问题、类型定义问题这些问题明明可以用工具自动拦掉却偏要等人肉眼去发现。impeccable 就是一套把 ESLint、Prettier、Stylelint、Husky 和 lint-staged 组合成默认配置的 npm 包装好后跑一条命令提交代码之前所有能自动处理的“脏乱差”都会先被打扫一遍。写这篇文章是希望把我在设计这套工具时的取舍、接入时的步骤还有踩过的坑都记录下来。如果你是前端团队的负责人可以参考它来建立自己的代码质量基线如果你是刚入行的新手也可以把它当成一份“工程质量规范模板”来读看看一个现代前端仓库到底应该配哪些检查。1. 整体设计与核心思路1.1 痛点质量检查为什么总是靠人肉先说一个困惑了我很久的问题为什么大家都在喊代码规范但很多项目依然靠 code review 人肉把关我分析下来有三个原因。第一规则太多记不住。一个稍微完整一点的 ESLint 配置光 react 相关规则就上百条正常人不可能全背下来。第二工具太散配置成本高。ESLint、Prettier、Stylelint、Husky 每个都是独立体系新人配一套环境少说半天配完还容易因为版本冲突跑不起来。第三反馈太晚。多数检查都在 CI 阶段做开发者在本地写代码时得不到即时反馈等到推上去才发现一堆 error。impeccable 的思路就是把这三点压掉。用默认配置替团队做决定用编辑器插件和 git hook 把反馈提前到写代码和提交的瞬间用 init 命令把配置成本降到两分钟。哪怕团队里最不熟悉工程化的成员也能在提示下快速修完问题。我还特意没把它做成脚手架这是很多人不理解的地方。一听到工程化规范大家总想到 create-vite、vue-cli 那种一键生成项目的工具。但真实团队里大部分代码库已经运行了一两年不可能为了接规范就重写项目。脚手架生成完规则深埋在模板里团队想调整某条规则得去翻生成器的源码。impeccable 的定位是一套可以被现有项目渐进式接入的“配置预设 命令行工具”不强制改变项目结构。1.2 设计目标零配置、可覆盖、可扩展我给自己定过三个关键词零配置、可覆盖、可扩展。零配置不是说完全没有配置而是说默认配置已经足够多数项目使用。你安装之后impeccable 会根据当前项目里有没有 typescript、有没有 react、有没有 vue 来自动加载对应规则所以不会出现“这个项目还没装 vue但配置里却引用 vue 插件”的报错。可覆盖是给那些有特殊需求的团队留的口子。比如你们明确不要单引号或者想强制函数必须显式返回类型可以在 eslint.config.js 里自己覆盖规则保留和覆盖的优先级明确。可扩展则意味着我不会把一个规则集硬编码成死文件而是拆成 core、react、vue、node 几个子模块内部按需组合。如果有人想在里面加一条团队内部规则可以直接写在外层配置里而不是 fork 一份回来改。这三个关键词在很多工具里都听过但真正落到实现上需要做一些取舍。比如零配置要求自动检测依赖这本来可以在安装阶段做但我选择在生成配置的 init 阶段做因为运行时的动态检测会让 ESLint 每次执行都要扫描 package.json影响性能。再比如可扩展我没有做成 vue-plugin 式的预设链而是让工厂函数接收用户覆盖项这样覆盖关系在代码里一目了然。1.3 技术架构一份配置两个入口上面说了设计目标再看实现。impeccable 的代码层面其实不复杂核心就是一个工厂函数和两条命令。工厂函数接收用户配置返回 ESLint 的 flat config 数组两条命令分别是impeccable check和impeccable fix。check 只读取不写文件适合 CIfix 会在格式化后重写文件。为了避免重复实现CLI 的检查逻辑和 pre-commit 里的逻辑完全共用同一个规则数组只是目标文件不同——CLI 默认处理项目所有文件pre-commit 只处理git diff --cached --name-only输出的文件。依赖上我没有自己写 lint 逻辑而是站在社区肩膀上。核心依赖包括 eslint 本身、typescript-eslint 的 parser 和 plugin、eslint-plugin-import、eslint-plugin-promise、prettier、husky 和 lint-staged。你可能觉得这也太多依赖了但换来的是你不用再自己拼装。对照一个正常项目这其实是每个质量工具链都会装的同样几个包impeccable 只是把它们集中管理并且把版本之间的兼容关系在peerDependencies里锁好了。架构上的另一个关键点是配置生成与执行分离。init 命令生成的eslint.config.js是纯静态文件之后用户把它提交到 git 仓库之后每次执行 lint 时不会再去访问网络或者动态生成。这样即使用户后来删掉了node_modules配置依然稳定存在。我把这个设计叫做“一次生成永久生效”它减少了工具在运行时的意外状态。1.4 为什么直接选择了 flat config这里要特别说一下 flat config也就是 ESLint 9 默认的配置格式。很多人从 .eslintrc 迁移时觉得不习惯但对我这种工具作者来说flat config 实在太适合做预设型工具了。旧版 eslintrc 的继承和覆盖规则很隐式预设里的规则容易被用户填充成一个很深的层级flat config 则是明文数组每个对象都对应一组文件覆盖就是简单的 JavaScript 展开。比如我们要关闭某条规则直接在一个对象里写rules就行后面对象的同名规则会覆盖前面的。这种风格让 impeccable 的调试变得非常直观出现意外规则冲突时我能直接打印出最终的配置数组逐项看。另外 flat config 天然支持files匹配模式我能精确控制“测试文件里放宽 any”、“配置文件里允许 console”这类场景而不需要像旧版那样写一堆 overrides。如果你还在用 .eslintrc我建议接入前先做一次迁移。这个迁移成本通常半天左右但换来的是一套更现代的配置体系。impeccable 从 1.x 开始就只支持 flat config所以如果你的项目还停在 ESLint 7 或 8 的旧写法规格直接装会报 peer 依赖冲突需先升级到 ESLint 9。2. 核心模块与规则集拆解2.1 ESLint 三层规则基础、语言、框架impeccable 默认提供的 ESLint 规则集分成三层。第一层是基础规则不管什么项目都适用像no-unused-vars、no-debugger、eqeqeq、no-fallthrough这种我从 eslint 的 recommended 集里选了一批又加了几条自己认为必要的比如no-console默认给 warning 而不是 off。第二层是语言扩展目前主要针对 TypeScript像typescript-eslint/no-explicit-any、consistent-type-imports、no-floating-promises等。第三层是框架扩展包括 react 和 vue。每层都是独立文件工厂函数按项目情况拼接。这带来一个很实际的好处同一个包既能服务纯 Node 项目也能服务 React 项目不会因为加载了用不到的插件导致配置报错。规则严苛程度上我的原则是“error 少而精warning 给余地”。比如no-explicit-any是 error因为 any 会破坏类型推导no-console是 warning因为有些调试场景确实需要临时打印但提交前应该清掉。给 warning 不影响提交但能在编辑器里时刻提醒。对于框架扩展我可以再展开说一点。React 部分我不光启用了 react-plugin 的 recommended还把react/jsx-no-useless-fragment、react/no-array-index-key这种偏质量的规则提到了 error。Vue 部分同样以 essential 和 strongly-recommended 为基础但去掉了几条容易误报的比如vue/max-attributes-per-line因为 Prettier 会管格式不需要 ESLint 再重复。这个取舍背后的逻辑是规则不追求数量而是追求每一条都能挡掉真实问题。2.2 Prettier 配置把风格争议交给机器关于代码风格我的态度很绝对风格问题不要靠人治让 Prettier 一把梭。impeccable 内置的 prettier 配置默认是单引号、无分号、行宽 100、缩进 2、尾随逗号 all。我知道每个团队对分号这个问题都能吵一年但请你注意这个配置只是给团队一个默认值你在.prettierrc.json里改成双引号也没问题关键是整个仓库只能有一种配置。Prettier 加入流程后ESLint 里的格式类规则就都可以关闭了因为它们本质上是 ESLint 在重复 Prettier 做的事。两者同时存在要么冲突要么多余。所以我在最后会引入eslint-config-prettier把它当作一道防护栏把所有可能产生冲突的格式规则全部置为 off。这一步很多人会忽略导致升级规则集后突然冒出一堆缩进报错。在 pre-commit 阶段我的顺序是先跑 Prettier 再跑 ESLint。因为格式化之后再检查能避免大量格式导致的规则误报。有人问为什么不让 ESLint 去修复格式ESLint 的--fix虽然能做一部分但 Prettier 的格式化能力更完整尤其是换行、模板字符串、函数签名这些复杂场景。让两者各司其职才是最省心的组合。2.3 覆盖与忽略给团队留的灵活接口为了满足“可覆盖”这个目标impeccable 导出的配置不是一个静态对象而是一个工厂函数。在底层实现上它会先展开内置配置数组再把用户的覆盖项 push 进去并自动把plugins和languageOptions做浅合并。覆盖时最常见的写法是在项目根目录创建eslint.config.js像下面这样import impeccable from impeccable export default impeccable( { // 所有文件 rules: { no-console: warn, }, }, { // 仅测试文件 files: [**/*.test.ts], rules: { typescript-eslint/no-explicit-any: off, }, }, )我建议把团队业务特定规则写在这里而不是去改 node_modules 里的包这样升级 impeccable 时不会冲突。忽略文件的处理也遵循同样逻辑核心库内置了一份常用忽略清单比如dist、coverage、node_modules同时会读取项目自己的.gitignore把其中的路径合并进去。这样你不需要在一个新工具里重新维护一份 ignore 列表少了很多重复配置。一个容易踩的小坑是覆盖项的放置顺序。如果你希望某条规则彻底关闭这条覆盖项应该放在 prettier 配置之后。我会在工厂函数内部确保用户覆盖项位于数组末尾但有些情况下用户会手动拼接数组那就需要注意顺序。规则冲突时 ESLint 并不是“报错”而是静默采用最后一个匹配对象的规则所以顺序错了你根本看不见提示规则依然被前面的配置挡住。2.4 性能优化缓存、增量与并行规则多不等于检查慢。eslint 的性能瓶颈通常不是规则数量而是每次都要重新解析文件。impeccable 通过三个手段控制耗时。第一默认开启 ESLint 的缓存把cache: true和cacheLocation指向node_modules/.cache/impeccable第二在 pre-commit 里只 lint 暂存文件避免全量扫描第三对 monorepo 场景忽略node_modules和构建产物避免解析那些根本没被 import 的文件。实测一个两千个文件的项目如果冷启动全量检查大概要 40 秒但配合缓存后第二次全量检查降到 8 秒左右而增量检查通常不到 5 秒。对一个五到十人的前端小组来说这个速度完全不会影响日常开发。如果你的项目规模更大可以考虑在 CI 里用--changed参数它会基于 git diff 只检查本次改动的文件这条命令在下面的实操部分会再提。性能优化的另一个细节是文件匹配。不要把**/*这种规则放在最前面否则 ESLint 会扫描所有文件包括图片、字体、md 等。impeccable 内部把匹配对象拆成**/*.{js,mjs,cjs,ts,mts,cts,jsx,tsx,vue}这样可以避免无意义的解析。如果项目里还需要检查 JSON 或 YAML建议单独接入 eslint-plugin-json 而不是扩大核心匹配范围。3. 实操接入从零跑通整套流程3.1 环境准备与安装命令如果你已经决定要试直接在测试仓库里这样装npm install --save-dev impeccable npx impeccable init第一个命令安装工具本身并触发 peerDependencies 的安装。第二个命令会做三件事生成eslint.config.js、生成.prettierrc.json、在package.json中注入lint和lint:fix脚本同时配置 husky 的 pre-commit 钩子。如果你不想要某一部分命令提供了--skip-hooks之类的选项比如你只想先接 lint 规则暂时不想强制提交钩子就可以跳过。我需要提醒一下 Node 版本。impeccable 要求 Node 18 或更高版本因为依赖的 ESLint 9 只在 Node 18 以上稳定运行。如果你还在用 Node 16建议先把运行时升级否则安装时会遇到 engine 不匹配的警告。另一个常见问题是项目中已经存在一份旧的 ESLint 配置init 会问你“是否覆盖”我建议此时先备份旧的.eslintrc.*文件等新配置跑通了再删。3.2 初始化后的项目结构初始化之后你的仓库里会比之前多出几个文件。以我的测试项目为例. ├── .husky/ │ └── pre-commit ├── .prettierrc.json ├── eslint.config.js ├── package.json └── src/pre-commit里的内容通常长这样npx --no-install lint-stagedpackage.json里会多一组脚本{ scripts: { lint: impeccable check ., lint:fix: impeccable fix . } }很多人会问为什么 pre-commit 里不直接写npm run lint这是因为全量 lint 在大仓库里太慢。lint-staged 的配置在 impeccable 初始化时也已经写进package.json默认规则是*.{js,ts,jsx,tsx,vue}这些文件先执行 Prettier 格式化再执行 ESLint 修复。这意味着你在提交时只会检查本次暂存进去的文件而不会因为历史遗留问题卡住提交。3.3 日常开发工作流接入之后日常开发流程会变成这样写代码时编辑器里的 ESLint 插件读的是根目录的 flat config所以黄色波浪线就是最终的规则提示写完后git add暂存在提交时 husky 触发 pre-commitlint-staged 会先格式化再检查如果失败提交会被中断屏幕上会直接列出问题文件和规则名。比如我随手写了一个const a: any {}提交时就会被typescript-eslint/no-explicit-any挡下来提示你换成更具体的类型或者标注 eslint-disable 并写清原因。如果问题是可以自动修复的比如 import 顺序乱了lint-staged 会直接改好文件你只需要重新git add一次再提交。这里有一个非常实用的小建议提交之前养成git diff --cached看一眼习惯因为你重新 add 过后能确认自动改动确实是你想要的。如果你用的是 VS Code建议在项目里装好“ESLint”和“Prettier”两个扩展并且把保存时格式化打开。这样大部分问题在写代码的瞬间就被处理掉了pre-commit 的拦截成功率也会特别高。团队里如果有人没用编辑器扩展也不要紧提交钩子依然能兜底。3.4 新项目一键接入 vs 存量项目渐进式接入如果你从零开始一个新项目可以用npx create-impeccable来生成项目骨架它会顺带把目录结构、基础配置、示例组件都建好。但如果是存量项目我强烈不建议走这条路因为你可能会被几百个报错吓住。更好的方式是分三步走第一步只启用 Prettier 和 import 排序规则把格式统一第二步把 ESLint 的 error 级规则打开先做到没有 errorwarning 可以留着第三步等团队习惯之后再把 warning 级规则逐步提升为 error。impeccable 的 init 命令里有一个--levelwarning选项就是给这种渐进式接入准备的。开了这个选项内置 error 规则会被降级成 warning提交不会被拦截。这样团队可以先把工具跑起来再通过一个迭代一个迭代地消灭问题。我自己的经验是大部分团队从接入到完全清零大概需要两到四周核心问题往往集中在历史代码里的 any 和 any-type 断言上。渐进式接入还有个技巧把历史问题比较严重的目录在初始阶段放进 ignore 列表先让新代码和核心模块达标。这个阶段我建议在周会上同步一份“白名单列表”明确哪些目录将在哪个时间点被解除豁免避免白名单变成永久保护区。工具要服务人不要让人跪着迁就工具这是底线。3.5 在 CI 中集成命令行工具天然适合作为 CI 的一环。我在 GitHub Actions 里通常是这样写的- name: Lint run: npx impeccable check .在 CI 里不建议开fix模式因为自动修改会产生未提交的差异容易掩盖问题。CI 应该只检查不修复。如果你用的 CI 服务不支持缓存node_modules/.cache那全量检查会慢一些但至少保证质量。我这里实际经验是在 CI 里额外跑一遍impeccable check --changed只需要对比 git diff 来检查本次改动文件常常 10 秒内就结束了。不过要注意--changed在 CI 的 PR 分支上一般没问题但如果你在本地跑它会基于当前分支和默认分支的 diff 计算。如果你的 CI 统一用 merge commit 方式那么应该用--ref origin/main之类的参数指定对比基线。这一块不同 CI 平台的 api 不一样我不展开但方向是让检查范围尽可能小反馈尽可能快。4. 常见问题、排查与避坑心得4.1 ESLint 和 Prettier 又打架了最经典的问题是格式化类规则冲突。以typescript-eslint/indent为例它试图管理缩进但 Prettier 也有一套完整缩进逻辑。解决方法是把 ESLint 中所有关于格式化的规则都关掉在 flat config 中加入eslint-config-prettier。impeccable 默认已经内置了这个配置并且把它放在数组的最后确保它能覆盖前面所有冲突规则。如果你自己写了覆盖项一定不要写错顺序用户覆盖项应该放在 prettier 配置之后。如果你发现某个 style 规则还在报错大概率是覆盖项顺序放错了。排查思路很简单在项目里跑一下npx eslint --print-config src/index.ts能看到最终应用的规则值。如果某条规则显示 off 但你明明配了 error就说明有更靠后的配置覆盖了它往前翻就能找到罪魁祸首。4.2 lint-staged 没有触发很多人装了后发现 pre-commit 不执行最常见的两个原因一是 husky 的目录没有初始化npx husky install只在 init 时跑了一次如果你把.husky目录加进.gitignore了其他同事克隆仓库后就没有 hook二是$PATH问题husky 在 GUI 提交工具里可能找不到npx建议在 pre-commit 脚本里使用npx --no-install lint-staged避免因为临时安装依赖导致卡死。这里有个小细节lint-staged 的配置文件不能写在 husky 钩子内部如果你要加自定义文件后缀记得去package.json的lint-staged字段里加。另外在某些 Windows 环境下husky 的 hook 会因为没有执行权限而失效需要在项目里运行一次chmod x .husky/pre-commit。这个坑在团队里出现过好几次换成跨平台脚本能省不少事。4.3 monorepo 场景子包与根目录在一个 pnpm workspace 里各子包可能使用不同规范。impeccable 针对 monorepo 的做法是支持在子包目录中执行impeccable check .它会向上查找最近的eslint.config.js同时为了避免规则在根目录把整个世界检查一遍默认忽略项里已经加上packages/*/node_modules以及pnpm-lock.yaml等。如果你希望全仓统一规则在根目录跑一次即可但记得关闭子包里的独立 config或者删掉子包里的eslint.config.js否则 ESLint 会向上合并规则叠加可能导致一些诡异问题。我遇到过一个很典型的场景根目录设置no-console: warn子包再设置no-console: error结果同一个项目在不同目录跑结果不一样团队直接懵了。统一规则就统一到底不一致必须在文档里白纸黑字写清楚。4.4 老项目首次接入的海量报错我试过一个有五年历史的中型项目首次接入时产生了 1200 多个 error。这时候千万别手动一条条改而是先运行impeccable fix .自动修复格式和可自动修复的规则通常能解决 80%剩下 20% 大多是no-explicit-any、no-non-null-assertion这种需要人判断的。我的建议是分文件提交先处理核心模块把非核心模块放到 ignore 清单里。同时可以在代码里加临时的eslint-disable注释但一定要在注释里写上责任人姓名和日期后续排期清理。我见过太多人加// eslint-disable-next-line就再也不管这些注释最后变成技术债的一部分。如果团队长期如此建议在规则里把eslint-disable的使用限制成“必须有注释描述”否则报错。还有一点就是并行 lint 的内存控制。老项目文件多如果 Node 内存不够impeccable fix可能会 OOM。你可以用--concurrency 2限制并行数量或者在命令前加NODE_OPTIONS--max-old-space-size4096。我一般先在本地跑 fix再全量检查确认没问题后才动 CI避免把构建机搞挂。4.5 版本升级与团队落地工具升级不是单纯npm update。impeccable 的规则集每次大版本都会跟着 ESLint 升级如果你的同事编辑器插件还停留旧版本新规则可能在他们本地不生效。我的做法是在 CI 中固定锁版本同时在团队 README 里写清楚“运行npm update之后必须跑一次npx impeccable init --force重新生成配置”。团队落地这件事比技术本身更考验执行力。我通常建议把代码规范写进团队的 Definition of Done比如“所有 PR 必须通过 lint 检查”“本地提交前必须走 pre-commit 钩子”。这看起来像流程负担但只要工具够快体验几乎是无感的。更重要的是规范不是用来惩罚人的而是用来把人的注意力从机械劳动里解放出来。团队里如果有成员总被 lint 卡住应该去优化配置或者教他配好编辑器而不是指责他写代码不认真。最后分享一个小技巧给 impeccable 的规则集写一个简单的速查表挂在项目 Wiki 里。上面列出最常触发的 10 条规则、为什么这么定、怎么改。我实际发现当团队理解规则背后的原因后无意义的抱怨会少很多。工具能拦住问题但让团队愿意遵守规则靠的还是这些细节。我个人在反复折腾这些配置后发现所谓“无可挑剔”不是追求零警告而是让机器帮我们把基础项管住把人的精力留给真正需要判断的问题。impeccable 的规则集永远会有争议但流程一旦确定下来团队里因为空格和引号而发生的争论确实可以彻底消失。如果你也在为 code review 里的细碎问题头疼不妨拿这套流程到自己的测试仓库里跑一遍。
阅读完成 · 觉得有帮助?