接手一个跑了几年的老项目,根目录躺着一份两百多行的 .eslintrc.json,env、parserOptions、extends、rules 一字排开,谁也不敢动。想加一条规则,不确定会不会被 extends 里的某个包覆盖掉;想让 test/ 目录松一点,又不知道该写在哪一层。
ESLint 是一个 JavaScript 静态检查工具,它能帮你养成良好的编程习惯。但要让它真的听话,得先看懂它的配置文件。这篇就把这个文件拆开,一个字段一个字段说清楚它管什么、什么时候用得上、写错了会出什么症状。那份带中文注释的完整规则清单我一条没删,只是按类别拆成了几段,每段前面补上背景。最后再补一节 ESLint 9 扁平配置的对应写法,老项目和新项目都能对上号。
在本篇文章中,我们将从浅入深,和大家一起学习以下知识:
- ESLint 到底去哪里找配置文件,
root: true这一行在挡什么 env和globals的分工,以及为什么 ESLint 9 里env消失了parserOptions的几个子字段分别影响解析的哪一步plugins和extends的关系,插件装了不等于规则开了rules的严重度写法、参数写法,以及一份逐条注释的完整规则清单overrides怎么给不同目录配不同规则- 多份配置叠在一起时,到底谁覆盖谁
- 行内注释临时关掉校验的三种写法
- 上面这些字段在 ESLint 9 扁平配置里对应成了什么
# 一、ESLint 先得找到配置文件
写规则之前,先搞清楚 ESLint 是怎么找到这个文件的。这一步不清楚,后面所有的「我明明改了但没生效」都没法排查。
.eslintrc.* 这套体系(下面统称 eslintrc)用的是级联查找。ESLint 拿到一个待检查的文件,从它所在的目录开始往上翻,每一层只要有配置文件就读进来,一直翻到某一层写了 root: true,或者翻到文件系统根目录为止。翻到的所有配置自上而下合并,离被检查文件越近的那份优先级越高。
同一个目录里放了多种格式的配置文件时,ESLint 只取第一个,其余的直接忽略。顺序是固定的。
.eslintrc.js
.eslintrc.cjs
.eslintrc.yaml
.eslintrc.yml
.eslintrc.json
package.json 里的 eslintConfig 字段
这里有个坑要注意。项目里同时存在 .eslintrc.js 和 .eslintrc.json 时,.json 那份是完全不生效的,而且没有任何警告。我见过好几次「改了半天规则纹丝不动」,最后发现是在改一份根本没被读的文件。
root: true 的作用是「到我为止,别再往上翻了」。这一行在 monorepo 里几乎是必写的,否则子包的 lint 会一路读到仓库根目录甚至用户主目录下的配置,行为在不同人的机器上还不一样。
还有一个独立于配置文件的 .eslintignore,语法和 .gitignore 一致,用来把 dist/、node_modules/、生成代码排除掉。它和 .eslintrc 是两个文件,很多人以为忽略规则也写在配置里,结果构建产物被一起 lint,报出几万条错误。
先记住这两点,因为待会儿讲 ESLint 9 的时候,这两条恰好都变了。
# 二、env 声明代码跑在什么环境里
env 解决的是一个很具体的问题:ESLint 不知道 window 和 process 是什么。
它只是个语法分析器,看到一个没声明过的标识符,no-undef 规则就会报 'window' is not defined。env 做的事情就是告诉 ESLint「这份代码跑在浏览器里,浏览器那一整套全局变量都是合法的」,ESLint 内部维护着这些环境对应的全局变量表,打开开关就把整张表塞进作用域。
// .eslintrc.json 片段
{
// 环境定义了预定义的全局变量。
"env": {
//环境定义了预定义的全局变量。更多在官网查看
"browser": true,
"node": true,
"commonjs": true,
"amd": true,
"es6": true,
"mocha": true
}
}
这几个开关分别对应什么,值得逐条对一下。browser 给的是 window、document、navigator 这类;node 给的是 process、__dirname、Buffer、require;commonjs 是给 Browserify、webpack 这种在浏览器里用 CommonJS 打包的场景准备的;amd 对应 define 和 require 的 AMD 形式;mocha 给的是 describe、it、before 这些测试全局函数。
es6 这一项要单独说,它不只是加全局变量。打开 es6 会同时把 Promise、Map、Set、Symbol 这些内置对象加进来,并且自动把 parserOptions.ecmaVersion 设成 6。所以你会看到很多老配置里 es6: true 和 ecmaVersion: 6 同时写着,其实后者是冗余的。
我自己的习惯是不要把 browser 和 node 一起全开。全开确实不报错了,代价是你在浏览器代码里写 process.env.XXX 也不会被拦住,直到运行时才炸。更好的做法是根目录只开 browser,用后面讲的 overrides 给构建脚本单独开 node。
# 三、globals 补 env 覆盖不到的全局变量
env 给的是成套的预设,globals 给的是零散的补充。CDN 引进来的 jQuery、后端模板注入的 __INITIAL_STATE__、老项目里挂在 window 上的那些全局对象,都归它管。
// .eslintrc.json 片段
{
"globals": {
// true 表示允许重新赋值,false 表示只读
"jQuery": false,
"$": false,
"__INITIAL_STATE__": true
}
}
值的含义容易记反。false(或者写成 "readonly")是只读,代码里给它重新赋值会被 no-global-assign 拦住;true("writable")才是允许改写。绝大多数场景应该写 false,第三方库的全局对象没有理由被业务代码覆盖。
如果只有单个文件用到某个全局变量,没必要写进配置,直接在文件顶部用注释声明就行。
/* global __INITIAL_STATE__ */
console.log(__INITIAL_STATE__)
这个写法和前面 rules 里 no-undef 那条注释说的是同一件事,「禁用未声明的变量,除非它们在 /*global */ 注释中被提到」。
# 四、parserOptions 决定代码怎么被解析
前面两个字段管的是「有哪些变量」,parserOptions 管的是「这段代码按什么语法解析」。解析这一步失败,后面所有规则都跑不起来,报错通常是 Parsing error: Unexpected token。
// .eslintrc.json 片段
{
// JavaScript 语言选项
"parserOptions": {
// ECMAScript 版本
"ecmaVersion": 6,
"sourceType": "module", //设置为 "script" (默认) 或 "module"(如果你的代码是 ECMAScript 模块)。
//想使用的额外的语言特性:
"ecmaFeatures": {
// 允许在全局作用域下使用 return 语句
"globalReturn": true,
// 全局使用严格模式(impliedStrict)
"impliedStrict": true,
// 启用 JSX
"jsx": true,
"modules": true
}
}
}
ecmaVersion 现在一般直接写 2022、2023 这种年份形式,或者写 "latest" 让它跟着 ESLint 版本走。写 6 的问题是可选链 ?.、空值合并 ?? 这些后来的语法会直接解析失败。老配置里留着 6 是因为当年就到这儿,新项目没必要跟。
sourceType 只有两个值有实际区别。写 "module" 时 ESLint 按 ES Module 解析,import/export 可用,并且代码默认处于严格模式;写 "script" 时按传统脚本解析,顶层 this 指向全局对象。这一项配错的典型症状是 import 那一行报 Unexpected token import。
ecmaFeatures 里几个子项都比较边缘。globalReturn 允许顶层 return,这是 CommonJS 模块的历史遗留;impliedStrict 让所有代码都按严格模式解析,等价于每个文件顶部都有 'use strict';jsx 是让内置解析器认识 JSX 语法。
这里要澄清一个常见误解。ecmaFeatures.jsx 只让 ESLint 能把 JSX 解析成语法树,它不提供任何 React 相关的检查规则,那是 eslint-plugin-react 的活。另外 modules 这一项在现在的 ESLint 里已经不需要了,用 sourceType: "module" 就够,老配置里带着它不会报错,纯属冗余。
还有一个和 parserOptions 平级的 parser 字段,这份配置里没写,但实际项目基本绕不开。内置解析器叫 espree,只认标准 JavaScript;写 TypeScript 要换成 @typescript-eslint/parser,写 Vue 单文件组件要换成 vue-eslint-parser。换解析器之后 parserOptions 里往往还要额外配 project 指向 tsconfig.json,那些依赖类型信息的规则才能跑。
# 五、plugins 和 extends 是两件事
这两个字段挨得近,作用完全不同,混淆的人非常多。
// .eslintrc.json 片段
{
//-----让eslint支持 JSX start
"plugins": [
"react"
],
"extends": [
"eslint:recommended",
"plugin:react/recommended"
]
//-----让eslint支持 JSX end
}
plugins 只做一件事,把插件里的规则注册进来,让你可以在 rules 里写 react/jsx-uses-vars 这种带前缀的规则名。注册不等于开启,光写 plugins: ["react"] 而不配任何规则,lint 结果一条都不会变。这是新手最容易懵的地方,装了插件却发现什么都没检查出来。
extends 才是「批量开规则」。它接受三类值:"eslint:recommended" 是 ESLint 内置的推荐集;"plugin:react/recommended" 是插件自己导出的推荐集,写法是 plugin: 加插件名加配置名;直接写包名(比如 "airbnb")则对应 eslint-config-airbnb 这个独立的配置包。
注意第二种写法里插件名可以省略 eslint-plugin- 前缀,react 会被解析成 eslint-plugin-react。这套省略约定在 monorepo 里经常出问题,因为 ESLint 是按自己的路径规则去 node_modules 里找包的,pnpm 那种非扁平的 node_modules 结构下解析失败是家常便饭。