你有没有遇到过这种情况同事交给你一个“能跑”的老项目你改了一行代码保存编辑器瞬间被波浪线淹没。你反复确认这行代码没有语法错误但 ESLint 就是在报错。然后你打开项目根目录那个.eslintrc.js盯着里面的rules看了半天改了几处还是不对。大多数时候真正的问题不是某条规则写错了而是你根本没看懂这份配置文件到底在表达什么。这篇文章我打算把 ESLint 配置文件这件事从头到尾聊透它是什么为什么项目里必须有一份里面每一个字段到底在做什么规则冲突时谁说了算以及新的eslint.config.jsflat config该不该上车。适合刚接手项目的前端新人也适合配置写得很随意、想系统梳理一遍的开发者。1. 先把话说清楚ESLint 配置文件到底在管什么1.1 从一段“看着正常却疯狂标红”的代码说起先看一段非常普通的代码const user window.localStorage.getItem(token);你在浏览器里写这行代码没有任何问题。但如果你的项目没有声明env.browserESLint 会直接给你画个红圈window is not defined。这就是 ESLint 配置文件存在的第一个意义它要在“检查代码”之前先告诉检查器“这堆代码跑在什么环境里”。没有配置文件ESLint 默认连浏览器环境都不认识更不用说什么 Vue、TypeScript、Node 的全局变量了。如果只是window还可以忍那你再感受一下这种场景项目里用了process.env.NODE_ENV配置文件里没有env.nodeESLint 报process is not defined。你明明知道 process 是 Node 自带的东西但 ESLint 不知道因为它没有你的“环境信息”。1.2 配置文件的五张职责清单顺着这个思路ESLint 配置文件的职责其实可以浓缩成五大类环境声明代码跑在浏览器、Node、还是浏览器NodeESLint 预设了 20 多种环境的全局变量比如browser、node、es6、mocha、jest。语法范围声明代码用到了 ES2020 的Optional Chaining还是 JSX还是 TypeScript这决定了解析器能把代码“读”到什么程度。规则聚合是全套采用业界推荐规范如airbnb、eslint:recommended还是自定义一套“土规矩”具体规则开关哪条规则报错、哪条规则只警告、哪条规则关掉带不带参数。全局变量白名单业务里偶发的globalThis.xxx、第三方注入的全局变量怎么让它不误报。你可以把 ESLint 配置文件想象成厨房的菜单顾客ESLint不知道自己能点什么菜厨房你的代码也不知道什么菜能做。配置文件就是那张写了“今日供应”的黑板——哪些食材有货、哪些做法禁用全写清楚大家才谈得拢。很多新手一上来就纠结“这条规则要不要开”这是本末倒置。先把这五类信息填对了ESLint 才是一个正常的“代码审阅者”而不是一个乱咬人的疯子。2. 格式选哪个、文件放哪里.eslintrc.js 为什么是默认答案2.1 六种写法优先级排第一的才有资格说话ESLint 配置文件的格式不是只有一种。常见的有格式文件名支持注释支持动态逻辑JavaScriptCommonJS.eslintrc.js/.eslintrc.cjs是是YAML.eslintrc.yaml/.eslintrc.yml是否JSON.eslintrc.json否否package.json 字段package.json中的eslintConfig否否这里有一个所有文档都会提、但很多人看完就忘的细节如果同一个目录下同时存在多个格式的配置文件ESLint 只会使用一个优先级顺序从上到下。.eslintrc.js排第一package.json里的eslintConfig排最后。为什么.eslintrc.js会成为默认答案因为它是 JavaScript 文件意味着你可以在里面写注释、写判断逻辑、按环境动态切换规则。比如// .eslintrc.js module.exports { rules: { no-console: process.env.NODE_ENV production ? error : warn, }, };这在 JSON 和 YAML 里是做不到的。YAML 虽然也能写注释但没法跑逻辑。JSON 最惨连注释都不支持。很多团队把.eslintrc.json用成了 JSONC里面塞满注释然后发现 ESLint 直接把整个配置静默忽略了——这个坑我后面细说。至于cjs后缀是因为如果你的项目配置了type: module.eslintrc.js会被 Node 当成 ESM 来解析如果此时用的还是module.exports启动就会报错。加上cjs后缀强制按 CommonJS 处理这是很多真实项目里不期而至的问题。2.2 向上一级逐目录查找ESLint 的“认亲”逻辑配置文件不一定要放在根目录。ESLint 有一套“逐级向上查找”的机制当你 lint 一个文件时它会从该文件所在目录开始依次向上找.eslintrc.*文件只要找到就停止。听起来很方便但这也是无数“配置不生效”的根源。举个例子你的项目结构是project/ ├── .eslintrc.js // 根配置 ├── src/ │ └── components/ │ └── Button.eslintrc.js // 子目录配置说不定是同事遗留的如果你在src/components/Button.js里写着不符合“根配置”的代码但 ESLint 检查时优先命中了Button.eslintrc.js这个子配置而它里面extends又不完整那根配置的部分规则就“覆盖”不了子目录的行为——这不是 ESLint 不听话而是它的查找逻辑决定了一切以更靠近文件的配置优先。更坑的是ESLint 默认会一路找到你的用户主目录。有的同学电脑的 home 目录下曾经装过某个 CLI 工具留下了一个.eslintrc结果整台电脑上的 JS 项目都在被那份陈年配置“照顾”。要终止这种向上寻亲方法是在项目根配置里写module.exports { root: true, // 到这一层为止不再向上找 };root: true是我见过的最该写而没写的配置之一。尤其是 monorepo 项目里子包没有自己的配置全靠根目录一份撑着一旦有人把root删了后面会发生什么你只能靠祈祷。2.3 flat config新项目该不该直接上车说到格式就绕不开 ESLint v9 开始主推的 flat config——就是你常看到的eslint.config.js。这不仅仅是文件名变了它的加载逻辑和之前完全不同.eslintrc时代配置文件“自动级联查找”多层配置自动合并规则冲突时“后写的覆盖先写的”。eslint.config.js时代配置文件不再自动合并所有配置都是导出一个数组每个元素是一个“配置块”你显式指定files匹配范围完全靠数组顺序控制优先级。一个典型的 flat config 长这样// eslint.config.js import js from eslint/js; export default [ { files: [**/*.js], ...js.configs.recommended, languageOptions: { ecmaVersion: latest, sourceType: module, }, rules: { no-unused-vars: warn, }, }, { files: [**/*.config.js], languageOptions: { globals: { process: readonly }, }, }, ];注意flat config 里已经没有了.eslintignore文件忽略逻辑直接在配置数组里用ignores字段定义。这就意味着你所有跟 lint 相关的行为都聚合在一个文件里非常干净。我的建议是新项目直接用 flat config老项目如果不是马上想动手术就别硬折腾。ESLint v9 里两种配置都支持但 v10 将会彻底移除.eslintrc的支持。你要是刚开一个新仓库没必要一出生就用注定要过时的写法。3. 逐字段拆解一份配置是怎么从声明变成规则的3.1 env先告诉 ESLint“代码活在什么环境里”env字段是配置文件里最直观、也最好理解的部分。它本质上是一组“预设全局变量白名单”。ESLint 官方提供了很多预置环境module.exports { env: { browser: true, // window、document、localStorage 等 node: true, // process、__dirname、global 等 es2021: true, // Promise、Proxy、Reflect 等 ES2021 内置对象 jest: true, // describe、it、expect 等 mocha: true, // describe、it、beforeEach 等 }, };这里一定要注意env.es2021的作用和parserOptions.ecmaVersion不是一回事。env.es2021主要注入可用的全局变量parserOptions.ecmaVersion决定解析器能解析的语法级别。写??空值合并运算符需要ecmaVersion: 2020用Promise.allSettled需要es2020这个全局变量环境。两件事都不做才能触发完整的报错。很多项目一上来就env: { browser: true }完事儿结果用到Axios时还好用到process就开始报错了。如果项目是 SSR 框架比如 Next.js、Nuxt你往往需要同时开browser: true和node: true。3.2 parserOptions 与 parserJSX、TS、Vue 的入场券parserOptions是告诉 ESLint“代码的语法难度”。默认情况下 ESLint 内置的解析器 Espree 只支持最新的标准 JavaScript 语法。你想用 JSX它默认解析不了。想用 TypeScript更不行。常见的配置是module.exports { parserOptions: { ecmaVersion: latest, // 解析器能识别多新的语法 sourceType: module, // 代码是 ECMAScript module允许 import/export ecmaFeatures: { jsx: true, // 允许解析 JSX }, }, };sourceType是个经常被忽略的字段。如果代码里用了import x from x但sourceType没设成moduleESLint 会把import语句当成语法错误报出来。反过来如果你写了use strict或 CommonJS 的require但sourceType设成了module且规则开得太死又会出现一堆“模块语法之外的 CJS 用法”报错。parser和parserOptions的区别是初级开发者最容易搞混的parserOptions是给现有解析器设置参数不换解析器。parser是直接换掉整个解析器。TypeScript 项目要装typescript-eslint/parsermodule.exports { parser: typescript-eslint/parser, plugins: [typescript-eslint], rules: { typescript-eslint/no-unused-vars: error, }, };Vue 3 项目则要装vue-eslint-parser并把它作为parser放在外层再把typescript-eslint/parser传给parserOptions.parsermodule.exports { parser: vue-eslint-parser, parserOptions: { parser: typescript-eslint/parser, ecmaVersion: latest, sourceType: module, }, extends: [plugin:vue/vue3-recommended], };这个写法第一次见到肯定懵为什么 parser 和 parserOptions.parser 都是 parser因为我需要先让vue-eslint-parser把.vue单文件组件的结构拆出来再把拆出来的script部分交给typescript-eslint/parser去解析 TypeScript。没有这个“二级解析”的链路Vue 文件里的类型语法根本没法检查。3.3 plugins 与 extends个人买卖 vs 全家桶plugins和extends的关系一句话就能说清plugins 是提供规则extends 是启用规则。plugins字段的作用是“把某个插件加载进来”相当于把这个插件开发的所有规则“上架”。但它不会自动开启任何一条规则就像超市把瑞士军刀摆上货架你买不买是另一回事。extends则是“直接帮你批量开启一堆规则”。它接收一个字符串数组每个字符串是一套规则集或者一份共享配置module.exports { extends: [ eslint:recommended, // ESLint 官方推荐规则 plugin:vue/vue3-essential, // 从 eslint-plugin-vue 里继承规则 plugin:typescript-eslint/recommended, prettier, // 必须是 eslint-config-prettier ], };注意字符串里的plugin:前缀。写plugin:vue/vue3-essential意味着“从eslint-plugin-vue插件的vue3-essential这个配置包中继承规则”。这种写法会自动加载对应的插件不需要你在plugins里再手动声明。但如果你的rules里想直接写vue/max-attributes-per-line这样的规则就必须在plugins字段里先声明vue否则 ESLint 不知道这条规则从哪里来。eslint:recommended是官方精选的一套“防止明显错误”的规则比如没有定义变量、有不必要的括号、case 里重复声明等。它覆盖面不大但足够兜底。我见过太多团队盲目extends: [airbnb]结果全组每天都在跟缩进和引号打架得罪了同事也降低了开发效率。对于大多数项目先以eslint:recommended 1~2 个框架相关的推荐规则集起步是更稳的选择。3.4 rules规则开关的具体写法与常见误会rules大概是配置文件里大家最熟悉、也最容易产生误解的字段。每条规则有三个档位off或 0关掉warn或 1警告不阻断构建error或 2报错可能导致构建失败一些规则还支持额外参数需要用数组写法module.exports { rules: { no-console: warn, eqeqeq: [error, always], max-len: [error, { code: 100, ignoreComments: true }], typescript-eslint/no-unused-vars: [error, { argsIgnorePattern: ^_ }], }, };argsIgnorePattern: ^_是我非常常用的一条配置让以_开头的函数参数不参加未使用变量的检查。这样你就可以光明正大地写function handler(_event, data) { ... }而不会被 no-unused-vars 烦。还有一个高频误解是extends里的规则和rules里的同名规则如果冲突rules里自己写的一定赢。因为extends本质上是“预先把规则放进配置里”而你本文件写的rules是后合并进来的后者覆盖前者。很多同学以为需要把 extends 里拉进来的规则逐个关掉其实只需要在rules里针对你关心的那几条重新定义即可。但这里有个隐蔽的问题如果extends后面又来了一个规则集比如先extends: [eslint:recommended, airbnb]那airbnb里的同名规则会覆盖recommended的同名规则。数组的排序就是覆盖顺序越靠后越优先。这也是 Prettier 相关配置必须放在extends数组最后一位的原因。eslint-config-prettier的作用就是关闭所有与 Prettier 冲突的格式类规则。它不放在最后前面随便一个规则集就把它的关闭操作又覆盖回去了冲突依然存在。这不是它没生效而是顺序在跟你作对。3.5 globals给“外来变量”办一张暂住证globals字段在最容易被忽略但它解决的痛点很实在。有些全局变量是第三方脚本运行时注入的不在代码里也不在 env 预设里。比如// 支付回调里window.somePaymentSdk 会被注入 if (somePaymentSdk) { somePaymentSdk.pay(); }如果不在配置文件里声明ESLint 会报somePaymentSdk is not defined。指向没有错但这种报错对业务代码没什么帮助反而打断检查节奏。解决办法module.exports { globals: { somePaymentSdk: readonly, myApp: writable, }, };readonly表示这个全局变量只能读不能改写代码时改它ESLint 会报错writable则允许重新赋值。这个字段和env的区别在于env是官方预制的“环境批量全局变量”globals是自定义的“临时白名单”两者可以同时存在互不冲突。不要滥用globals更不要一听到 no-undef 报错就先把变量扔进白名单。先想想是不是真的拿不到这个变量、是不是该换个引入方式。白名单里每进一个变量就少了一层对未知引用的防护。4. 覆盖、合并与继承规则冲突时到底听谁的4.1 配置叠加的顺序和合并逻辑ESLint 的配置文件是支持“叠加合并”的。一个文件的最终规则由一系列配置决策共同拼出来如果该文件所在目录或向上能找到多个.eslintrc.*离文件最近的目录配置优先级最高。每一份配置文件内部先应用extends中的规则集数组顺序决定覆盖优先级。再应用配置文件自身的rules自身rules覆盖extends引入的同名规则。同一规则集内若extends数组里有多个规则集冲突后者覆盖前者。这个逻辑很像 CSS 的层叠只不过 ESLint 的“层叠”是单向的越具体、越靠近文件的配置越有话语权。只要你记住“规则是叠加的不是二选一”大部分“配置不生效”的问题就少了一大半。用eslint --print-config可以直观看到最终合并结果npx eslint --print-config src/index.js resolved-config.json这个命令会输出该文件实际生效的完整配置包括所有 extends 展开后的 rules。我每次排查“我关了规则怎么还报错”时第一件事就是跑这个命令而不是盯着配置文件瞎猜。4.2 三种最常见的“配置不生效”第一种配置文件里有语法错误被静默忽略了。这主要发生在.eslintrc.json里写了注释的情况下。JSON 格式不允许注释一旦有人写了//或/* */ESLint 会解析失败。但解析失败的表现往往是“使用默认配置继续检查”而不是“崩溃报错”。你以为自己在用项目配置其实 ESLint 已经偷偷用内置默认值在跑了。所有自定义规则全部失效还找不到原因。第二种VSCode 的 ESLint 插件没有重载配置。插件默认只在你编辑文件时重新检查但如果你修改了配置文件本身有时候不会立即触发。你改了.eslintrc.js回到代码文件它还是按老规则标红。解决方法是 VSCode 命令面板里执行ESLint: Restart ESLint Server或者直接重载窗口。这个操作在执行任何配置调试之前先做能省下很多无效排查。第三种项目级配置和编辑器级配置打架。如果你在 VSCode 的用户设置里配置过eslint.options或者装了某些全家桶插件比如 Volar 内部的 ESLint 配置这些设置可能覆盖或干扰项目级的.eslintrc。判断标准是命令行执行npx eslint src/index.js的结果和编辑器里标红的结果不一。这时候优先相信命令行按命令行为准去对齐编辑器。4.3 override给不同目录搞“特殊政策”overrides是配置里一个比较高级的字段适合在单体项目里给不同文件类型或目录定制规则module.exports { rules: { no-unused-expressions: error, }, overrides: [ { files: [test/**/*.js], env: { mocha: true }, rules: { no-unused-expressions: off, }, }, ], };上面这段的含义是默认情况下no-unused-expressions是报错的但test目录下的文件不适用于这个检查测试里经常有expect(fn).to.not.throw()这种“带副作用的表达式”写法开了这个规则全是红波浪线。overrides还经常用来单独处理.ts文件和.vue文件或者在脚手架生成的目录里关闭特定规则。它本质上是一种“基于文件匹配的配置切换”比拆多个.eslintrc子文件更直观、更好维护。5. 团队协作中配置落地的几个真实教训5.1 先分清 ESLint 和 Prettier 的职责边界很多团队为什么配置越写越乱因为一开始就没有划分好 ESLint 和 Prettier 的工作界限。ESLint 应该管“代码质量”和“运行时隐患”未定义变量、未使用变量、重复定义、不可达代码、误用等。Prettier 应该管“代码格式”缩进、单双引号、行宽、结尾分号。要让 ESLint 不要插手格式领域你只需要在extends里加一条prettier来自eslint-config-prettier它会把所有可能和 Prettier 冲突的格式类规则一次性关掉。有个反例是团队里什么都要用 ESLint 管连换行和缩进都写进rules然后开发者之间因为格式化偏好天天吵架。实际上把这些交给 PrettierESLint 的配置文件瞬间瘦身一大半冲突面也小很多。5.2 版本漂移ESLint 8 的配置换到 9 为什么翻车我身边不止一个同事遇到过这种场景从老仓库复制一份.eslintrc.js到新项目装好依赖一跑一堆插件报错再看一眼ESLint 版本是 9而老配置是 8 时代的写法。ESLint 9 开始把 flat config 作为默认配置入口虽然还支持.eslintrc.*但很多老版插件生态已经逐步只适配 flat config 了。所以拿到别人的配置先看版本是否匹配。eslint --version查版本npm list eslint看本地依赖。如果你在维护一个工具库最好把 ESLint 版本和eslint-config-*、eslint-plugin-*都锁定在 package.json 里避免“今天跑是绿的、明天跑是红的”的版本漂移问题。5.3 提交前卡一道husky lint-staged 的落地配置配置文件的最终价值要落到“实际执行”上。只装 ESLint、不在流程里跑它的意义会大打折扣。一个比较轻量的做法是配合husky和lint-staged在提交代码前对暂存区文件做检查npm install --save-dev lint-staged husky然后在package.json里配置{ lint-staged: { **/*.{js,ts,vue}: [eslint --fix, prettier --write] } }初始化 huskynpx husky init在生成的.husky/pre-commit文件里写上npx lint-staged这样每次git commit时只会检查暂存区里跟 JS/TS/Vue 相关的文件。这样一来即使有同事平时不看编辑器里的标红提示提交动作也会被卡一道。还有一个细节eslint --fix能自动修的就自动修修不了的才报错阻止提交。这个体验比“全仓 lint 一下执行了半天报一堆错误”要好得多——至少它不是一次性把整条 CI 流程阻塞而是把问题控制在“你这次改的文件”里。5.4 我踩过的几个配置文件坑最后交代几个真实踩过的坑每一个都伴随了无故加班的教训。第一个.eslintrc.json里写了注释整个配置失效。当时项目跑起来明明有规则在生效但怎么改rules都不生效。后来用--print-config一查发现输出的是裸默认配置我才意识到 JSON 解析挂了但 ESLint 的选择是“沉默地退回默认状态”。此后我统一改用.eslintrc.js彻底根治。第二个改完配置VSCode 还标红我以为配置没生效。其实是 ESLint 插件的缓存服务器没重启多的是这种“我明明关了这条规则为啥还报错”的灵异事件。现在我的固定动作是改完配置文件执行一下ESLint: Restart ESLint Server。第三个extends顺序写反。那一次我把prettier放在了数组第一位后面跟着airbnb结果所有格式冲突全回来了单引号和分号在团队里引发了一轮“两种格式交替提交”的乱象。ESLint 对同类规则的覆盖逻辑是后者赢prettier 必须沉底。第四个没有用root: true。在某个公司老电脑上用户主目录里藏着一份上古.eslintrc导致新项目各种诡异报错有些规则项目里从来没配过但居然在生效。后来我养成了习惯任何项目的配置文件第一行先写root: true从根上切断和其他配置的串味。配置文件这东西写的时候克制一点比炫技重要得多。它能帮你把代码质量问题挡在门外但前提是你真的知道自己写了什么、顺序是什么、为什么这样写。下次遇到“配置怎么改都没反应”别急着加规则先eslint --print-config看看那份真正生效的配置长什么样九成以上的谜团在这里就能解开。