二、js/ts 代码规范的基础——ESLint

794 阅读10分钟

1. ESLint 简介

关于 ESLint,官方网站 用一句话概括了它的作用:

  • Find and fix problems in your JavaScript code. (查找并修复存在于你的 JavaScript 代码中的问题)

ESLint 中文网 是这么介绍它的:

  • ESLint:可组装的 JavaScript 和 JSX 检查工具

一句话介绍 ESLint:

  • ESLint 是一个插件化的、基于规则的 JavaScript/Typescript/JSX/TSX 代码检查与部分问题修复的工具

ESLint 最大的作用就是规范化你的 js/ts/jsx/tsx 代码。

注:以前有专门用于规范化 ts 代码的工具叫作 TSLint,现在 TSLint 已经不再维护,ts 代码规范化工具被统一到了 ESLint 中

虽然不同项目对 ESLint 配置有不同的需求,但集成的过程以及使用的过程都是完全一致的。

在使用 ESLint 之前,推荐在 IDE 中安装 ESLint 插件,详细流程请查阅:附:IDE 集成 ESLint 插件

2. 集成 ESLint

2.1 安装 ESLint

首先要安装的就是 eslint 这个包:

npm i eslint -D     # npm
yarn add eslint -D  # yarn
pnpm i eslint -D    # pnpm

依赖安装好之后,即可开始配置 ESLint

2.2 配置 ESLint

ESLint 提供了多种配置方式:

  • .eslintrc.js
    • package.json 未设置 type 字段时使用
  • .eslintrc.cjs
    • package.json 设置 type 字段为 commonjsmodule 时使用
  • .eslintrc.json
  • .eslintrc.yaml
  • package.jsoneslintConfig 配置项

比较推荐的两种方式是:

  • .eslintrc.js
    • 便于提供类型注解和条件语句
  • package.jsoneslintConfig 配置项
    • 有配置项和规则提示
    • 如果有封装专门的 ESLint 规则,配置时只需要简短的3行即可,这3行代码没必要单独放到一个文件中

单独配置文件可以与 package.jsoneslintConfig 配置项共存。
一个项目里可以有多个 ESLint 配置文件,但同一目录下仅支持一个。

ESLint 配置的优先级:

  • 从当前文件所在目录向上查找,ESLint 优先使用最临近的一个配置文件
  • 如果查找到项目根目录仍未查找到 ESLint 配置文件,则查找 package.json 里的 eslintConfig 配置项
  • 如果 package.json 里没有 eslintConfig 配置项,则使用系统主目录 (~/) 下的 ESLint 配置文件

如果系统主目录 (~/) 下没有配置文件,我也不知道会怎么样 ( ̄< ̄||)。
所以通常都会在项目根目录下编写 ESLint 配置。

.eslintrc.js 为例,配置过程如下:

  1. 在项目根目录下创建 .eslintrc.js 文件
  2. 根据 ESLint 配置,编写适合于当前项目的配置文件
    • 文末附上了我汇总的 ESLint 配置文件中每一个配置项的含义
    // 推荐的一点配置
    module.exports = {
      root: true,
      env: {
        es6: true,
        node: true,
        browser: true,
      },
      parserOptions: {
        sourceType: "module",
      },
      extends: ["eslint:recommended"],
      plugins: [],
      rules: {
        "global-require": "off",
        "import/no-dynamic-require": "off",
        "no-console": "warn",
        "no-debugger": "warn",
        "spaced-comment": "warn",
        "comma-style": ["warn", "last"],
      },
      globals: {
        defineProps: "readonly",
        defineEmits: "readonly",
        defineExpose: "readonly",
        withDefaults: "readonly",
      },
    };
    

3 补充 ESLint 插件

3.1 Typescript

官方教程推荐:typescript-eslint 官方

大致步骤如下:

  1. 安装需要的依赖

    npm i @typescript-eslint/parser @typescript-eslint/eslint-plugin eslint typescript -D     # npm
    yarn add @typescript-eslint/parser @typescript-eslint/eslint-plugin eslint typescript -D  # yarn
    pnpm i @typescript-eslint/parser @typescript-eslint/eslint-plugin eslint typescript -D    # pnpm
    

    如果是在 Vue3 项目中集成 Typescript 的规则,建议使用 @vue/eslint-config-typescript 这个插件,而不是使用 @typescript-eslint/eslint-plugin

  2. 补充 ESLint 配置

    在之前配置的基础上,额外加上如下配置即可:

    module.exports = {
      // ... 其他配置
      parser: '@typescript-eslint/parser',
      plugins: ['@typescript-eslint'],
      extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended'],
      // ... 其他配置
    };
    

完成上述基本配置之后,你的项目将不会提示 Typescript 相关的语法错误了。其他更高阶的配置,可参考对应插件的官方文档来配置。

3.2 Vue

官方插件:eslint-plugin-vue

配置步骤:

  1. 安装依赖

    npm i eslint-plugin-vue -D     # npm
    yarn add eslint-plugin-vue -D  # yarn
    pnpm i eslint-plugin-vue -D    # pnpm
    
  2. 补充 ESLint 配置

    如果之前补充了 Typescript 的配置,则只需补充如下配置即可:

    module.exports = {
      // ... 其他配置
      parser: '@typescript-eslint/parser',
      plugins: ['@typescript-eslint'],
      extends: [
        // 'eslint:recommended', // Vue 插件中已经加入这个了,所以不需要再加这个了
        'plugin:vue/vue3-recommended', // 如果是 Vue3 项目则开启这个
        // 'plugin:vue/recommended' // 如果是 Vue2 项目则开启这个
        'plugin:@typescript-eslint/recommended'
      ],
      // ... 其他配置
    };
    

按照如上步骤执行之后,通常来说 .vue template 里不会报之前的错误了,但里面的标签还是会报错。

原因是 parser 没有切换,所以更新一下 parser 和 parserOptions 配置即可,更新之后将会是如下:

module.exports = {
  // ... 其他配置
  parser: 'vue-eslint-parser',
  parserOptions: {
      parser: "@typescript-eslint/parser",
      sourceType: "module"
  }
  // ... 其他配置
};

vue-eslint-parser 这个 parser 提供了额外的几个 parserOptions 配置属性,可参考 vue-eslint-parser Github 的文档描述。

3.3 React

官方插件:eslint-plugin-react Github

配置步骤:

  1. 安装依赖

    npm i eslint-plugin-react -D      # npm
    yarn add eslint-plugin-react -D   # yarn
    pnpm i eslint-plugin-react -D     # pnpm
    
  2. 补充 ESLint 配置

    只需在你的配置文件里加入如下配置即可:

    module.exports = {
      // ... 其他配置
      plugins: [
        'react',
        // ...其他插件
      ],
      extends: [
        'eslint:recommended',
        'plugin:react/recommended',
        // 'plugin:react/jsx-runtime', // 如果使用的 React 17 的新版 JSX transform,就要启用这个
        // ...其他规则集合
      ],
      // ... 其他配置
    };
    

    如果上面的配置不够,就再开启 JSX 支持

    module.exports = {
      // ... 其他配置
      parserOptions: {
        ecmaFeatures: {
          jsx: true,
          // ... 其他配置
        },
        // ... 其他配置
      },
      // ... 其他配置
    };
    

4. 使用 ESLint

4.1 命令行

ESLint 常用的命令如下:

  • 检查 ESLint 结果:eslint --ext .js,.jsx,.ts,.tsx,.vue src
  • 修复 ESLint 可修复的错误:eslint --fix --ext .js,.jsx,.ts,.tsx,.vue src

详细命令可查阅中文文档 ESLint 命令行接口

4.2 结合 lint-staged 使用

在前一篇文章 一、项目规范的基石——husky 与 lint-staged 中详细介绍了如何集成 huskylint-staged,ESLint 集成后,只需在 package.json 中的 lint-staged 配置项中加入 ESLint 的命令即可。如 Vue 项目中会增加如下命令:

{
    "lint-staged": {
        "*.{js,jsx,vue,ts,tsx}": "eslint --fix"
    }
}

附:ESLint 相关资源

ESLint 网站

Sherry Standard ESLint

根据我自己的开发习惯,我封装了自己的 ESLint 配置插件——@sherry-standard/eslint-plugin,这个插件里包含了多个类型的配置,可根据项目实际需要来选择不同的类型,详细可查看对应的文档。

ESLint 部分配置释义

注:这部分的所有代码都总结自 ESLint 中文网:用户指南-配置,如有错误,望指正 ~

注:如需官方的类型定义文件,可安装 @types/eslint 来查看。

// eslint.d.ts

/* eslint-disable @typescript-eslint/no-explicit-any */

declare namespace ESLint {
  export interface Env {
    browser?: boolean; // 浏览器环境中的全局变量
    node?: boolean; // Node.js 全局变量和 Node.js 作用域
    commonjs?: boolean; //  CommonJS 全局变量和 CommonJS 作用域
    "shared-node-browser"?: boolean; //  Node.js 和 Browser 通用全局变量
    es6?: boolean; // 启用除了 modules 以外的所有 ECMAScript 6 特性(该选项会自动设置 ecmaVersion 解析器选项为 6)
    worker?: boolean; // Web Workers 全局变量
    amd?: boolean; // 将 require() 和 define() 定义为像 amd 一样的全局变量
    mocha?: boolean; // 添加所有的 Mocha 测试全局变量
    jasmine?: boolean; //  添加所有的 Jasmine 版本 1.3 和 2.0 的测试全局变量
    jest?: boolean; // Jest 全局变量
    phantomjs?: boolean; // PhantomJS 全局变量
    protractor?: boolean; // Protractor 全局变量
    qunit?: boolean; // QUnit 全局变量
    jquery?: boolean; // jquery 全局变量
    prototypejs?: boolean; // prototypejs 全局变量
    shelljs?: boolean; // ShellJS 全局变量
    meteor?: boolean; // Meteor 全局变量
    mongo?: boolean; // MongoDB 全局变量
    applescript?: boolean; // AppleScript  全局变量
    nashorn?: boolean; // Java 8 Nashorn 全局变量
    serviceworker?: boolean; // Service Worker 全局变量
    atomtest?: boolean; // Atom 全局变量
    embertest?: boolean; // Ember 全局变量
    webextensions?: boolean; // WebExtensions 全局变量
    greasemonkey?: boolean; // GreaseMonkey 全局变量
    /**
     * 支持自定义的环境
     * 如果你想在一个特定的插件中使用一种环境,确保提前在 plugins 数组里指定了插件名,
     *     然后在 env 配置中不带前缀的插件名后跟一个 / ,紧随着环境名
     */
    [key: string]: boolean | undefined;
  }

  export interface ParserOptions {
    /**
     * 可以使用 3、5、6、7、8、9 或 10 来指定你想要使用的 ECMAScript 版本
     * 开启这个配置项时,`不会` 自动启用 ES6 全局变量
     */
    ecmaVersion: 3 | 5 | 6 | 7 | 8 | 9 | 10;
    /**
     * script: 通用 JavaScript 语法
     * module: ECMAScript 模块语法
     */
    sourceType: "script" | "module";
    /**
     * 想使用的额外的语言特性,所有配置项都默认为 false
     */
    ecmaFeatures: {
      /**
       * 是否启用 jsx 语法支持
       * * 注意:支持 JSX 语法并不等同于支持 React
       * *       如果你期望支持 React 语义,可使用 eslint-plugin-react 插件
       */
      jsx: boolean;
      globalReturn: boolean; // 是否允许在全局作用域下使用 return 语句
      impliedStrict: boolean; // 是否在 ecmaVersion 是 5 或更高时,启用全局 strict mode
      experimentalObjectRestSpread: boolean; // 一个实验性功能
    };
    /**
     * 支持自定义的任意配置属性
     */
    [key: string]: any;
  }

  /**
   * 0 <=> "off"  关闭这条规则
   * 1 <=> "warn"  开启这条规则,不会导致程序退出
   * 2 <=> "error"  开启这条规则,当被触发的时候,程序会退出
   */
  export type RuleValue = "off" | "warn" | "error" | 0 | 1 | 2;

  export type RuleOptions = string | number | boolean | symbol;

  export interface Rules {
    [key: string]: RuleValue | [RuleValue, ...RuleOptions[]];
  }

  export interface Override {
    files: string[];
    processor?: string;
    /**
     * 为指定的文件类型配置 ESLint 规则
     */
    rules?: Rules;
  }

  export interface Config {
    /**
     * ESLint 一旦发现配置文件中有 "root": true,它就会停止在父级目录中寻找
     */
    root?: boolean;
    /**
     * 环境支持配置,所有的配置项默认都为 false
     * 这些环境并不是互斥的,所以你可以同时定义多个
     */
    env?: Env;
    /**
     * 默认使用 Esprima 作为解析器
     * Esprima @see https://github.com/eslint/espree
     * 可自定义解析器
     * 解析器必须要满足以下要求:
     *   * 1. 它必须是一个 Node 模块,可以从它出现的配置文件中加载。通常,这意味着应该使用 npm 单独安装解析器包
     *   * 2. 它必须符合 parser interface。
     *        parser interface @see http://eslint.cn/docs/developer-guide/working-with-plugins#working-with-custom-parsers
     * 与 ESLint 兼容的 parser:
     *     Esprima @see https://www.npmjs.com/package/esprima
     *     Babel-ESLint @see https://www.npmjs.com/package/babel-eslint 一个对Babel解析器的包装,使其能够与 ESLint 兼容
     *     @typescript-eslint/parser @see https://www.npmjs.com/package/@typescript-eslint/parser 将 TypeScript 转换成与 estree 兼容的形式,以便在ESLint中使用
     */
    parser?: string;
    parserOptions?: ParserOptions;
    /**
     * 值为字符串:指定配置的字符串(配置文件的路径、可共享配置的名称、eslint:recommended 或 eslint:all)
     * 值为字符串数组:每个配置都继承它前面的配置
     * extends 的一个属性值可以由以下内容组成:
     *   - `plugin:`
     *   - 包名,如 `prettier`
     *   - `/`
     *   - 配置名称 (比如 `recommended`)
     * extends 的一个属性值可以是到基本配置文件的绝对路径,也可以是相对路径
     */
    extends?: string | string[];
    /**
     * 插件是相对于 ESLint 进程的当前工作目录解析的
     * 插件名称可以省略 eslint-plugin- 前缀,也可携带这个前缀
     */
    plugins?: string[];
    /**
     * 指定统一的处理器
     * 处理器的作用:
     *     1. 从另一种文件中提取 JavaScript 代码,然后让 ESLint 检测 JavaScript 代码
     *     2. 在预处理中转换 JavaScript 代码
     * 可以生成命名的代码块,ESLint 将这样的命名代码块作为原始文件的子文件处理
     */
    processor?: string;
    /**
     * 为特定类型的文件指定处理器
     */
    overrides?: Override[];
    /**
     * writable: `允许` 被重新赋值。由于历史原因,布尔值 true 和字符串值 "writeable" 等价于 "writable"
     * readonly: `不允许` 被重新赋值。由于历史原因,布尔值 false 和字符串值 "readable" 等价于 "readonly"
     * * 虽然但是,不建议使用旧值!
     * * 注意:要启用 no-global-assign 规则来禁止对只读的全局变量进行修改
     */
    globals?: {
      [key: string]: "writable" | "readonly";
    };
    /**
     * 配置定义在插件中的一个规则的时候,必须使用 插件名/规则ID 的形式
     *     比如配置 prettier: "prettier/prettier": "warn"
     * * 当指定来自插件的规则时,确保删除 eslint-plugin- 前缀
     *     比如 prettier 对应的插件 eslint-plugin-prettier
     */
    rules?: Rules;
    /**
     * 共享设置
     * 它将提供给每一个将被执行的规则
     * 如果你想添加的自定义规则而且使它们可以访问到相同的信息,这将会很有用,并且很容易配置
     */
    settings?: {
      [key: string]: any;
    };
  }
}