从 .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 终于把配置系统做对了。迁移过程有一些学习成本,但换来的是更可预测、更安全的配置,值。

评论区

0 条评论

登录后可评论。

查看完整榜单
查看完整榜单
查看完整榜单