1. 接手一个满是告警的仓库,我先做了什么
去年冬天我接手了一个迭代了三年多的中后台项目,仓库里积压的 ESLint 告警数量是 3400 多条,分布在 600 多个文件里。第一次跑npx eslint .的时候,终端刷了将近四十秒才停下来,同事在旁边看了一眼说"这玩意儿早就没人管了"。这句"没人管"其实很典型——大多数项目的 ESLint 整改都不是从零开始配置,而是从一个已经被反复// eslint-disable-next-line糊满的历史状态里往外捞。所以这篇内容讲的是 ESLint 整改代码过程中真实会遇到的问题和解决方案,偏向实战排障,不是配置教程。如果你手上正好有一个告警数量在三位数以上、--fix跑完还剩一大半、团队里没人愿意接的仓库,那这篇基本就是为你写的。零基础也能看懂,因为我会把每条规则为什么报、报完之后该怎么处理都拆开讲。
先说清楚一个认知:ESLint 整改不是"把红色波浪线消掉",而是"把代码里潜在的问题显性化并逐条做出决策"。3400 条告警里有大概 40% 是真问题(未使用变量、错误依赖数组、隐式 any),30% 是风格不一致(引号、分号、import 顺序),剩下 30% 是老代码用了新规则自然会触发的东西。这三类处理方式完全不同,混在一起改,改到最后一定是越改越乱。我当时的做法是先不动代码,花半天时间把"告警长什么样"摸清楚,再决定从哪儿下刀。下面这条路线是我踩了两轮之后总结出来的,后面会一节一节展开。
1.1 整改前的三件准备工作
第一件事是锁定版本。很多人整改到一半发现规则突然变多,原因是package.json里写的是"eslint": "^8.0.0",某次npm install之后小版本跳了,新版本默认规则集变了。我的做法是把 eslint、parser、各个 plugin 全部改成精确版本,去掉^和~,同时在 CI 里加一条npm ls eslint的检查。这件事听起来很琐碎,但它决定了你后面所有的统计数字是否可信。
第二件事是确认当前配置到底生效了哪些规则。别相信README里写的,直接跑:
npx eslint --print-config src/main.ts > /tmp/effective-config.json这条命令会输出某个文件最终合并后的完整配置,包括 extends 链路里带进来的所有规则。我当时就是这么发现项目里同时装了eslint-config-airbnb和另一个团队自定义的 config,两边的quotes规则互相覆盖,最终生效的是后者,但很多人还按 airbnb 的写法在改。规则冲突是整改期最浪费时间的坑之一,早点用--print-config看一眼能省掉大量"我明明改了怎么还报"的困惑。
第三件事是导出一份 JSON 报告做基线。这一步是后面所有分批操作的依据:
npx eslint . -f json -o eslint-baseline.json有了这份文件,你才能做统计、做对比、做"改完前后告警是否真的减少"的验证。我记得第一次整改完提交,同事 review 的时候问"你确定减少了吗",我直接把两份 JSON 用脚本 diff 出来的表格甩过去,比任何口头说明都有说服力。
1.2 我给自己定的整改顺序
整改顺序这件事,我试过两种极端。第一种是"按目录推",从src/utils开始一个目录一个目录清,好处是范围可控,坏处是同一个规则问题会在不同目录反复出现,改到第十个目录的时候人会麻。第二种是"按规则推",先把no-unused-vars全部清掉,再清import/order,好处是同类问题集中处理效率极高,坏处是一次性改动面太大,review 成本高,出问题不好回滚。
最后我采用的是混合策略:先按规则统计出 Top 10,把其中"可自动修复"的规则用--fix一把梭;剩下的按"规则 + 目录"交叉切分,一次提交只处理一条规则在一个模块内的问题。这样每次 PR 的 diff 大概在 200 到 500 行之间,review 的人能看得完,出问题也能精准回滚。
这里有个经验值可以参考:单次提交的 diff 控制在 500 行以内,告警消除量控制在 100 到 200 条之间。超过这个量级,人脑已经无法有效 review 了,你只是在制造"看起来改了"的假象。
1.3 先跑统计,再谈方案
统计脚本很简单,但对后续决策帮助巨大:
// scripts/eslint-stats.js const fs = require('fs'); const report = JSON.parse(fs.readFileSync('eslint-baseline.json', 'utf8')); const byRule = {}; const byDir = {}; let total = 0; for (const file of report) { const dir = file.filePath.split('/src/')[1]?.split('/')[0] || 'root'; for (const msg of file.messages) { const rule = msg.ruleId || 'fatal'; byRule[rule] = (byRule[rule] || 0) + 1; byDir[dir] = (byDir[dir] || 0) + 1; total++; } } const sorted = Object.entries(byRule).sort((a, b) => b[1] - a[1]); console.log('总告警数:', total); console.table(Object.fromEntries(sorted.slice(0, 15))); console.table(byDir);跑出来的结果里,前五名占了总量的 70% 左右:@typescript-eslint/no-explicit-any、no-unused-vars、import/order、react-hooks/exhaustive-deps、prettier/prettier。这个分布非常典型,基本可以说明后面的整改重点在哪。有了这张表,你跟团队同步进度的时候就不再是"改了一半",而是"no-explicit-any从 800 降到 300",量化之后推动力完全不一样。
2. 配置文件本身的坑,比代码还多
真正开始改代码之前,配置层面的问题必须先解决干净。我遇到的情况是:代码改完了,告警没减少,一查发现规则压根没生效;或者本地不报,CI 报,两边跑出来的结果差了一大截。这一类问题的根因几乎都在配置上。
2.1.eslintrc和 flat config 的代际差异
现在处于两代配置并存的状态。老项目基本是.eslintrc.js/.eslintrc.json,新版 ESLint(v9 之后)默认只认eslint.config.js这种 flat config。我接手那个项目是 v8 +.eslintrc.js,升级到 v9 的时候踩了一串坑,最典型的是extends的写法变了。
老写法:
// .eslintrc.js module.exports = { extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended'], parser: '@typescript-eslint/parser', };新写法:
// eslint.config.mjs import js from '@eslint/js'; import tseslint from 'typescript-eslint'; export default tseslint.config( js.configs.recommended, ...tseslint.configs.recommended, { files: ['**/*.{ts,tsx}'], rules: { 'no-unused-vars': 'off', '@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }], }, }, );这里有两个必须知道的细节。第一,flat config 里不能再写parser字段,parser 是通过 config 对象里的languageOptions.parser或者 preset 带进来的,直接写parser: 'xxx'会被忽略而且不报错——这属于沉默失败,排查起来非常折磨。第二,ignorePatterns变成了独立的ignores字段,而且在 flat config 里ignores只有放在单独的 config 对象里才表示"全局忽略",跟别的配置混在一起会被当成 scoped 忽略。我第一次迁移的时候把ignores和rules写在一个对象里,结果dist目录被一起 lint 了,告警数从 3400 直接飙到 12000。
2.2 规则集的取舍:别一口气上 recommended
eslint:recommended和plugin:@typescript-eslint/recommended是好东西,但一次性全部打开会让存量项目瞬间爆炸。我的做法是分三档:
| 档位 | 处理方式 | 适用规则举例 |
|---|---|---|
| 第一档:必须报 error | 阻塞合并,立刻修 | no-undef、no-dupe-keys、no-unreachable |
| 第二档:先 warn | 不阻塞,逐步清理 | no-unused-vars、import/order、prefer-const |
| 第三档:关掉或延后 | 存量太多,先不管 | no-explicit-any、react-hooks/exhaustive-deps |
这个分档不是拍脑袋,判断标准是**"这条规则报的是不是真正的运行时风险"**。no-undef报出来说明代码里引用了不存在的变量,这种大概率是 bug,必须 error。import/order纯粹是风格,报 error 只会让所有人骂你,warn 就够了。no-explicit-any严格说要处理,但存量项目里 800 个 any 一夜之间全改完不现实,所以先 warn,设置一个季度清理目标。
提示:把规则从 error 降成 warn 只是争取时间,不是终点。如果一年之后还是 warn,那这条规则等于没加。我一般会在 issue 里挂一个带日期的清理任务,到期就升 error。
2.3 用 overrides 做局部豁免,而不是全局关规则
这是我觉得最值得分享的一条经验。很多人的处理方式是"某条规则太吵了,全局关掉",结果就是新代码也不受约束了。正确做法是用 scoped config,让规则只在特定目录下降级:
export default tseslint.config( js.configs.recommended, { files: ['src/legacy/**/*.ts'], rules: { '@typescript-eslint/no-explicit-any': 'off', 'no-console': 'off', }, }, { files: ['src/**/*.ts'], rules: { '@typescript-eslint/no-explicit-any': 'warn', }, }, );这样老代码区域保持宽松,新写的东西逐渐收紧,团队不会因为"一上来就报几百个错"而集体抵触。我把它叫做**"隔离区"策略**——把历史债务圈起来,新代码按新标准走,两边互不干扰。这个策略推下去阻力明显小很多,因为没人会觉得你在给他添麻烦。
同样的手法也适用于测试文件、构建脚本、类型声明文件,它们对规则的要求本来就不一样。*.d.ts里大量any是正常的,scripts/*.js里console.log也是正常的,这些都应该用files精确圈定,而不是简单粗暴地写/* eslint-disable */。
3. 高频报错逐个拆解:报错的真实原因
代码层面的整改,80% 的时间花在前 5 条规则上。我把每一条的处理逻辑都整理出来了,包括什么情况下该改代码、什么情况下该改规则。
3.1no-unused-vars:不能无脑删
这条规则报的是"声明了但没用到"。看起来最好处理,实际上最容易改出 bug。原因是有些变量被"隐式"使用了:比如通过解构把属性从对象里摘出来的场景,剩下的属性用 rest 收集:
// 报错:'password' is assigned a value but never used const { password, ...rest } = user;这里的password确实没直接用到,但它的作用是从rest里剔除,删掉它会让rest里多出password字段,可能造成敏感信息泄露。这种情况下正确做法是配置ignoreRestSiblings: true:
'@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_', varsIgnorePattern: '^_', ignoreRestSiblings: true, }],另一种高频情况是函数参数。事件回调里经常写成(e) => {...},但实际没用到e;或者接口约定必须有三个参数,你只用了两个。我的处理原则是:
- 如果是回调签名必须保留的位置,用下划线命名
(_e, _index)配合argsIgnorePattern - 如果是真正多余的死参数,直接删,同时检查所有调用点
- 如果是
catch (err)但没处理,考虑改成catch {},前提是运行环境支持可选 catch binding
注意:
argsIgnorePattern只对未使用但位置靠后的参数生效,如果你改成(_a, b),调用方传参位置不能变。别为了消告警把参数顺序调了,那是纯粹的破坏性改动。
还有一类是只写不读的变量,比如let result;后面只做result += x但从没被读取。这种 ESLint 报no-unused-vars是对的,因为整段逻辑都是死代码,删掉反而让代码更干净。所以遇到这条告警,我的判断流程是:先问"这个变量有没有副作用",没有副作用就删,有副作用就改配置。
3.2no-explicit-any:从 any 迁移到 unknown 的实操
这是存量项目里数量最多的一条。800 多个any不可能全靠手工改,我的做法是分三批。
第一批只在"函数边界"上处理,也就是参数和返回值。这类位置改成unknown之后,编译器会强制调用方做类型收窄,收益最大:
// 改前 function parseConfig(raw: any) { return raw.data.items; } // 改后 function parseConfig(raw: unknown) { if (typeof raw !== 'object' || raw === null) { throw new TypeError('config must be an object'); } const data = (raw as { data?: { items?: unknown[] } }).data; return data?.items ?? []; }第二批处理"泛型约束位置"。很多any出现在Record<string, any>这种地方,换成Record<string, unknown>基本不影响使用,但能拦住把值随手传给别人的行为。
第三批是真的没法改的,比如第三方库的类型声明本身就有问题,或者动态 key 访问。这类我的处理是保留any但加注释说明原因,同时在配置里用/* eslint-disable-next-line @typescript-eslint/no-explicit-any -- 第三方 SDK 类型缺失 */。注意我在 disable 后面加了--和原因,这是 ESLint 支持的行内说明语法,review 的时候一眼就能看出是"有意为之"还是"忘了改"。
这里补一个细节:unknown不是any的替代品。any可以直接访问任意属性、可以直接赋值给任何类型,unknown必须先收窄。所以从any到unknown的迁移,本质上是在逼你补上"运行时校验"这一步。这一步不做,改了也是白改。
3.3import/order:路径别名和自动排序的冲突
import/order是那种"改起来没难度但数量巨大"的规则。开--fix大部分能自动排好,但有个坑:路径别名解析不出来。
项目里用@/components/Button这种别名时,如果 ESLint 不知道@指向哪里,会把它归类到external,结果就是内部模块和react、lodash混在一起排序,看着乱七八糟。解决方式是给 resolver 配别名:
settings: { 'import/resolver': { alias: { map: [['@', './src']], extensions: ['.ts', '.tsx', '.js', '.jsx'], }, }, },配完之后@/开头的会被正确识别为internal,排序才对。另外import/order的groups配置建议写清楚,我常用的是:
'import/order': ['warn', { groups: ['builtin', 'external', 'internal', 'parent', 'sibling', 'index'], 'newlines-between': 'always', alphabetize: { order: 'asc', caseInsensitive: true }, }],newlines-between: 'always'会让每组之间空一行,视觉上更容易扫读。这个设置一开始会被同事吐槽"太占地方",但坚持两周之后几乎没人再说了,因为找 import 的速度确实快了。
提示:
import/order的--fix是安全的,它只调整顺序不改语义。但如果你有import './polyfill'这类副作用导入,顺序是有语义的,需要给它单独分组或者加/* eslint-disable */。我见过一次因为自动排序导致 polyfill 排到后面、老浏览器直接白屏的事故。
3.4react-hooks/exhaustive-deps:最容易被错误处理的一条
这条规则报的是"useEffect依赖数组不完整"。绝大多数人的第一反应是加一行 disable 注释,然后告警就消失了——但 bug 还在。我处理这条规则的流程是三步判断:
第一步,看这个 effect 是不是"只在挂载时执行一次"。如果是,说明依赖数组设计从根上就有问题,应该改成useEffect(() => {...}, [])并在内部用 ref 或者函数式更新来避免闭包陷阱,而不是靠 disable 掩盖。
第二步,看缺失的依赖是不是"函数"。如果是组件内定义的函数,加进依赖数组会导致每次渲染都重建、effect 无限执行。这时候正确做法是用useCallback把函数稳定下来,或者把逻辑挪到 effect 内部。
第三步,只有在"依赖确实是稳定的但 ESLint 无法推断"的情况下,才用 disable,并且必须写原因:
useEffect(() => { // ... // eslint-disable-next-line react-hooks/exhaustive-deps -- dispatch 来自 useReducer,引用恒定 }, []);我统计过那次整改,600 多条exhaustive-deps告警里,真正需要 disable 的不到 50 条,剩下的都是真 bug 或者代码结构问题。这条规则的价值就在于此:它逼你把闭包问题想清楚。
3.5 Prettier 和 ESLint 打架
这个问题的表现是:改完格式,ESLint 说不对;按 ESLint 改,Prettier 又说不符合。根因是两套规则在争同一个格式决定权。解决方案很明确:让 Prettier 负责格式,ESLint 只负责代码质量。
具体做法是装eslint-config-prettier,放在 extends 的最后一位,它会把所有跟 Prettier 冲突的格式类规则关掉:
import prettier from 'eslint-config-prettier'; export default tseslint.config( js.configs.recommended, // ... 其他配置 prettier, // 必须是最后一个 );然后编辑器里装 Prettier 插件设成保存时格式化,ESLint 插件设成保存时修复。两者的分工明确了,就不会再打架。
还有一种情况是项目里同时有.prettierrc和 ESLint 的quotes/semi规则,这种情况必须二选一。我的选择是删掉 ESLint 里的格式规则,全部交给 Prettier,因为 Prettier 的格式化是可预测的,而 ESLint 的--fix在复杂表达式上偶尔会给出奇怪的结果。
4. 自动化整改的实操路径
手工改 3400 条告警,按每条 30 秒算也要 28 个小时。所以核心思路一定是"能自动的自动,不能自动的批量处理,剩下的人工收尾"。
4.1--fix的能力边界
--fix只对标记了可修复的规则生效。哪些可修复?在 ESLint 官方文档的规则列表里,带扳手图标的都是。常见的可自动修复规则:
| 规则 | 是否可自动修复 | 备注 |
|---|---|---|
prefer-const | 是 | 安全 |
quotes/semi | 是 | 注意与 Prettier 冲突 |
import/order | 是 | 副作用导入需注意 |
no-var | 是 | 安全 |
no-unused-vars | 否 | 删除有语义风险 |
no-explicit-any | 否 | 需要人工判断 |
react-hooks/exhaustive-deps | 否 | 必须人工分析 |
执行的时候不要直接npx eslint . --fix一把梭,因为格式规则和逻辑规则混在一起,改完根本没法 review。我的做法是先关掉所有格式规则,只让逻辑类规则的可修复项跑一遍:
npx eslint . --fix --rule '{"quotes":"off","semi":"off","indent":"off"}'这样 diff 里就只剩下prefer-const、no-var这类真正有语义意义的改动,review 起来清爽很多。格式统一放到最后一步单独提交,用 Prettier 一次性刷。
4.2 分批整改的脚本设计
我的批次划分逻辑是"按文件 + 按规则"二维切分。先选一条规则,再选出这条规则告警最集中的 20 个文件,作为一批:
// scripts/pick-batch.js const fs = require('fs'); const report = JSON.parse(fs.readFileSync('eslint-baseline.json', 'utf8')); const TARGET_RULE = process.argv[2]; const LIMIT = Number(process.argv[3] || 20); const hits = report .filter(f => f.messages.some(m => m.ruleId === TARGET_RULE)) .map(f => ({ file: f.filePath, count: f.messages.filter(m => m.ruleId === TARGET_RULE).length, })) .sort((a, b) => b.count - a.count) .slice(0, LIMIT); console.log(hits.map(h => h.file).join('\n'));拿到文件列表之后,可以只对这些文件跑 fix:
node scripts/pick-batch.js import/order 20 | xargs npx eslint --fix这样一批一批推,每批都能独立提交、独立验证。我大概用了三周时间,每天中午花一个小时处理两三批,整个过程没有加过班,也没有一次因为改动太大而出事故。
4.3 利用 ESLint 的抑制机制做渐进式整改
ESLint 新版本里有个功能叫 bulk suppressions,可以把现存的所有告警"一键冻结":
npx eslint . --suppress-all执行后会生成一个eslint-suppressions.json,里面记录每一条现存告警的位置和规则。之后跑 lint 的时候,这些问题不再报错,但新增的问题会正常报。这个机制的价值在于:你可以立刻让 CI 变绿,同时保证新代码不再产生新的债务。
我一般把它当作过渡手段,用在两种场景:一是团队需要立刻恢复"提交不阻塞"的时候;二是大规模重构期间,老文件的告警暂时顾不上。但要记住这是"冻结"不是"清理",冻结之后必须有计划地解冻,否则一年后就是另一堆 3400。
解冻的方法是配合--prune-suppressions:
npx eslint . --prune-suppressions这个命令会删掉那些"已经不再触发"的抑制记录,让 JSON 文件逐渐瘦身。我一般每两周跑一次,顺便看看哪些文件可以整体移出抑制列表。
4.4 提交门禁:让整改成果不再流失
整改完最大的风险是"过两个月又变回去了"。所以必须把门禁做上。我用的方案是lint-staged+husky,只对暂存区的文件跑 lint:
{ "lint-staged": { "*.{ts,tsx,js,jsx}": [ "eslint --fix --max-warnings=0 --no-eslintrc -c eslint.config.mjs" ] } }这里--max-warnings=0是关键,它让任何 warn 级别的告警也变成阻塞。这个设置要有心理准备,刚开始团队会有意见,需要配合前面的"隔离区"策略一起用,老代码区域的规则已经降级了,新代码区域卡严格一点是合理的。
CI 侧再补一层全量检查:
npx eslint . --cache --cache-location node_modules/.cache/eslint/ --max-warnings=0--cache这个参数一定要加,它会把每个文件的检查结果缓存起来,第二次跑只处理改动过的文件。我们这个项目加缓存之后,CI 里的 lint 时间从 47 秒降到 6 秒。缓存目录放在node_modules/.cache里的好处是自动被清理,不用额外写.gitignore。
5. 常见问题与排查技巧实录
这一节是我整个整改过程中攒下来的问题清单,按出现频率排。
5.1 问题速查表
| 现象 | 可能原因 | 排查命令 | 解决方向 |
|---|---|---|---|
| 改了代码告警没减少 | 规则未生效 / 文件被 ignore | npx eslint --print-config <file> | 检查 config 合并链路和ignores |
| 本地不报,CI 报 | 两边 ESLint 版本或配置不同 | npm ls eslint | 锁定版本,CI 用同一个 config |
--fix跑完没变化 | 该规则不支持自动修复 | 查规则文档的扳手图标 | 改为手工处理或降级 |
| 告警数量突然暴涨 | ignore 配置写错,把dist也扫了 | npx eslint . -f json | jq '.[].filePath' | 修正ignores位置 |
| lint 特别慢 | 没开缓存 / 扫描了 node_modules | npx eslint . --debug | 加--cache,检查 ignores |
no-undef在 TS 文件里误报 | 没配 TS 的 globals | npx eslint --print-config x.ts | 引入globals配置或关掉该规则 |
5.2 我踩过的三个具体坑
第一个坑:eslint --fix把代码改坏了。那次是no-extra-parens规则对一段涉及??和||混用的表达式做了自动修复,加了几行括号之后优先级变了,业务逻辑跟着变。当时单元测试覆盖不足,直接发到测试环境才发现。教训是:--fix之后必须跑一遍测试再提交,尤其是涉及运算符、可选链、逻辑表达式的自动修复。现在我的习惯是 fix 完先git diff扫一眼,看有没有非预期的改动,再跑测试。
第二个坑:禁用了规则,但禁用错了位置。有些时候// eslint-disable-next-line放在函数外层,其实只对紧接着的那一行生效,如果目标行中间隔了注释或空行,注释就失效了。更隐蔽的情况是// eslint-disable-next-line写在了return之后,那一行是空行,注释对着空行生效,真正的代码行还在报。排查办法是看 ESLint 报错的行号,对照着改;实在找不准,用npx eslint <file> --format json看具体的line和column。
第三个坑:多个 config 文件的优先级。项目里同时存在.eslintrc.js、.eslintrc.local.js,还有package.json里的eslintConfig字段。ESLint 的规则是就近生效,越靠近被检查文件的配置优先级越高。结果就是src/admin目录下的文件用的是src/admin/.eslintrc,跟主配置完全不是一回事。这种情况在新项目里不该出现,但存量项目里几乎一定有。排查靠--print-config加--debug,后者会打印每个文件加载了哪些配置。
5.3 关于eslint-disable的纪律
我最后在团队里定了一条规矩:所有eslint-disable必须带原因,且原因写在同一行的--后面。这条规矩配合一个简单的 grep 检查就能落地:
grep -rn "eslint-disable" src --include="*.ts" --include="*.tsx" \ | grep -v "\-\-" \ && echo "存在未注明原因的 disable" && exit 1这条命令放在 CI 里,没写原因的 disable 直接让流水线红掉。规矩执行了两个月,项目里 disable 注释从 400 多条降到 90 多条,其中大部分是真的必要(第三方类型问题、确实需要绕过的钩子依赖)。
另一个细节是优先用eslint-disable-next-line而不是eslint-disable。行级 disable 作用范围精确,块级 disable 容易误伤后面新增的代码。我在 review 里见过一段/* eslint-disable */开在文件头,三年没人敢删,里面的所有规则全关了,等于这个文件从来没被 lint 过。
5.4 性能优化:让 lint 跑得动
最后一个实用技巧是关于速度的。大项目里 ESLint 慢是普遍问题,几个立竿见影的优化:
--cache:必加,效果最明显ignores里加上构建产物:dist、build、coverage、*.min.js- 避免在
files里写**/*:能限定到具体扩展名就限定,**/*.{ts,tsx}比**/*快一截,因为不用去读 CSS、MD、图片 - 多项目分开跑:如果仓库是 monorepo,按 package 分别跑 lint,利用
--cache的目录隔离,比一次性全量扫快得多
我们那个项目做完这四项优化,本地全量 lint 从 38 秒降到 4 秒,CI 里从 47 秒降到 6 秒。速度上来之后,大家才愿意在提交前主动跑一次,这件事对"整改成果不流失"的意义比任何规矩都大。
6. 一些不太适合写进规范的个人体会
整改这件事,技术难度其实只占三成,剩下七成是节奏和预期管理。我最大的体会是不要把 ESLint 整改当成一个"任务",要当成一次"代码审计"。3400 条告警里藏着的那些未使用变量、错误的依赖数组、隐式的 any,本身就是代码质量问题的清单。你按着清单一条一条过,顺手就能发现一堆潜在 bug。我那次整改过程中,光靠no-unused-vars就翻出来三处"变量声明了但赋值逻辑写错了导致后续判断永远为真"的问题,这种问题跑测试跑不出来,但上生产就是事故。
另一个体会是别追求 100% 零告警。有些告警注定是要用 disable 绕过的,比如第三方类型缺失、某些必须用 any 的动态场景。追求零告警的结果往往是写一堆毫无意义的包装类型,反而让代码更难读。我给自己定的目标是"告警数量稳定在 50 条以内且每条都有注释说明",这个目标比"零告警"现实得多,也健康得多。
还有个操作上的小建议:整改期间把git blame用起来。删一个变量之前看一眼它是谁加的、为什么加,经常能发现"这个是留给某个未上线功能用的"这种信息。花十秒钟看 blame,比事后回滚一次强太多。工具不会替你判断语义,它只告诉你哪里可能有坑,跳不跳还得自己看。