从 .eslintrc 到 eslint.config.js,我把团队 6 个项目全迁移到了 Flat Config,有几个坑得说
从 .eslintrc 到 eslint.config.js,我把团队 6 个项目全迁移到了 Flat Config,有几个坑得说
ESLint 9 出来有一阵了,最核心的变化就是扁平配置(Flat Config)成了默认。以前那个 .eslintrc 一套配置级联十层八层的日子,终于到头了。
我上个月把团队 6 个项目全从 ESLint 8 迁到了 9,顺带把配置全部换成 flat config 格式。踩了不少坑,写下来给准备动手的同行当个参考。
扁平配置到底扁在哪
先理清一件事:Flat Config 不是简单的语法变化,是整个配置系统的重构。
以前的 .eslintrc 有多乱?配置继承像套娃:
// .eslintrc.json(老)
{
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/recommended",
"plugin:react/recommended",
"plugin:react-hooks/recommended",
"prettier"
],
"plugins": ["@typescript-eslint", "react"],
"env": { "browser": true, "es2021": true },
"parserOptions": { "ecmaVersion": "latest", "sourceType": "module" },
"rules": { ... }
}
extends 链一层层嵌套,env 字段需要你记住所有环境变量,plugins 靠 npm 包名自动解析,parser 必须写字符串路径。出了问题想溯源,得一路追到 node_modules。
扁平配置就直白多了:
// eslint.config.js(新)
import js from '@eslint/js';
import tseslint from 'typescript-eslint';
import react from 'eslint-plugin-react';
export default [
js.configs.recommended,
...tseslint.configs.recommended,
{
files: ['**/*.{js,jsx,ts,tsx}'],
plugins: { react },
languageOptions: {
ecmaVersion: 'latest',
sourceType: 'module',
globals: { ...globals.browser },
},
rules: {
'react/jsx-uses-react': 'error',
},
},
{ ignores: ['dist/', 'node_modules/'] },
];
看到区别了吗?每个配置项都是一个独立的数组元素,文件匹配用 files 字段精确控制,插件直接是导入的对象变量,没任何隐式解析。
迁移第一步:该装什么包
ESLint 9 把核心逻辑拆得更细了,先搞清楚依赖关系。
# 核心
npm install -D eslint@^9
# ESLint 官方推荐规则包(替代 eslint:recommended)
npm install -D @eslint/js
# TypeScript 支持
npm install -D typescript-eslint
# 全局变量定义
npm install -D globals
有个细节:ESLint 9 不再内置 eslint:recommended,你需要从 @eslint/js 显式导入。typescript-eslint 也出了 v8 大版本,完全适配 flat config,推荐直接用最新的。
迁移中的大坑和小坑
坑一:env 字段没了
这是最大的心智转折。以前写 env: { browser: true } 就把 window、document 全认识了。Flat Config 里 env 不存在了,统一用 globals 包:
import globals from 'globals';
export default [
{
languageOptions: {
globals: {
...globals.browser,
...globals.node,
},
},
},
];
globals 包里自带所有标准环境的全局变量定义,按需组合就行。
坑二:parser 配置变了
以前 parser 写在 parserOptions 外面,通过字符串指定:
{
"parser": "@typescript-eslint/parser",
"parserOptions": { "project": "./tsconfig.json" }
}
扁平配置里 parser 是 languageOptions 的子字段:
import tseslint from 'typescript-eslint';
export default [
{
files: ['**/*.ts', '**/*.tsx'],
languageOptions: {
parser: tseslint.parser,
parserOptions: { project: './tsconfig.json' },
},
},
];
注意 parser 是直接传对象,不是字符串!这是最容易被 ESLint 9 版 type 定义卡住的地方。
坑三:plugin 不再自动解析
以前写 plugins: ['react'],ESLint 自动去 node_modules 找 eslint-plugin-react。新配置里 plugin 必须是显式导入的对象:
import react from 'eslint-plugin-react';
import hooks from 'eslint-plugin-react-hooks';
export default [
{
plugins: {
react,
'react-hooks': hooks, // 带连线的 key 要这样写
},
rules: {
'react/jsx-key': 'error',
'react-hooks/rules-of-hooks': 'error',
},
},
];
好处是 Plugin 的命名空间完全由你控制,不会有隐式注入的安全隐患。坏处是……真的要多写几行 import。
坑四:overrides 被 files 替代了
以前用 overrides 对不同文件单独配置规则:
{
"overrides": [
{
"files": ["*.test.js"],
"rules": { "no-unused-expressions": "off" }
}
]
}
扁平配置里直接加数组元素就行:
export default [
{
// 全局规则
rules: { 'no-unused-expressions': 'error' },
},
{
files: ['**/*.test.js', '**/*.spec.js'],
rules: { 'no-unused-expressions': 'off' },
},
];
更清晰了,每个配置对象只说自己的事。
坑五:ignores 需要单独成组
export default [
{ ignores: ['dist/', 'node_modules/', '*.config.js'] },
// ... 其他配置
];
注意 ignores 配置项如果和其他字段(如 rules)写在一起可能会有意料之外的覆盖行为。最佳实践是把 ignores 单独放数组最前面。
迁移工具帮你省一半功夫
ESLint 官方提供了一个迁移工具,能自动把老的 .eslintrc 转成扁平配置格式:
npx @eslint/migrate-config .eslintrc.json -o eslint.config.mjs
但注意,这个工具生成的配置只能当草稿。你得手动检查:
1. env 转 globals 是否正确(尤其是 node/browser 混合项目)
2. parser 是不是传了字符串(需要改成对象引用)
3. plugin 的 import 路径对不对
4. ignores 有没有被正常提取
我建议先生成样板,然后对着上面前几个坑一条条改。
一个完整的 React + TypeScript 配置模板
下面是我最终用下来的配置,可以直接复制改:
// eslint.config.mjs
import js from '@eslint/js';
import tseslint from 'typescript-eslint';
import react from 'eslint-plugin-react';
import reactHooks from 'eslint-plugin-react-hooks';
import globals from 'globals';
export default [
{ ignores: ['dist/', 'node_modules/', '*.config.*'] },
js.configs.recommended,
...tseslint.configs.recommended,
{
files: ['**/*.{js,jsx,ts,tsx}'],
plugins: { react, 'react-hooks': reactHooks },
languageOptions: {
ecmaVersion: 'latest',
sourceType: 'module',
globals: { ...globals.browser },
parser: tseslint.parser,
},
settings: {
react: { version: 'detect' },
},
rules: {
'react/jsx-key': 'warn',
'react-hooks/rules-of-hooks': 'error',
'react-hooks/exhaustive-deps': 'warn',
'@typescript-eslint/no-unused-vars': ['warn', {
argsIgnorePattern: '^_',
}],
'no-console': ['warn', { allow: ['warn', 'error'] }],
},
},
];
这个配置跑在我们三个前端项目上,实测 lint 速度比之前快了大概 20%(主要因为扁平配置的解析路径更简单,不再逐层回溯)。
性能也有惊喜
很多人没注意到,Flat Config 除了写起来爽,解析性能也有提升。
我拿公司一个中等体量的项目(500+ 文件)测了一下:
- ESLint 8 + .eslintrc:首次 lint 耗时 12.3s
- ESLint 9 + flat config:首次 lint 耗时 9.8s
快约 20%,主要因为:
1. 不再需要级联查找配置文件,减少了文件系统 IO
2. 配置对象预编译,减少了运行时解析
3. 不用每次 resolve plugin 的 npm 路径
CI 上感受尤其明显,每个 PR 的 lint 步骤能省 2-3 秒。
什么时候可以迁
如果你现在用的是 ESLint 8 + .eslintrc,我建议可以动手了:
- 新项目:直接上 ESLint 9 + flat config,没理由走回头路
- 老项目:如果只有几个规则配置,迁移成本很低;如果有复杂的 extends 链加几十个插件,建议先用迁移工具生成草稿,再手动打磨
- ESLint 10 已发布:目前 ESLint v10 已经出来,依旧延续 flat config 体系,所以先迁到 v9 稳一稳很合理
团队规模大的话,建议分批迁移,从最独立的子项目开始,积累经验再铺开。
一句话总结:Flat Config 是一次迟到了十年的清理,ESLint 终于把配置系统做对了。迁移过程有一些学习成本,但换来的是更可预测、更安全的配置,值。
评论区
登录后可评论。












