正儿八经来学一学 ESLint

624 阅读14分钟

本文正在参加「金石计划 . 瓜分6万现金大奖」

计算机科学中,lint是一种工具程序的名称,它用来标记源代码中,某些可疑的、不具结构性(可能造成bug)的段落。它是一种静态程序分析工具,最早适用于C语言,在UNIX平台上开发出来。后来它成为通用术语,可用于描述在任何一种计算机程序语言中,用来标记源代码中有疑义段落的工具。——来自维基百科

现状

ESLint 可能是开发中最常用的工具之一了,不过由于各种脚手架工具的盛行,虽然一定程度上提升了开发效率,但是却也将那些构建细节束之高阁。

如果此时公司需要你从零开始搭建一个完整的项目框架,为了规范团队的代码风格,ESLint 该如何去配置呢?本篇就来说说如何构建一个 ESLint + React 的实践,并且尝试去了解 ESLint 背后鲜为人知的细节,而对于其他框架,诸如 Vue 这种,构建的流程也是类似的。

重读 ESLint

先来看一张概念图来了解 ESLint 的组成:

无标题-2022-10-23-1252.png

  • 自顶向下来看,最上面就是 ESLint 的相关生态,比如 Prettier、Husky 等等工具。

  • 再下面一层就是 ESLint 的使用,分为使用配置文件,和使用命令行以及通过 NodeJS API 来使用,这个和其他的构建工具,比如 Webpack、Rollup 是一致的。

  • 接着就是 ESLint 的核心,规则以及插件,这一层很多人都知道它的重要性,但是却没有真正了解过。

  • 最底层就是 ESLint 处理代码的逻辑。Parser 解析器用来将 JS 代码解析成 AST,默认 ESLint 采用 ESPree 来解析 JS,也可以采用其他的解析器,但是必须符合 ESLint 的接口要求。如果是 TS 的代码,那么就需要能处理 TS 文件的解析器了。

  • Processor 是处理器,用来提取 JS 代码,或者对 JS 代码进行转换。一般处理器作为插件一个属性,暴露给外部使用。

对于开发者来说,配置、插件以及规则才是需要重点了解的,下面就来看看这些熟悉而又陌生的概念。

ESLint 配置文件

先说配置文件,其格式有下面几种,按照 ESLint 读取配置文件的优先级顺序排列:

image.png

ESLint 自动去项目下寻找 .eslintrc.* 这种格式的配置文件,如果要使用其他格式的配置文件,则可以使用 --config 选项来指定该文件的路径:

eslint --config myConfig.js ./src

除此之外,比较有趣的一点就是,我们知道在 json 文件中是不能有注释的,但是在 .eslintrc.json 文件中是可以写注释的,ESLint 会忽略该注释。需要注意的是,package.json 文件中还是不能写注释的

主要的配置文件接口类型如下:

interface ESLintConfig {
    root?: boolean; // 表示当前文件是否为根配置文件,用于多配置文件时
    parser?: string; // 解析器,默认为Espree,解析 AST 节点
    env?: Record<string, boolean>; // 代码运行环境
    parserOptions?: ParserOptions; // 解析配置
    extends?: string | string[]; // 继承其他的规则
    plugins?: string[]; // 插件配置
    rules?: any[]; // 规则配置
    processor: string; // 处理器,具体可看上一节的解释
    overrides?: any[]; // 针对不同文件,应用不同规则
    globals: Record<string, boolean | 'off' | 'readonly' | 'readable' | 'writable' | 'writeable'>; // 设置全局变量
    ignorePatterns: string | string[] // 忽略目录或文件,等同于使用.eslintignore
}

其中有几个选项是需要重点关注的。

首先 env 选项表示代码的运行环境,常用的如,浏览器和 NodeJS 以及 Jest 单元测试,这些环境中存在一些全局变量,比如 document、process 这种,如果使用这些变量,则需要配置 env 属性,否则使用它们 ESLint 会提示报错:该变量未定义。

除了配置 env 属性外,parserOptions 也是常用的配置,有几个属性需要关注一下。

ecmaVersion 属性 指定 JS 的版本,经过笔者反复尝试,发现这个属性没什么作用,想比较而言,env 属性中的配置更加重要。

sourceType 属性指定 JS 的模块类型,默认为 script 脚本,一般项目中则使用 module 值,表示模块化。

最后则是 ecmaFeatures 属性,其包含了 jsx 属性,用来启用 jsx。完整的配置类型如下:

interface ParserOptions {
    ecmaVersion?: 3 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 2015 | 2016 | 2017 | 2018 | 2019 | 2020 | 2021 | 2022 | "latest" | undefined;
    sourceType?: "script" | "module" | undefined;
    ecmaFeatures?: {
        globalReturn?: boolean | undefined;
        impliedStrict?: boolean | undefined;
        jsx?: boolean | undefined;
        experimentalObjectRestSpread?: boolean | undefined;
        [key: string]: any;
    } | undefined;
    [key: string]: any;
}

然后就是 extends 属性,经常会看到 extends 属性这么配置的:

{
    extends: ['eslint:recommended'],
    ...
}

这里的 recommended 就是在 ESLint 的规则中,定义的一个属性 recommended,值为 boolean 类型,为 true 表示 extends 会采用该规则。在下一节还会提到该属性。除了 recommended 外,还可以配置 all:

{
    exntends: ['eslint:all']
}

这样就是采用所有的 ESLint 中的规则,随着 ESLint 的版本迭代,某些规则可能会有兼容问题,所以这种做法是不推荐的。

其他配置比较简单,就不再一一赘述了,重点来看看如何实现规则和插件,它们之间又是什么关系呢?

ESLint 规则

ESLint 中的规则就是对书写代码做了一定的约束,如果不按照这个约束来,则 ESLint 中会报告这个错误,并且如果存在修复错误的方法,也可以自动修复该错误,比如当我们多加了空格,使用 --fix 的参数则可以修复该错误。

以 max-lines 规则为例,其大概的结构如下:

image.png

主要分为两个部分,meta 表示规则的元数据信息,create 则是规则验证的方法。

先从 meta 说起,type 表示规则的类型,其定义如下

type Type = 'problem' | 'suggestion' | 'layout'

按照问题的优先级顺序来理解,problem 表示不使用该规则,代码可能会导致错误,suggestion 则表示使用该规则会更好,layout 则是代码风格上的规则,比如空格、缩进等。

docs 的类型如下:

interface Docs {
    description: string; // 规则的描述信息
    recommended: boolean; // 是否推荐使用该规则,搭配eslint:recommended使用
    url: string; // 指定文档的url链接
}

接着 schema,则是配置规则的选项模式,ESLint 会根据这个模式来验证你填写的规则选项是否正确。ESLint 使用 ajv这个库来验证 schema。

最后 meta 中的 message 则表示不符合规则时的报错信息,搭配规则上下文对象 context.report 方法使用,展开 create 方法可以看到下面这句:

// ...省略
message: {
    exceed: "File has too many lines ({{actual}}). Maximum allowed is {{max}}."
}
// ...省略
create (context) {
    // ...省略
    return {
        "Program:exit"() {
                let max = 300;
                // ...省略
                if (lines.length > max) {
                    const loc = {
                        start: {
                            line: lines[max].lineNumber,
                            column: 0
                        },
                        end: {
                            line: sourceCode.lines.length,
                            column: sourceCode.lines[sourceCode.lines.length - 1].length
                        }
                    };

                    context.report({
                        loc,
                        // 指定对应哪条信息
                        messageId: "exceed",
                        // data 中的数据提供给上面 message 使用
                        data: {
                            max,
                            actual: lines.length
                        }
                    });
                }
            }
        };
    }
}

create 方法返回了一个对象,对象中的每个 key 都是 AST 中的节点,对节点做检查,发现不满足规则,使用 report 来报告错误。create 中的参数 context 是 ESLint 提供的上下文对象,其中包含了很多方法,具体可以查看 ESLint 的详细的文档

上面提到过,可以通过 --fix 来修复错误,这可以通过设置 report 中的参数来实现:

context.report({
    node: node,
    message: "Missing semicolon",
    fix: function(fixer) {
        return fixer.insertTextAfter(node, ";");
    }
});

ESLint 插件

说完了规则,来说说 ESLint 的另一个核心,插件。

ESLint 中的插件和其他库中的插件对比,比如 Vue 或者 Webpack 中的插件,后者的插件提供了扩展功能,而 ESLint 的插件更像是集成了上面所有配置的工具包,比如,可以在插件中定义规则 rules,定义解析器 processors,也可以定义 parserOptions 等等。

ESLint 的插件通常命名都是 eslint-plugin-* 这种格式,使用这种格式,在配置文件中设置 plugins 属性时,可以省略掉前缀的字符串。类似的还有 eslint-config-* 这样的 npm 包,比如 eslint-config-airbnb 这种,那么二者有什么区别呢?

先来看一个比较著名的插件 eslint-plugin-react ,它最后导出的模块结构时这样的:

module.exports = {
    rules: ...,
    configs: {
        recommended: ...,
        all: ...,
        'jsx-runtime': ...
    }
}

可以看到插件就是对外暴露了一些规则、配置对象。在 ESLint 中可以看到完整的插件属性如下:

interface Plugin {
    configs?: Record<string, ConfigData> | undefined;
    environments?: Record<string, Environment> | undefined;
    processors?: Record<string, Linter.Processor> | undefined;
    rules?: Record<string, ((...args: any[]) => any) | Rule.RuleModule> | undefined;
}

再来看看另外一个著名的 ESLint 的配置库 eslint-config-standard,里面的入口文件是这样的:

module.exports = require('./eslintrc.json');

这样,两者的区别就一目了然,如果是对于一些自定义的环境,比如说 Vue、React 这样的环境,使用了 .vue h或者 .jsx .tsx 这样的文件,ESLint 是无法识别的,所以要对这样的代码进行 Lint,就需要自定义环境,解析文件,规则等。而如果是使用这些框架,都是具体的业务代码,其实使用 eslint-config-* 更合适,其中包含这些环境下的 ESLint 插件。

那如何去构建一个 ESLint 的实践呢?

规范代码的实践

以一个 React + TypeScript 的项目为例,首先需要在 VSCode 中下载 ESLint 的插件,可以在写代码的时候,即时发现问题。

接下来,新建一个空项目,eslint-best-practice,采用 pnpm 来下载依赖,先安装 ESLint。然后新建一个 index.js 的文件。得到的目录如下:

image.png

安装好以后发现 VSCode 弹出了一个错误提示:

image.png

这里是因为 VSCode 中的 ESLint 的插件更新,以前的一些配置现在已经不支持了,在 VSCode 中找到 ESLint 的配置项,删掉关于其中相关配置就恢复正常了。

接着来写 .eslintrc.js 的配置文件,也可以用你喜欢的任意上面提到的格式:

module.exports = {
 env: {
     browser: true,
     node: true,
     es6: true,
     commonjs: true,
     jest: true
 },
 plugin: ['react'],
 parserOptions: {
     sourceType: 'module',
     ecmaFeatures: {
         jsx: true
     }
 },
 extends: [
     'eslint:recommended',
     'plugin:react/recommended',
     'plugin:prettier/recommended',
     'plugin:react-hooks/recommended'
 ]
}

这样一个基于 React 项目的 ESLint 便基本配置好了,唯一缺少的就是关于 TS 的代码 Lint。关于 TS 的 ESLint 的规则,目前使用的比较多的就是 @typescript-eslint/eslint-plugin 这个包。添加这个包的话,需要对配置文件做一些改动。

改动的原因,就是上面提到的 parser 解析器只能去解析 JS 文件,对于 TS 则无能为力,当然也可以选择先把 TS 转换成 JS,再去应用 JS 的规则,但是经过转换后的 JS 代码并不能实时提醒编写代码时出现的错误,所以最好的实践,就是添加这个包来单独解析 TS 文件。

基于上面的配置文件,改动如下:

module.exports = {
 env: {
     browser: true,
     node: true,
     es6: true,
     commonjs: true,
     jest: true
 },
 plugins: ['react', '@typescript-eslint/eslint-plugin'],
 extends: [
     'eslint:recommended',
     'plugin:react/recommended',
     'plugin:prettier/recommended',
     'plugin:react-hooks/recommended',
     'plugin:@typescript-eslint/eslint-plugin/recommended'
 ]
}

这么一看,好像更简单了。因为插件 @typescript-eslint/eslint-plugin 中已经包含了 parserOptions 的配置,所以这里就无需再去配置了,另外需要注意的是,除了安装这个插件,还需要安装 @typescript-eslint/parser 解析器的包。

大部分类型错误 TS 已经帮我们解决了,所以你可能不想应用这么多的 @typescript-eslint/eslint=plugin 中的规则,此时可以选择使用 overrides 配置,来对 TS、TSX 文件做单独的配置:

module.exports = {
    // ...省略
    overrides: [
        files: ['*.ts', '.tsx'],
        rules: {
            '@typescript-eslint/no-semi': 'off',
            // ...其他规则配置
        }
    ]
}

@typescript-eslint 中会有一些规则,需要配合 TS 的配置文件,否则就会出现如下的错误:

image.png

按照这个错误提示,添加 parserOptions.project 的配置即可让规则生效:

module.exports = {
    // ...省略其他
    parserOptions: {
        project: './tsconfig.json'
    }
}

综上可以知道,ESLint 的配置灵活多变,没有哪种配置可以一次配置到处使用的,需要根据自己的需要,配置如下几个关键属性,env、plugins、extends、parserOptions、overrides等,即可以达到适合自己或团队的最佳 ESLint 的配置。

其他工具

如果想要保证在前端团队中所有的代码风格一致,除了 ESLint 外,还需要一些其他工具搭配使用,才能更好的保证代码风格的一致性。

prettier 代码格式化

prettier 是一个和 ESLint 类似的工具,只是 prettier 更趋向于代码风格的规则:诸如缩进多少、使用tab、还是空格等等。他只是让代码保持一致的风格,并不能起到如 ESLint 这种发现错误、纠正错误的作用。

一般主要的配置项就是以下几项,至于具体使用哪种风格,还是要看自己的团队:

{
  "arrowParens": "always", // 箭头函数参数只有一个时,也需要添加括号
  "semi": true, // 语句分号
  "singleQuote": true, // 启用单引号
  "jsxSingleQuote": false, // 关闭jsx中使用单引号
  "printWidth": 100, // 每一行代码的长度,超出会换行
  "useTabs": false, // 不使用tab缩进
  "tabWidth": 2, // tab缩进为2个字符
  "trailingComma": "es5" // 对象、数组等结尾的逗号是否添加,es5是需要添加结尾逗号
}

style lint

另外项目中除了 JS/TS 文件外,还有大量的样式文件,同样也需要保证风格一致。对于样式文件,可以使用 stylelint 工具,它和 ESLint 的配置是类似的,只需要继承一些常用的配置即可:

{
  "extends": ["stylelint-config-standard", "stylelint-config-prettier"], // 常用的基础配置,这样够用了
  "customSyntax": "postcss-less", // 在代码中使用自定义的语法,比如less、scss、CSS in JS这种css语法验证
  "rules": {
    // ...按照团队的需要
  }
}

editorconfig

editorconfig 也是代码风格约束的工具,搭配 VSCode 的扩展插件 EditConfig for VSCode,主要是针对 IDE 工具,可以在编写代码的时候,就能保证一致良好的代码风格,而 prettier 这样的工具,是在执行命令以后才能格式化代码,不能在实时编写时,就保持一致的风格。这也是 editorconfig 和 prettier 的最大的差异。

该工具配置都是大同小异的,所以项目中用到的话,直接复制就可以了。在项目的根目录下新建一个 .editorconfig 文件,内容如下:

root = true

[*]
charset = utf-8
end_of_line = lf
indent_style = space
indent_size = 2
insert_final_newline = true
max_line_length = 100
trim_trailing_whitespace = true

配置很好理解,键值对形式的配置项,[*]表示对所有类型的文件都会应用这个配置,注意,需要下载了上面提到的扩展插件,才能在每次保存文件的时候自动格式化代码。

至此,一个风格一致的项目代码规范就实现了,最后可以添加一个脚本来执行代码 Lint:

prettier . --write && eslint . --fix && stylelint **/*.less

除了这些,也可以选择使用 Husky 这样的工具,在提交代码前进行格式化。

综上所述,这里使用了 ESLint + stylelint + prettier 来做代码检查,为了方便其他项目使用,可以利用 ESLint 的共享配置,将这些功能都集中到一个 npm 包中,像是上面提到的 eslint-config-standard 包。

可以创建一个如下结构的项目:

image.png

在 index.js 文件中导出 eslint 的配置:

module.exports = require('./eslintrc.js');

这样项目中的 ESLint 配置就可以直接继承该配置了。对于 stylelint、prettier 等则可以在项目中创建一个同名文件,文件中引入对应的配置文件即可,像这样:

module.exports = require('./styleintrc.js');
module.exports = require('./prettierrc.js');

别忘了最后在 package.json 文件中的 main 属性,添加 index.js 文件的路径。

以后无论是哪个项目都可以引用同一个 ESLint 以及其他的格式化配置规则,达到所有项目,代码风格一致,减少初级的代码错误的目的。

尾声

ESLint 还有很多没有深入的地方,比如整个 ESLint 的架构,分为 cli-engine,init 等等模块,任何一个模块深入研究下去都足够学习很久。

最后总结一下:虽然框架的飞速发展,给开发带来了极大的便利,但是也不要忘记隐藏在框架、规范背后的技术细节,这些细节才是拉开前端差距的关键。