2023-11-08
ESLint 最佳实践

本文主要内容为阐述现在比较主流的 ESLint 配置方案。当然本文也有一定倾向性,主要是针对 TS 和 React 的生态。

独立的 eslint-config 包

无论是怎么样的项目统一风格是非常必要的;对于个人而言,名下所有的项目有统一的代码风格意义也类似。

所以我们通常需要将 ESLint 配置文件抽象成单独的包,可以作为 monorepo 的本地包使用或者发布到远程给到各个独立的工程使用,相对都很灵活。

由于我的项目都塞在一个由 rushjs 管理的大 monorepo 中,所以下文所有内容的都是在 monorepo 下进行的。

我们新建一个名为 eslint-config 的包,当然为了便于区分 monorepo 中的包名都会有一个 scope,比如我的包就会带上我的艺名叫 @july_cm/eslint-config

由于 ESLint 配置文件支持多种语言,且可以通过 extends 字段继承。

这里顺便列一下 extends 配置的几种做法:

// 数组中的每个配置项继承它前面的配置
"extends": [
  // eslint-config-standard
  "standard",
  // @standard/eslint-config
  "@standard",
  // eslint-plugin-react 中的 recommended 配置
  "plugin:react/recommended",
  // @typescript-eslint/eslint-plugin 中的 recommended 配置
  "plugin:@typescript-eslint/recommended",
  // 直接写路径也行
  "./node_modules/coding-standard/.eslintrc-es6"
]

创建一个名为 .eslintrc.base.js 作为基础配置包方便后续共享衍生出多种适用于不同场景的包,JavaScript 要比 JSON 等纯配置文件灵活太多,所以用 JavaScript。

梭哈的艺术之 @babel/eslint-parser

市面上有众多 ESLint 解析器,我们首选 babel 作为通用且默认的解析器(没错就是一把梭哈)。

我们希望 babel 能把一些js、jsx等通常的文件解决掉。

module.exports = {
  parser: '@babel/eslint-parser',
  parserOptions: {
    requireConfigFile: false,
    babelOptions: {
      babelrc: false,
      configFile: false,
      cwd: __dirname,
      presets: ['@babel/preset-env', '@babel/preset-react'],
    },
  },
  ignorePatterns: ['**/node_modules', '**/dist']
};

做到这步已经完成了百分之八十的工作,整个 eslint-config 已经可以 work 了。

高贵的 TS 之 @typescript-eslint/parser

简单来讲就是 babel 搞定不了 ts,咱们 typescript 有自己的解析器。

我们需要借助 ESLint config 中的 overrides 字段对 .ts 和 .tsx 文件做一些额外的解析

{
  overrides: [
    {
      files: ['*.ts', '*.tsx'],
      parser: '@typescript-eslint/parser',
      extends: [
        'plugin:@typescript-eslint/eslint-recommended',
        'plugin:@typescript-eslint/recommended',
        'plugin:prettier/recommended',
      ],
    },
  ],
}

上面的代码只是一个简单的示例,想要把 ESLint 规则配置的“称心如意”还得细致的看看每个插件的 rules 配置项。

代码风格必备之 Prettier

Prettier 的话大家应该也不陌生,官方对他的定义是这样的:

Prettier · Opinionated Code Formatter

有了 ESLint 为啥我们还需要 Prettier 呢?可以理解为对代码的格式化分为两个方面:

代码质量问题 and 代码风格问题

ESLint 主要处理的还是代码质量问题,比如 no-unused-varsprefer-promise-reject-errors 等等。其实把这些搞定了基本上 cover 了程序的潜在风险,奈何敲代码的都是强迫症,对于换不换行这样一个小问题都可以讨论半天。所以 Prettier 就站出来了说:“你们别吵了,都听我的”。大家听了纷纷点头。

但 ESLint 和 Prettier 的标准并非完全对齐的,所以很容易出现冲突导致大家左右为难。所以我们依然以 ESLint 作为主体,而 Prettier 以 ESLint 插件的形式入场。

我们使用 eslint-config-prettier 这个配置 ban 掉 ESLint 所有和 Prettier 有冲突的配置,也就是说 ESLint 的一部分职责被 Prettier 接管了。

{
	extends: ['prettier']
}

然后其实到此为止已经能用了,但我们还可以继续做些事情。我们可以引入 eslint-plugin-prettier 这个插件,它的主要作用是把 Prettier 推荐的格式问题的配置以 ESLint rules 的方式写入,这样可以统一报错来源,我们不再需要关系 Prettier 只需要知道 ESLint 有规则在报错即可。

{
  "plugins": ["prettier"],
  "rules": {
    "prettier/prettier": "error"
  }
}

如果觉得没问题,那我们还可以把上述两个步骤合起来,这也是官方推荐的配置

{
  extends: ["plugin:prettier/recommended"],
  rules: {
    'prettier/prettier': [
      'warn',
      {
        // 这里写 prettier 的配置
        // 不读取 prettier 配置文件,统一走 eslint 配置
        usePrettierrc: false,
      },
    ],
  },
}

当然别忘了我们需要安装相关的内容

pnpm i eslint-config-prettier eslint-plugin-prettier prettier -D

强迫症必备之 jsonc-eslint-parser

JSON 从来都不是 ESLint 的目标客户,因为 JSON 文件在项目中往往有多种作用。

它可能是某种 schema 或者是 i18n 文件甚至可能是数据文件,不方便一把去校验格式,一句话就是 JSON 的水很深 ESLint 把握不住。

但有一种 JSON 文件它非常规范,没错就是 package.json。乱七八糟的字段顺序和 dependencies 顺序足以逼死每一位强迫症。

如果我们能通过 ESLint 规范字段和依赖顺序,将大大降低协作时的代码冲突问题。

所以福音来了,我们在 overrides 数组末尾增加一项,专门检查和格式化 package.json

{
  overrides: [
    {
      files: ['package.json'],
      parser: 'jsonc-eslint-parser',
      extends: ['plugin:jsonc/base', 'plugin:jsonc/prettier'],
      rules: {
        'jsonc/sort-keys': [
          'warn',
          {
            pathPattern: '^$',
            order: ['name', 'version', 'author', 'scripts', 'dependencies', 'devDependencies'],
          },
          {
            pathPattern: '^(?:dev|peer|optional|bundled)?[Dd]ependencies$',
            order: { type: 'asc' },
          },
        ],
      },
    },
  ],
}

jsonc-eslint-parserjsonc/sort-keys 可以对 JSON 的字段属性做排序,上面的代码只是个简单的例子,不是完全体,可以按照 npmjs docs 的顺序把规则补充完整,然后你就会得到一个十分有序的 package.json

VSCode 配置

要让 validate 和 format 能力在 VSCode 中生效,还需要下载安装 VSCode ESLint Extension(其他 IDE 没用过不了解,非常卑鄙的无视)

{
  // 指定 eslint 路径,这样其他 package 就不需要再安装 eslint 了
  "eslint.nodePath": "packages/eslint-config/node_modules/eslint",
  "eslint.probe": [
    "javascript",
    "javascriptreact",
    "typescript",
    "typescriptreact"
  ],
  "eslint.validate": [
    "json"
  ],
  "editor.codeActionsOnSave": {
    "source.fixAll.eslint": true
  }
}

有一个要注意的坑,ESLint 插件的文档上说明了 eslint.probe 基本上是代替了 eslint.validate

但实际测试下来 json 配置在 probe 字段上是无效的,请配置在 validate 字段上,否则 jsonc-eslint-parser 什么都做不了。

需要注意的是当配置 eslint.validate: [”json”] 时,ESLint 插件会校验所有的 .json 文件,所以我们需要在 .eslintrc 文件中补充 ignorePatterns 字段:

{
	ignorePatterns: ['**/**.json', '!package.json']
}

它意为忽略所有的 .json 文件校验,但排除 package.json。同时还要注意该字段只能配置在顶层,不能放在 overrides 字段内哦。

配置使用

把写完的配置共享给其他包使用有多种方式,我个人比较推荐的方式是写成一个 defineConfig 函数,比较符合前端的直觉。

// 其他项目中的 .eslintrc.js 文件
const { defineConfig } = require('@july_cm/eslint-config');

module.exports = defineConfig({});

代价就是其他引用包也只能使用 JavaScript 作为配置文件语言,但这点代价相对于 JavaScript 灵活性带来的好处只不过是沧海一粟完全可以接受。