深入浅出 Eslint,告别 Lint 恐惧症

2022-09-27 19:24:44 浏览数 (2)

前言

相信大多数同学在日常项目中对于 Eslint 相关配置感到痛不欲生。

对于 Lint 配置的不了解导致项目中总是会莫名其妙的提示报错,这应该是大多数同学面临的窘境。

其实这一切都是源自同学们对于 EsLint 的陌生而已。

文章会从使用配置指南过渡到插件开发指南,从而全面的为大家讲解 EsLint 的各种相关内容。帮助大家告别 EsLint 恐惧症。

使用配置指南

首先,我们从基础出发来看看 EsLint 相关配置代表的含义。

配置文件

在 EsLint 中所有的检测规则都是可配置的,自然所有的配置规则都需要有一个配置的存储文件。

在 EsLint 中支持两种方式来进行规则配置:

  1. Configuration Comments 在你的 JavaScript 代码中直接使用 JS 注释的形式将配置嵌入你的原代码。
  2. Configuration Files 使用单独 Eslint 配置文件来整合你的相关配置,支持 JavaScript、JSON 或者 YAML 文件三种格式(.eslintrc.*),当然也可以直接将配置写入项目的 package.json 中。在调用 EsLint 命令时,Eslint 会自动寻找对应的配置文件。

同时在 EsLint 中配置文件也支持向上查找的方式(层叠配置),比如我们的项目定义目录如下所示:

代码语言:javascript复制
- demo
    - packages
        - projectA
            - index.js
            - .eslintrc.js
        - projectB
            - index.js
    .eslintrc.js
    package.json

层叠配置使用离要检测的文件最近的 .eslintrc文件作为最高优先级,然后才是父目录里的配置文件。

这也就意味这,在 projectA 项目中不仅仅 projectA/.eslintrc.js 中的配置规则会生效,同时它也会继承上一层目录中的 .eslintrc 的配置(.eslintc.js)。

默认情况下,ESLint 会在所有父级目录里寻找配置文件,一直找到根目录。少部分情况下如果我们想要你所有项目都遵循一个特定的约定时,这将会很有用。(比如在 monorepo 项目中,我们通常会存在一份根级别的 EsLint 配置文件约束)。

同时,当寻找到项目根目录的 eslintrc.js 时,我们希望它停止向上查找。那么此时 Eslint 的配置文件也支持设置 root: true 的选项来停止这种层叠配置的查找机制。

比如,如果在 demo/packages/projectA/.eslintrc.js 中设置了 root: true,那么此时 projectA/index.js 仅会有 projectA/eslintrc.js 的配置生效。

demo/.eslintrc.js 仅会影响 projectB/index.js 并不会影响 packageA/index.js

ParserOptions

EsLint 支持任何类型的 JavaScript 语言选项(比如 ES6、模块类型等等),默认不进行任何配置时 EsLint 默认检测规则为 ES5 代码,

我们可以通过配置中的 ParserOptions 选项来进行语言选项设置,比如:

代码语言:javascript复制
// .eslint.js
module.exports = {};
代码语言:javascript复制
// index.js
const a = '1'; // error: Parsing error: The keyword 'const' is reserved
console.log(a);

文章之后的例子都会以 .eslint.js 的配置文件方式来演示。

上述的 Eslint 配置文件中,我们没有设置任何 ParserOptions。默认会使用 ES5 规范来检查我们的代码,自然当我们在项目中使用 const 时,EsLint 会提示错误 const 作为保留关键字。

当然,我们可以使用 ParserOptions 来修改 EsLint 对于语法检测的规则:

代码语言:javascript复制
// .eslint.js
module.exports = {
  parserOptions: {
    ecmaVersion: 6, // 指定 EsLint 支持 ECMAScript 6 的语法检测
  },
};
代码语言:javascript复制
// index.js
const a = '1'; // correct
console.log(a);

当然 ParserOptions 中支持的全部配置选项:

  • ecmaVersion: 如上边示例的,表示应用代码中支持的 ECMAScript 版本。默认为 5 ,支持3、5、6、7、8、9 或 10 来指定你想要使用的 ECMAScript 版本。当然也可以使用 latest 表示最新的 ECMA 版本。
  • sourceType: 表示应用代码中支持的模块规范,默认为 script。支持 scriptmodule (ESM) 两种配置。
  • ecmaFeatures: 它是一个对象,表示代码中可以使用的额外语言特性。
代码语言:txt复制
- globalReturn 允许全局下使用 return 。
- impliedStrict 启用全局严格模式。(ES 5以上有效)
- jsx 允许代码中使用 jsx 语法。
代码语言:javascript复制
module.exports = {
  parserOptions: {
    ecmaFeatures: {
      // 允许 js 代码中使用 jsx
      jsx: true,
      // 允许顶层 return
      globalReturn: true,
    },
    ecmaVersion: 6,
  },
};

总之,**ParserOptions** 选项表示 EsLint 对于不同的 Parser(解析器)配置的语言检查规则。

Parser

上边我们提到所谓的 ParserOptions 代表 Eslint 中支持我们使用哪些语法。

在 EsLint 配置中有一个和它名称非常相似的配置 Parser ,它表示 Eslint 在解析我们的代码时使用到的解析器。

关于 EsLint 是如何帮助我们进行代码检查的,简单来说本质上它仍是将我们的代码根据规定的解析器转化成为 AST 抽象语法树。

之后根据我们传入配置中的各种规则对于源代码生成的 AST 语法树进行代码检查以及代码修复。

ESLint 默认情况下使用Espree作为其解析器,当然我们也可以传入一些自定义的解析器。比如:Esprima、@typescript-eslint/parser 等等

通常,我们在项目中使用 typescript 代码:

代码语言:javascript复制
// .eslint.js
module.exports = {
  parser: 'espree', // 使用默认 espree 解析器
  rules: {
    'no-unused-vars': ['error'], // 定义规则禁止声明未使用的变量
  },
};


// index.ts 定义 b 但未使用,并没有报错
const b: string = '1'

上述我们使用了 typescript 语法定义了变量 b 但是并没有使用变量 b ,此时 EsLint 规则检查并没有生效。

这是因为我里上述配置文件的 parser 默认使用的是 espree,它并不支持 typescript 语法的检查,要额外支持 ts 语法的检查需要使用额外的 ts 解析器。

代码语言:javascript复制
module.exports = {
  parser: '@typescript-eslint/parser', // 修改解析器为 @typescript-eslint/parser
  rules: {
    'no-unused-vars': ['error'],
  },
};


// index.ts
const b: string = '1' // error: 'b' is assigned a value but never used

我们提到过本质上 ESLint 内部是基于 AST 进行检查,对于 AST 想深入了解的同学可以查看我这篇文章。

所以 tsc 在处理 ts 语法转译后的 ast 规则是 eslint 默认的 espree 是完全不一致的,所以我们需要通过 @typescript-eslint/parser 解析器来解析我们的代码。

当我们使用特定的解析器时,比如使用 @typescript-eslint/parser 最终会将 ts 文件转移后的 ast 结构转化成为 espree 支持的 ast 结构进行静态检查。

当然,我们最开始提到的 parserOptions 也是针对于不同的解析器(Parser)的选项配置,具体各个 ParserOptions 选项内容可以在不同的 parser 文档中查找对应规则。

所以,Parser 选项简单来说就是表示我们以何种解析器来转译我们的代码。本质上,所有的解析器最终都需要讲代码转化为 espree 格式从而交给 Eslint 来检测。

Environments

同样在 EsLint 中我们可以通过 env 选项来设置环境变量支持,从而支持一组通用的全局变量。

比如通常我们在浏览器环境下执行 JavaScript 代码的话需要使用到浏览器下的一些全局相关的 Api 参数,比如 documentwindow 等等。

默认情况下 Eslint 会检测我们的代码,并且并不支持这些不同环境下的全局变量。比如:

代码语言:javascript复制
module.exports = {
  root: true,
  parserOptions: {
    ecmaVersion: 'latest',
  },
  rules: {
    'no-undef': ['error'], // 设置允许使用未声明的全局变量
  },
};

// index.js
console.log(window) // 'window' is not defined.eslintno-undef
console.log(document) // 'document' is not defined.eslintno-undef

可以看到,当我们在项目代码中使用 window 时此时 EsLint 会提示 window 未被声明。

我们可以通过指定 env 配置来告诉 EsLint 当前项目支持的运行环境,从而可以使用当前环境下相关的全局变量:

代码语言:javascript复制
// .eslintrc
module.exports = {
  root: true,
  parserOptions: {
    ecmaVersion: 'latest',
  },
  env: {
    browser: true,
  },
  rules: {
    'no-unused-vars': ['error'],
     // 禁止使用未定义的变量
    'no-undef': ['error'],
  },
};


// index.js
console.log(window); // correct
console.log(document) // correct

同样 env 选项支持非常多的配置:

  • browser 支持浏览器环境,表示支持浏览器环境下的相关全局变量。比如 windowdocument 等等
  • node 支持 NodeJs 环境,可以使用 Node 环境下的全局变量。比如 processglobalrequire 等等
  • shared-node-browser 表示可以使用 Node 环境和浏览器环境下同时存在的全局变量,比如 console 相关。
  • es6 启用除了 modules 以外的所有 ECMAScript 6 特性(该选项会自动设置 ecmaVersion 解析器选项为 6)。
  • 等等非常多的预设环境,具体你可以在这里查看到。

需要额外强调的是这里 env 中的 es6 和 parserOptions 中的 ecmaScript 区别:

  • parserOptions 中的 ecmaScript 设置时(如果为 6 或者更高版本),仅表示 Lint 在检查时支持一些高版本的语法。比如 letconst、箭头函数等等。
  • env 中的 es6 开启时,表示允许代码中使用高版本语法的 Api 比如:PromiseSetMap 等全局相关模块。当然开启 env: {es6:true} 想等于同时设置了ecmaVersion: 6

Globals

上述我们提到了,我们可以 env 来预设来支持不同环境下的全局变量。

那么,如果我们定义了一些特殊的全局变量。那么我们应该如何告诉 EsLint 呢?

在 Typescript 中我们可以通过 *.d.ts 声明文件来解决 Ts 对于自定义全局变量的支持。

在 Eslint 同样,我们可以在配置文件中通过 globals 选项来支持自定义的全局变量。

比如,我们在全局定义了 wang.haoyu 的变量,我们希望在 index.js 来直接使用这个变量:

代码语言:javascript复制
// .eslintrc
module.exports = {
  root: true,
  parserOptions: {
    ecmaVersion: 5,
  },
  env: {
    browser: true,
  },
  // 通过 globals 定义额外的全局变量
  globals: {
    wangHaoyu: true,
  },
  rules: {
     // 禁止使用未定义的变量
    'no-undef': ['error'],
  },
};


// index.js
console.log(wangHaoyu);

当访问当前源文件内未定义的变量时,no-undef 规则将发出警告。如果我们想在一个源文件里使用某些全局变量,并且避免 EsLint 发出错误警告。那么我们可以使用 globals 配置来定义这些特殊的全局变量。

上述我们定义 globals 中的 wangHaoyu: true 相当于我们定义了 wangHaoyu: 'writable'

同样,globals 中的值还支持:

  • "writable"或者 true,表示变量可重写;
  • "readonly"或者false,表示变量不可重写;
  • "off",表示禁用该全局变量。

Configuring Rules

EsLint 通过我们在配置文件中定义规则从而对于我们的代码进行静态检测,同样我们可以通过定义一系列 Rules 从而利用 Lint 工具来检查我们的代码。

同样 EsLint 内部内置了一系列规则从而来帮助我们来检查对应代码。

我们可以通过以下规则选项设置当前规则的检测等级:

  • "off" 或 0 表示关闭本条规则检测
  • "warn" 或 1 表示开启规则检测,使用警告级别的错误:warn (不会导致程序退出)
  • "error" 或 2 表示开启规则,使用错误级别的错误:error (当被触发的时候,程序会退出)

比如:

代码语言:javascript复制
// .eslintrc
module.exports = {
  root: true,
  parserOptions: {
    ecmaVersion: 6,
  },
  env: {
    browser: true,
  },
  globals: {
    wangHaoyu: true,
  },
  rules: {
    'no-console': [1], // 对于 console 进行警告检测
    'no-unused-vars': ['error'], // 对于未使用的变量进行错误检测
  },
};

// index.js
console.log('hello world'); // warn: Unexpected console statement
const name = '19Qingfeng'; // error:'name' is assigned a value but never used

在 rules 对象中,通常 key 为规则的名称,比如上述的 no-console 代表具体的规则名称,而 value 可以为一个数组。

数组第一个项代表规则 ID ,通过 0 1 2 或者 off warn error 表示检测的等级,而其余参数代表规则的具体配置。比如:

代码语言:javascript复制
module.exports = {
  root: true,
  parserOptions: {
    ecmaVersion: 6,
  },
  env: {
    browser: true,
  },
  globals: {
    wangHaoyu: true,
  },
  rules: {
    quotes: ['error', 'single', { allowTemplateLiterals: true }],
  },
};

上述的 rules 中的 quotes 引号规则配置,第一个参数表示错误的等级。

第二个参数,表示传递给 quotes 的具体配置,它支持;

  • "double"(默认)要求尽可能使用双引号
  • "single"要求尽可能使用单引号
  • "backtick"要求尽可能使用反引号

同时第三参数,同样作为传递给 quotes 的配置选项,它是一个对象配置,这里的 allowTemplateLiterals 表示支持单引号的同时允许模版字符串的写法。

当然,具体某个规则中支持哪些具体配置。同学们可以具体规则的对应文档中进行查看。

如果,你不需要传递任何参数。可以直接使用字符串的形式进行简写。比如: 'no-console': 2

还记得我们上述提到的 Config File 中的层叠配置吗。

Rules 除了定义一些额外的规则配置的同时也支持在层叠配置下的覆盖(扩展)规则,比如:

  • 改变继承的规则级别而不改变它的配置选项
代码语言:txt复制
- 基础配置:`"eqeqeq": ["error", "allow-null"]`
- 派生的配置:`"eqeqeq": "warn"`
- 最后生成的配置:`"eqeqeq": ["warn", "allow-null"]`
代码语言:txt复制
- 基础配置:`"quotes": ["error", "single", "avoid-escape"]`
- 派生的配置:`"quotes": ["error", "single"]`
- 最后生成的配置:`"quotes": ["error", "single"]`

Plugins

接下来我们来聊聊关于 EsLint 中的 Plugins 配置。

通常 EsLint 中默认提供了一系列内置的 Rules 规则提供给我们进行配置,从而检测我们的 JavaScript 代码。

这些内置的规则你可以在 eslint/node_modules/eslint/lib/rules/array-bracket-spacing.js 中详细查看到:

但是在某些特定条件下,内置的一些规则并不能满足我们的代码检查。

所以此时我们就要基于该情况做一些特殊的拓展了,Plugin 的作用正是处理这些功能而生。

比如,通常在我们使用 Eslint 来检查我们的代码时,需要将解析器替换为 @typescript-eslint/parser 的同时针对于一些 TypeScript 特定语法我们还需要使用 @typescript-eslint/eslint-plugin 来支持一些特定的 TS 语法检查。

这里我们额外安装的 @typescript-eslint/eslint-plugin 中就包含了一系列有关于 TS 文件检测的特殊规则。

在 EsLint 内部并不支持有关于 Ts 的一些语法,自然我们需要通过 Plugin 来拓展对应的检测规则。

当我们在 Plugins 中声明对应的插件后,就可以在 rules 配置中使用对应插件中声明的特殊规则限制了。比如:

代码语言:javascript复制
// .eslintrc.js
module.exports = {
  root: true,
  parser: '@typescript-eslint/parser',
  parserOptions: {
    ecmaVersion: 6,
  },
  plugins: ['@typescript-eslint/eslint-plugin'],
  env: {
    browser: true,
  },
  rules: {
    '@typescript-eslint/array-type': 2
  },
};

// index.ts
const array: Array<number> = [1, 2, 3] // error: Array type using 'Array<number>' is forbidden. Use 'number[]' instead.

const correctArray: number[] = [1, 2, 3] // correct

上述我们将 parser 更换为 @typescript-eslint/parser 后,同样引入了 @typescript-eslint/eslint-plugin 插件来扩展针对于 ts 文件的 Lint 检查规则。

同时,我们在 rules 配置中使用 @typescript-eslint/array-type 来定义数组类型声明时的规则规范。

我们可以看到上边的 index.ts 中针对于 TS 声明数组类型的检查会进行额外的 Eslint 检查。

简单点来说,所谓的 Plugin 正是对于 EsLint 内置的规则拓展,通过 Plugin 机制我们可以实现 EsLint 中自定义的 Rules。

注意:声明了 Plugin 时仅表示我们引入了该规则对应的集合,并不代表会立即启动。需要我们手动在 rules 中去声明对应插件的规则。

同时 plugins 中的插件寻找规则,遵循了 Nodejs 中的模块查找规则。

简单来说比如我们上述引入的插件 plugins: ['@typescript-eslint/eslint-plugin'] 就相当于 require('@typescript-eslint/eslint-plugin') 查找规则一致。

Extends

如果说上述 Plugins 和 Rules 可以满足项目的 Lint 配置的话,那么 Extends 关键字可以理解为关于 Plugins 和 Rules 结合而来的最佳实践。

正如其名,Extends 表示继承的意思。通常在不同的项目中,大多数情况下都具有相同的 Lint 相关配置。

基于这种情况 EsLint 提供了 Extends 关键字来解决不同项目下存在的通用配置。我们可以利用 EsLint 中的 Extends 关键字来继承一些通用的配置。

比如,EsLint 官方提供了 eslint:recommended 规则,当我们在配置文件中继承 "eslint:recommended" 时,相当于启用了一系列核心规则,这些规则会被 EsLint 官方维护在 "eslint:recommended" 中定期更新:

代码语言:javascript复制
// .eslintrc.js
module.exports = {
  root: true,
  parser: 'espress',
  parserOptions: {
    ecmaVersion: 6,
  },
  extends: ['eslint:recommended'],
};

// index.js
const a = 'hello world'; // error: 'a' is assigned a value but never used.eslintno-unused-vars

上述的代码可以看到,我们没有定义任何 rules 以及 plugins 。仅仅是 extends: ['eslint:recommended']

此时我们在 index.js 中定义了 a 变量但为使用,EsLint 会为我们检测出错误 'a' is assigned a value but never used.eslintno-unused-vars

其实,**extends** 的作用简单来说就是在项目内继承于另一份 EsLint 配置文件而已。

Extends 继承关键字存在三种写法(情况):

  • 从 EsLint 本身的规则进行继承,比如 extends: ['eslint:recommended']
  • 从第三方的 NPM 包规则进行继承,比如 extends : ['eslint-config-airbnb']
  • 从 ESLint 的插件进行继承,比如 extends: ['plugin:react/recommended']
  • 从绝对路径继承而来,比如 extends: ["./node_modules/coding-standard/eslintDefaults.js"]
代码语言:javascript复制
// .eslintrc.js
module.exports = {
   "extends": [
     // 直接从 EsLint 本身集成的规则继承
     "eslint:recommended",
    // 从一些第三方NPM包进行继承,比如 eslint-config-standard、eslint-config-airbnb 
    // eslint-config-* 中 eslint-config- 可以省略 
     "airbnb",
     // 直接从插件继承规则,可以省略包名中的 `eslint-plugin`
     // 通常格式为 `plugin:${pluginName}/${configName}`
     "plugin:@typescript-eslint/recommended",     
   ]
}

所谓的规则继承,我们提到过就是继承于另一份 EsLint 配置文件,比如我们以 plugin:@typescript-eslint/recommended 为例:

代码语言:javascript复制
// .eslintrc.js
module.exports = {
   "extends": [
     // 继承于 @typescript-eslint 插件下的推荐配置
     "plugin:@typescript-eslint/recommended",     
   ]
}

// 上述的配置完全等价于
module.exports = {
    // 继承而来
    parser: '@typescript-eslint/parser',
    parserOptions: { sourceType: 'module' },
   "plugins": [
       "@typescript-eslint"
   ],
   "rules": [
       // ...
      // 省略@typescript-eslint/recommended 中 N 多种规则的声明
   ]
}

既然存在继承,那么一定会有覆盖。接下来,我们稍微来聊聊继承中针对于 Rules 的覆盖规则:

rules 属性可以做下面的任何事情以扩展(或覆盖)规则:

  • 启用额外的规则
  • 改变继承的规则级别而不改变它的选项:
代码语言:txt复制
- 基础配置:`"eqeqeq": ["error", "allow-null"]`
- 派生的配置:`"eqeqeq": "warn"`
- 最后生成的配置:`"eqeqeq": ["warn", "allow-null"]`覆盖基础配置中的规则的选项
代码语言:txt复制
- 基础配置:`"quotes": ["error", "single", "avoid-escape"]`
- 派生的配置:`"quotes": ["error", "single"]`
- 最后生成的配置:`"quotes": ["error", "single"]`

关于 Rules 中的覆盖规则其实是完全和 config File 的层叠配置是完全一致的。

Overrides

通常在一些项目中,我们需要针对不同的文件进行不同的 Lint 配置,那么此时 EsLint 同样为我们提供了 Overrides 选项来解决这个问题。

比如,我们项目中存在一些以 .test.spec 结尾的测试文件,那么此时我们希望这些测试文件可以拥有不同的 Lint 配置规则。

那么我们就可以使用 Overrieds 配置来进行特定文件的规则覆盖,比如:

代码语言:javascript复制
// .eslintrc.js
module.exports = {
  rules: {
      'no-console': 2
  },
  overrides: [
    // *.test.js 以及 *.spec.js 结尾的文件特殊定义某些规则
    {
      files: ['*-test.js', '*.spec.js'],
      rules: {
        'no-unused-expressions': 2,
      },
    },
  ],
};

上述的配置中,针对于 *.test.js/*.spce.js 的话既支持 no-console 的规则同时也开启了 no-unused-expressions 的规则。

当然你可以 overrides 你还可以配置更多的规则,比如 excludedFilesparserparserOptioons 等等

Processor

在配置手册的最后,我们来聊聊 Processor 配置。通常针对于 Web 项目大多数情况下我们都使用框架辅助我们的开发。

比如,我们在使用 Vue 来开发我们的项目时,希望利用 Eslint 代码来约束我们的 Vue 代码。那么此时 Processor 的作用就体现了。

我们清楚在一个 .vue 文件中,并不单纯的由 JavaScript 组成而来,所以我们希望 EsLint 检查我们的 JavaScript 代码时,就需要以一种额外的处理手段将特殊代码中的 JS 提取出来从而进行检查。

Processor 正是这种用法,

插件可以提供处理器。处理器可以从另一种文件中提取 JavaScript 代码,然后让 ESLint 检测 JavaScript 代码。或者处理器可以在预处理中转换 JavaScript 代码。

通常我们在编写 EsLint 插件时,如果是针对于非 Js 文件的话可以单独使用一个 Processor 来处理,当然这个后续我们在谈。

如果我们要在配置文件中指定处理器,直接使用 processor 属性就可以。

使用由插件名和处理器名组成的串接字符串加上斜杠。比如,下面的选项就代表启用插件 a-plugin 提供的处理器 a-processor

代码语言:javascript复制
{
    "plugins": ["a-plugin"],
    "processor": "a-plugin/a-processor"
}

简单来说处理器的原理是将我们的非 JS 文件经过处理成为一个一个具名的代码块,最终在将这些处理后的 js 文件当作原始文件的子文件交给 EsLint 处理。

这也就意味着我们可以通过 overrides 配置针对一些特定的文件进行特殊的处理,比如:

代码语言:javascript复制
{
    "plugins": ["a-plugin"],
    "overrides": [
        {
        // 针对于 .md 结尾的文件使用 a-plugin 的 processor 进行处理
            "files": ["*.md"],
            "processor": "a-plugin/markdown"
        },
        {
        // 上述提到过本质上会将 .md 文件通过 processor 转化为一个个具名的代码块
            "files": ["**/*.md/*.js"],
            "rules": {
                "strict": "off"
            }
        }
    ]
}

上述代码中我们首先针对于 .md 结尾的文件使用 a-plugin 的 processor 进行处理。

我们提到过 processor 最终会将 md 文件中的 JS 代码提取出来,并且作为当前文件的子文件。

此时,我们就可以通过 ["**/*.md/*.js"] 对于 processor 提取后的 js 文件进行规则配置了,这里我们配置了关闭它的严格模式。

插件开发指南

上边我们聊了聊 EsLint 中相配的配置规则,之后我们来聊聊一些简单的插件开发流程。

这里并不会涉及比较深入的插件开发流程,如果针对于插件开发有兴趣的话大家可以详细阅读 插件开发手册。

在上半部分我们提到过,所谓的 EsLint Plugin 简单来说就是一系列规则的合集。

所以我们可以将一个 Plugin 理解成为多个 Rules 的承载体。

社区为我们提供了一个 Yeoman generator 的脚手架来辅助我们快速生成 EsLint 插件模板。

安装完毕后,我们通过可以运行js yo eslint:plugin来快速创建一个 Plugin 模板。

之后如果想要为 Plugin 中添加具体的规则,同样也可以通过 yo eslint:rule 为这个插件创建对应的规则。

之后我们着重来看下校验单个规则是如何编写的:

在 EsLint 中单个约定规则存在三个重要的目录:

  • docs 相关规则的文档说明
  • lib 相关规则的具体实现代码
  • tests 相关规则的测试用例代码

我们着重来看下 lib 目录下的内容,lib 目录中包含一个 rules 文件夹用于存储定义的各种规则文件:

rules 目录中存放定义的各个规则,index.js 作为当前 Plugin 的统一入口文件从而进行导出。

之后在当前插件目录运行yo eslint:rules,当前终端会以问询的方式来为我们创建对应的规则模板:

接下来我们来看看 lib/rules/no-function-expression.js 中具体的模板内容:

代码语言:javascript复制
// lib/rules/no-function-expression.js

/** @type {import('eslint').Rule.RuleModule} */
module.exports = {
  meta: {
    type: null, // `problem`, `suggestion`, or `layout`
    docs: {
      description: "for test rule",
      recommended: false,
      url: null, // URL to the documentation page for this rule
    },
    fixable: null, // Or `code` or `whitespace`
    schema: [], // Add a schema if the rule has options
  },

  create(context) {
    // variables should be defined here

    //----------------------------------------------------------------------
    // Helpers
    //----------------------------------------------------------------------

    // any helper functions should go here or else delete this section

    //----------------------------------------------------------------------
    // Public
    //----------------------------------------------------------------------

    return {
      // visitor functions for different types of nodes
    };
  },
};

每一个 Rules 规则文件中倒出的对象中必须存在两个属性:

  • meta: 表示本条规则相关的元数据,比如类型、文档、可接受的参数等,具体可以查看官方文档。这里我就不累赘了。
  • create: meta 属性记录了本条规则相关的一些基础信息,而 create 中的正是实现本条规则的具体检测逻辑的。

接触过 Babel-Plugin 开发的同学会对这里非常熟悉,本质上 create 方法中是类似于一种基于 AST 的访问者模式的深度遍历过程。

简单来说,我们可以 create 方法中返回的对象中定义 key 为对应的 AST 节点类型,而当 Eslint 调用该 Plugin 处理我们的代码时,如果匹配到对应的节点类型就会进入对应的函数处理。

比如:

代码语言:javascript复制
 create(context) {
    return {
      // visitor functions for different types of nodes
      FunctionDeclaration() {
        // 碰到函数定义时进入 do somthing
      },
    };
  },

而 create 函数中的 context 参数则为我们提供了一系列上下文相关的 Api 从而让规则完成他们的工作。

关于 context 参数的具体内容,你可以点击这里查看。

比如说,我们希望实现在项目内部禁止使用函数表达式声明的规则:

代码语言:javascript复制
  create(context) {
    return {
      // 碰到变量声明时进入
      VariableDeclaration(node) {
        const declarations = node.declarations;
        if (
          declarations.find(
            (node) =>
              node.init.type === 'FunctionExpression' ||
              node.init.type === 'ArrowFunctionExpression'
          )
        ) {
          context.report({
            node,
            message: '不使用函数表达式声明。',
          });
        }
      },
    };
  },

此时一个简单的不能在代码中使用函数表达式声明的规则就已经书写完毕。

此时,我们仅仅需要在某个项目中安装到我们刚刚编写的 Plugin 包同时进行配置:

代码语言:javascript复制
module.exports = {
  env: {
    browser: true,
    es2021: true,
    node: true,
  },
  // 使用插件 初始化的插件名为 eslint-plugin-custom
  // 当名称为 eslint-plugin-* 时,可省略 eslint-plugin-
  plugins: ['custom'],
  rules: {
    // 使用插件中的规则
    'custom/no-function-expression': 2,
  },
  parserOptions: {
    ecmaVersion: 'latest',
  },
};

之后,我们在代码中使用函数表达式的方式声明时 EsLint 就会为我们提示错误:

代码语言:javascript复制
/Users/wanghaoyu/Desktop/eslint/packages/project/index.js
  1:1  error  不使用函数表达式声明。  custom/no-function-expression

通常情况下对于此类要求检测的语法错误,我们一般都会利用 EsLint 来进行自动修复。细心的同学可能也会发现针对于我们当前编写 EsLint 插件是无法为我们提供修复选项的。

上述这个问题,EsLint 插件开发同样为我们提供了一个 context.report.fix 属性用于尝试为我们的错误进行自动修复。

代码语言:javascript复制
context.report({
    node,
    message: '不使用函数表达式声明。',
    fix: function (fixer) {
        // do something 
    },
});

具体的修复逻辑我就不花篇幅去累赘了,这里更多涉及的是 AST 层面的操作。有兴趣的朋友可以查看我之前的系文章 编译原理。

同时关于 fix 的相关 Api 你可以在这里查阅到。

结尾

文章重心更多的是想为大家起到一个抛砖引玉的作用,对于 EsLint 我相信大多数同学在开发中对于它接触的并不是很多。

所以花了较多篇幅用在了各个配置详细代表的含义上,当然如果你之后有关于 Lint 插件编写的想法完全可以查阅文档相关 API 进行处理。

文章到这里就结束了,感谢每一个能看到结尾的小伙伴。

0 人点赞