cover_image

ESlint 过去、现在、未来

查杨 大转转FE
2025年01月17日 01:00

ESlint 过去、现在、未来

前言

2023年10月26日,ESlint的作者发布了一篇公告,宣布将在未来的ESlint版本中逐渐弃用核心格式化规则,建议用户使用其他的formatter工具,而ESlint也将回归成为更加纯粹的lint工具。

2024年4月5日,ESlint9正式发布,ESlint发布了一个很重要的内容:Flat Config 扁平化配置。

过去,ESlint仅仅是一个用于维护代码质量的一个工具,但是随着Flat Config的发布,以及由于官方放弃格式化规则带来的社区自由发展,ESlint在做好一个维护代码质量的工具的同时,也具有更多的可能性。

让我们来看一下ESlint的Flat Config的发展时间线

图片
Flat Config的发展时间线

可以看到,Flat Config这个方案早在2019年就发布了意见征求稿。而纵观2019-2024这长达五年的时间跨度上,ESlint团队似乎一直在为Flat Config的推出做工作(实际上,从官方的Blog中也能发现这点),这让我不禁好奇,到底是什么样的痛点,让ESlint团队可以让ESlint为Flat Config的更新布局谋划这么长时间。这在Flat Config的介绍文章中也许能找到答案。ESLint's new config system, Part 1: Background[1]

推出Flat Config的前因后果

问题一、extends属性

The first significant change to eslintrc was with the introduction of the extends key. The extends key, borrowed lovingly from JSHint, allowed users to import another configuration and then augment it

第一个痛点就是配置文件中的 extends属性,extends属性设置的初衷是参考JSHint,为了让用户可以更加方便的导入另一个配置,并进行自定义的修改或者增强。例如

{
  "extends": ['./.other-config.json'],
  "rules": {
    "semi""warn"
  }
}

extends的使用本身是好的,让用户可以基于npm分享一些自己配置的规则集,让ESlint的更加的方便快捷。但是简单的使用extends导入其他的配置带来了一系列的规则级联上的问题,这让eslint团队苦不堪言。

而对于使用者来说,extends关键字在底层的逻辑是通过require导入其他的包实现的规则拓展,在配置文件中仅仅是几个字符串,这需要使用者花费额外的心智去了解自己配置的这些内容都在哪个位置,并通过寻找node_modules文件夹,或者查阅相关文档才能知道究竟配置了哪些规则。

问题二、多格式支持

As part of a refactor, we discovered that it would be trivial to allow different config file formats. Instead of forcing everyone to use a nonstandard .eslintrc file, we could formalize the JSON format as .eslintrc.json and also add support for YAML (.eslintrc.yml or .eslintrc.yaml) and JavaScript (.eslintrc.js). For backwards compatibility we continued to support .eslintrc because it was a trivial amount of code to keep around.

eslint的配置文件是支持多种格式的。本意是想增加兼容性,但是添加JS格式的配置文件导致他与非JS格式之间出现了一些不兼容的情况。

问题三、可共享的配置和依赖项

前面提到,extends的底层实现是使用require()引入对应的包。这就导致在 npm v3 之前,ESLint 建议将插件作为 peer dependencies而不是 dependencies来包含在可共享配置中,才能让 require() 正确解析对应的依赖包。但是npm在v3版本中停止了对 peer dependencies自动安装的功能,导致出现了依赖于此行为的所有共享配置都出现了异常。尽管后面npm恢复了自动安装 peer dependencies,但是对eslint的社区伤害已经造成,且难以挽回

问题四、overrides也支持extends

本身overrides的推出是解决extends级联问题的最好时机,但是官方并没有这么做,并且后续还在overrides中添加了额外的extends属性,这种逻辑的堆叠对于配置使用方,和eslint维护者而言都增加了复杂度,造成了很大的负担。

Flat Config的配置方案

在上一节的内容中,我们已经对ESlint早期版本的一些痛点有了初步的了解,那么Flat Config方案是如何处理这些痛点的?

配置文件的对比

在eslint9中,eslint的配置文件仅支持js文件格式,彻底解决了不同文件格式带来的兼容问题,以下是两种配置方式的案例

  • 早期配置文件:.eslintrc.js
  module.exports = {
    env: {
      browsertrue,
      es2021true,
      nodetrue
    },
    extends: ['eslint:recommended'],
    overrides: [
      {
        files: ["**/*.{js,jsx,ts,tsx}"],
        plugins: ["react""jsx-a11y"],
        extends: ["plugin:react/recommended"],
      },
    ],
    parser'@typescript-eslint/parser',
    parserOptions: {
      ecmaVersion'latest',
      sourceType'module'
    },
    plugins: ['react''@typescript-eslint''react-hooks'],
    rules: {
      '@typescript-eslint/no-var-requires'0,
    }
  };
  • Flat Config:eslint.config.js
  import js from '@eslint/js'
  import globals from 'globals'
  import reactHooks from 'eslint-plugin-react-hooks'
  import reactRefresh from 'eslint-plugin-react-refresh'
  import tseslint from 'typescript-eslint'
  
  export default tseslint.config(
    { ignores: ['dist'] },
    {
      extends: [js.configs.recommended, ...tseslint.configs.recommended],
      files: ['**/*.{ts,tsx}'],
      languageOptions: {
        ecmaVersion2020,
        globals: globals.browser,
      },
      plugins: {
        'react-hooks': reactHooks,
        'react-refresh': reactRefresh,
      },
      rules: {
        ...reactHooks.configs.recommended.rules,
        'react-refresh/only-export-components': [
          'warn',
          { allowConstantExporttrue },
        ],
      },
    },
  )

extends的处理

在旧版本的配置中,extends使用字符串的方式隐式地引入插件,在eslint的内部使用required加载内容。在新版本的配置中,extends拓展配置时,需要先将待拓展的插件以ESM的import的方式显式地导入,然后在extends中进行配置拓。

对于使用者而言,他们能更加清晰、便捷的找到自己拓展的依赖在node_modules中的源码,方便查看一些配置的规则,简化了规则的理解。对于eslint团队而言,显式地导入插件也能解决配置依赖的问题。可谓一举多得。

plugins的处理

在旧版本的配置中,plugins主要是一个数组的形式,通过输入插件的名称,在eslint内部确定需要加载的插件,最终实现插件的引入。这种方式本身没有太多的问题,但是由于pluginsrules往往是有联动的。比如:

module.exports = {
 //...
  plugins: [ '@typescript-eslint'],
  rules: {
    '@typescript-eslint/no-var-requires''off',
  }
}

这代表着no-var-requires这个自定义的rule是属于@typescript-eslint这个插件的。在某些场景下,插件的作者可能期望修改自己插件的名称,或者用户使用另一个拥有相同规则的插件,比如将@typescript-eslint修改为ts-eslint。此时配置文件就需要修改为

module.exports = {
 //...
  plugins: [ 'ts-eslint'],
  rules: {
    'ts-eslint/no-var-requires''off',
  }
}

如果配置文件中的规则过长,更改插件名字后需要重新配置的内容将会非常多,增加使用者负担。

因此在flat config中,plugins属性被定义为一个对象,key是可自定义的插件的名字,value则是显式导入进来的插件代码。这么做的好处在于提供了配置者自定义插件名称的功能。

// 原配置
export default [
  {
     plugins: {
      'react-refresh': reactRefresh,
    },
    rules: {
      'react-refresh/only-export-components''off'
    },
  }
]
// 改名后
export default [
  {
     plugins: {
      'refresh': reactRefresh,
    },
    rules: {
      'refresh/only-export-components''off'
    },
  }
]

基于Flat Config的新生态

Config Inspector

前面提到,在eslint中,由于可以便捷地拓展他人的配置,导致配置者在某些情况下自己都不清楚到底配置了什么内容,需要按照extends的内容分别去查看他们的源码或者查询文档,并且每一条规则适用与什么场景也并不了解。举个经常出现的例子。no-unused-vars这个规则,在eslint的基本规则和eslint-typescript插件中都有使用,我们可能期望禁用这条规则

{
  rules: {
    'no-unused-vars''off'
  }
}

此时我们再看编辑器。会发现出现了eslint-typescript发出的no-unused-vars的异常,这一定程度上拉低了开发效率。

所以在eslint9中。eslint官方采取了开源大佬Anthony Fu制作的Config Inspector,通过收集规则,在本地起一个nuxt服务来展示具体配置的规则。在Configs这个Tab中可以通过输入文件的路径+名称来确定应用到这个文件上有哪些规则,非常的清晰明确。

图片
Config Inspector

工厂函数方案

工厂函数的配置方案,在非Flat Config场景下也适用,而Flat Config的发布让工厂函数配置方案具有更高的灵活度。

首先我们有一个共识,不论什么技术栈,本质还是js、ts、css这些内容,在eslint的规则上有很大一部分雷同的内容。因此很容易就想到可以通过工厂函数,组合不同的框架、不同语言的对应的eslint规则,最终返回一个配置对象,实现配置工厂化。但是实际操作起来,在配置的可拓展性上存在一定的缺陷。

这时候就可以用到eslint-flat-config-utils[2]这个工厂化配置工具。他提供的composer工具支持链式调用配置,在需要拓展时,可以在.append中配置自定义的一些内容,保证工厂化的同时也提供了相当大的灵活性。

// eslint.config.mjs
import { composer } from 'eslint-flat-config-utils'

export default composer(
  {
    plugins: {},
    rules: {},
  }
  // ...some configs, accepts same arguments as `concat`
)
  .append(
    // appends more configs at the end, accepts same arguments as `concat`
  )
  .prepend(
    // prepends more configs at the beginning, accepts same arguments as `concat`
  )
  .insertAfter(
    'config-name'// specify the name of the target config, or index
    // insert more configs after the target, accepts same arguments as `concat`
  )
  .renamePlugins({
    // rename plugins
    'old-name''new-name',
    // for example, rename `n` from `eslint-plugin-n` to more a explicit prefix `node`
    'n''node'
    // applies to all plugins and rules in the configs
  })
  .override(
    'config-name'// specify the name of the target config, or index
    {
      // merge with the target config
      rules: {
        'no-console''off'
      },
    }
  )

ESLint Stylistic[3]

eslint是否应该具有格式化功能,这是一个在社区迟迟没有讨论出结果的问题。对于大对数人而言,eslint + prettier + stylelint的方案似乎已经是一种最佳实践了。但是在笔者看来,一个项目配置三套规则,在任何时候都是一种负担,prettier的“读取和重新输出”的方法会丢弃源代码中的所有风格信息,比较典型的一个点就是printWidth的强制换行。因此,笔者个人更推荐仅使用eslint一套规则通吃的配置方案,简化配置、减轻负担。

我在前言中也提到,eslint官方将在9.0级以后版本中逐渐弃用核心格式化规则,但是社区对eslint拥有格式化功能的需求并未减少,于是社区成员就将eslint中的格式化规则进行了单独的移植、封装、拓展,发布了ESLint Stylistic。

他总共包含了三个迁移插件和一个额外引入的补充规则插件

  • eslint -> @stylistic/eslint-plugin-js  eslint -> @stylistic/eslint-plugin-js

    • 迁移的JavaScript风格的规则
  • @typescript-eslint/eslint-plugin -> @stylistic/eslint-plugin-ts  @typescript-eslint/eslint-plugin -> @stylistic/eslint-plugin-ts

    • TypeScript 的风格规则
  • eslint-plugin-react -> @stylistic/eslint-plugin-jsx  eslint-plugin-react -> @stylistic/eslint-plugin-jsx

    • 框架无关的 JSX 的风格规则
  • @stylistic/eslint-plugin-plus

    • ESLint Stylistic 引入的补充规则

在配置上,和其他的eslint插件配置思路相同,以下是示例代码

// eslint.config.js
import stylistic from '@stylistic/eslint-plugin'

export default [
  stylistic.configs.customize({
    // the following options are the default values
    indent2,
    quotes'single',
    semifalse,
    jsxtrue,
    // ...
  }),
  // ...your other config items
]

配置的TypeScript提示

Flat Config的显式导入修改而形成的完整的上下文,让ESlint生成类型提示成为了可能,这就是eslint-typegen[4] 他只需要在eslint配置外面添加一个typegen函数,他就能从规则中自动推断生成类型,并为规则选项提供自动补全和类型检查。

/// <reference path="./eslint-typegen.d.ts" />
import typegen from 'eslint-typegen'

export default typegen(
  [
    // ...your normal eslint flat config
  ]
)
图片
eslint-typegen

ESlint的未来

ESlint9发布至今,选择主动升级eslint的大型开源项目仍然是少数(比如react),而ESlint放弃formatter的规则似乎也已经做好了完全成为一个优秀的Linter工具的准备。但是对于社区,ESlint却不止于此,ESlint拥有强大的IDE插件,以及优秀的cli工具,与其将ESlint作为一个纯粹的Linter工具,不如打破桎梏,发觉一些新的可能。

那么ESlint可以是什么?

更多语言通用的linter+formatter

ESLint now officially supports linting of JSON and Markdown[5]

Taking our first steps towards providing a language-agnostic platform for source code linting.

ESlint目前已经添加了对markdown和json的支持,文章的副标题叫做“迈出第一步,提供一个与语言无关的源代码静态分析平台。”,可以见得ESlint未来对多语言支持的一个发展方向。而社区又拥有ESlint Stylistic这类格式化工具,在未来ESlint或将成为多语言通用的代码规范和格式化工具。

图片
eslint宣布支持markdown和json

更强大的AST分析处理工具

ESlint的本质是对代码的AST分析,同时,由于其完善的ide插件体系和cli命令工具体系,结合其AST分析的底层逻辑,ESlint或许在AST处理上有更多的可能性。

比如 ESLint Plugin Command[6],利用注释命令触发对应的eslint规则,达到想要的修复目的

图片
ESLint Plugin Command

总结

随着时代的发展和技术的演变,ESlint从最初作为比JSHint更灵活的Linter工具,逐步演变成支持多种语言的静态分析平台。这不仅体现了ESlint对时代需求的适应,更推动了整个开发生态的进步。通过彻底推行Flat Config方案,ESlint不仅解决了长期存在的问题,还为自己开辟了一条更加光明的未来道路。

回顾ESlint的发展历程,我们可以清晰地看到开源社区的强大动力和影响力,这正是所有开源贡献者最希望看到的结果。展望未来,随着ESlint对多语言的深入支持和社区的持续推动,我们有充分的理由相信,ESlint的未来将是一条充满希望和机遇的康庄大道。

Reference
[1]

ESLint's new config system, Part 1: Background: https://eslint.org/blog/2022/08/new-config-system-part-1/

[2]

eslint-flat-config-utils: https://github.com/antfu/eslint-flat-config-utils

[3]

ESLint Stylistic: https://eslint.style/

[4]

eslint-typegen: https://github.com/antfu/eslint-typegen

[5]

ESLint now officially supports linting of JSON and Markdown: https://eslint.org/blog/2024/10/eslint-json-markdown-support/

[6]

ESLint Plugin Command: https://eslint-plugin-command.antfu.me/



想了解更多转转公司的业务实践,点击关注下方的公众号吧!


前端 · 目录
上一篇【1.7 万字】破解代码质量密码:可维护性技术深度解析下一篇亲测好用!揭秘这款前端开发提效神器
继续滑动看下一个
大转转FE
向上滑动看下一个