阐述一下你对esd的理解和认识:5000字带你深入理解ESLint
阐述一下你对esd的理解和认识:5000字带你深入理解ESLint{ // 指定环境,比如是浏览器还是 Node,会提供一些预定义的全局变量 "env": { "browser": true "es2021": true } "extends": "eslint:recommended" // 要扩展的配置文件 "parserOptions": { "ecmaVersion": "latest" // 指定你要使用的 ECMAScript 语法版本,"latest" 表示始终启用最新的 ECMAScript 版本 "sourceType": "module" // "script" (默认值) 或 "
这篇文章详细介绍了 ESLint 相关的一些知识,主要分成三大部分:
- ESLint 基本介绍与使用
- ESLint 运行原理与 AST
- 如何编写 ESLint 插件
ESLint 是一个开源的 Javascript 代码检查工具,由 Nicholas C. Zakas 于2013年6月创建。代码检查是一种静态的分析,常用于寻找有问题的模式或者代码,并且不依赖于具体的编码风格。对大多数编程语言来说都会有代码检查,一般来说编译程序会内置检查工具。
JavaScript 是一个动态的弱类型语言,在开发中比较容易出错。因为没有编译程序,为了寻找 JavaScript 代码错误通常需要在执行过程中不断调试。ESLint 可以让程序员在编码的过程中发现问题而不是在执行的过程中。
ESLint 的初衷是为了让程序员可以创建自己的检测规则。ESLint 的所有规则都被设计成可插拔的。ESLint 的默认规则与其他的插件并没有什么区别,规则本身和测试可以依赖于同样的模式。为了便于人们使用,ESLint 内置了一些规则,当然,你可以在使用过程中自定义规则。
安装与使用你可以使用 npm 或者 yarn 安装 ESLint,本文会使用 yarn。首先创建一个目录eslint-start,初始化package.JSON文件,然后安装 eslint。
yarn init
yarn add eslint --dev
复制代码
安装完成之后需要设置一个配置文件,可以通过命令行工具直接生成:
yarn create @eslint/config
复制代码
在这个过程中,ESLint 会让你选择一些选项:
✔ How would you like to use ESLint? · problems
✔ What type of modules does your project use? · esm
✔ Which framework does your project use? · none
✔ Does your project use TypeScript? · No / Yes
✔ Where does your code run? · browser
✔ What format do you want your config file to be in? · JSON
复制代码
之后会得到一个.eslintrc.json文件,内容如下:
{
// 指定环境,比如是浏览器还是 Node,会提供一些预定义的全局变量
"env": {
"browser": true
"es2021": true
}
"extends": "eslint:recommended" // 要扩展的配置文件
"parserOptions": {
"ecmaVersion": "latest" // 指定你要使用的 ECMAScript 语法版本,"latest" 表示始终启用最新的 ECMAScript 版本
"sourceType": "module" // "script" (默认值) 或 "module"(如果你的代码是 ECMAScript 模块)
}
// 配置规则
"rules": {}
}
复制代码
JSON 和 YAML 配置文件是支持注释的,ESLint 会 ignore 配置文件中的注释
现在你可以在任何文件或目录上运行 ESLint。
例子下面来看一个简单的例子,首先添加一条规则到.eslintrc.json中的rules部分:"prefer-const": "error",这条规则要求声明后没有被重新赋值的变量必须使用const,否则会报错。
ESLint 官方提供的所有规则都可以在这个页面找到:eslint.org/docs/rules/[1]
{
// ...
"rules": {
"prefer-const": "error"
}
}
复制代码
之后在项目根目录创建一个index.js文件,并把以下内容写入到文件中:
let myName = 'dapangmao';
console.log(myName);
复制代码
接下来就可以运行 ESLint 了:
yarn run eslint index.js
复制代码
命令执行完之后,会在控制台看到以下错误:
也可以通过在上面命令的基础上添加--fix,这样 ESLint 会尝试去修复错误,对于上图中的错误,ESLint 会自动把let替换为const,感兴趣的读者可以自行尝试。
rulesESLint 中有两个重要的部分:rules 和 plugins。
在上一个例子中,我们使用了键值对的形式来添加一个规则,键是规则的名称,值是错误级别,这一类的规则是没有属性的,只需要开启或者关闭。
Rule 的错误级别可以是以下值之一:
- off或者0:关闭规则
- warn或者1:开启规则,使用警告级别的错误
- error或者2:开启规则,使用错误级别的错误
{
"no-debugger": "error"
"no-delete-var": "warn"
"no-dupe-args": "off"
}
复制代码
除了键值对形式的规则外,还有一部分规则除了需要开启或关闭,还需要配置属性。
"rules": {
"quotes": ["error" "single"] // 如果不是单引号,则报错
"one-var": [
"error"
{
"var": "always" // 每个函数作用域中,只允许 1 个 var 声明
"let": "never" // 每个块作用域中,允许多个 let 声明
"const": "never" // 每个块作用域中,允许多个 const 声明
}
]
}
复制代码
plugins
尽管 ESLint 附带了一些很好的规则,但通常它们不足以满足项目的所有需求,特别是如果使用 React、Vue、Angular 等库和框架进行构建时。ESLint 插件允许我们根据项目的需要添加自定义规则。插件作为 npm 模块发布,命名格式为eslint-plugin-<plugin-name>。
要使用插件,首先需要通过 npm 安装插件,然后把插件添加到eslintrc配置文件中的plugins中。例如,你想使用一个名为eslint-plugin-my-awesome-plugin的插件,你可以像这样把它添加到你的配置文件中:
{
"plugins": ["my-awesome-plugin"] // "eslint-plugin" 前缀可以省略
}
复制代码
需要注意的是,添加了这个插件不意味着这个插件的所有规则都会被自动启用,仍然需要单独应用要与该插件一起使用的每个规则,在配置文件中的 rules 对象上配置。
{
"rules": {
"eqeqeq": "off"
"curly": "error"
}
}
复制代码
但是如果每一个规则都需要配置一遍,对开发者来说很不友好,所以 ESLint 提供了另一种方式:可共享的配置。
可共享的配置ESLint 允许我们通过将配置发布到 npm 来共享配置。与插件的名字类似,可共享的配置以eslint-config-<config-name>的格式发布。
要使用可共享配置,首先也要从 npm 安装,然后可以通过extends部分来扩展项目的 ESLint 配置。
{
"extends": "standard" // 与插件类似,"eslint-config" 前缀可以省略
}
复制代码
我们可以通过将多个配置添加到数组中来扩展它们,如果配置修改相同的规则,则前面的配置的规则将被后面的配置覆盖,因此在这些情况下顺序很重要。
需要注意的是,可共享配置不仅仅用于共享规则集,它们可以是具有自己的插件、格式化程序等的完整配置,甚至还可以从其他配置扩展。
以eslint-config-standard为例,当我们使用"extends": "standard"时,实际上是使用了这个配置文件[2]。
带有配置的插件除了使用eslint-config-<config-name>来发布可共享配置之外,插件本身也可以附带不同的可共享的配置集,我们可以根据项目需要来选择使用哪一个。如果你以前有配置过 ESLint,很可能见过这样的写法:
{
"extends": {
"plugin:prettier/recommended"
}
}
复制代码
我们可以通过plugin:前缀使用插件附带的这些配置。例如,我们正在使用一个名为eslint-plugin-my-awesome-plugin的插件,它带有一个名为recommended的配置。然后,我们可以将plugin:my-awesome-plugin/recommended添加到配置中的extends部分,来从该可共享配置扩展。
我们甚至不需要在eslintrc配置文件中把prettier添加到plugins中,因为recommended配置中已经包含了。我们以eslint-plugin-prettier为例,如果查看代码[3],就会发现这个插件导出了一个recommended配置,内容如下:
module.exports = {
configs: {
recommended: {
extends: ['prettier']
plugins: ['prettier']
rules: {
'prettier/prettier': 'error'
'arrow-body-style': 'off'
'prefer-arrow-callback': 'off'
}
}
}
};
复制代码
通过"extends": "eslint:recommended",所有在 rules[4] 页面打钩✔️的 rules 都会被开启。
ESLint 工作原理了解了 ESLint 基本的使用之后,我们再来了解一下 ESLint 的工作原理,也为接下来的编写 ESLint 插件部分做准备。
在 ESLint 中,默认使用 Espree[5] 来解析 JavaScript,将代码转换成 AST(抽象语法树),然后去拦截检测是否符合我们规定的书写方式,最后让其展示报错、警告或正常通过。
ESLint 的核心就是一系列 rules,而 rules 的核心就是利用 AST 来做校验。在 ESLint 中,一切都是可插拔的,每条规则相互独立。
架构这张图是 ESLint 官网[6]给出的一个架构图。
- bin/eslint.js - 这个是命令行应用程序实际上执行的文件,它仅仅是个封装,用来启动 ESLint,并向cli传递命令行参数。
- lib/api.js - 这个是require("eslint")的入口,导出了一个包含Linter、ESLint、RuleTester和SourceCode的对象。
- lib/cli.js - 这个是 ESLint CLI 的核心。它接受一个参数数组,然后使用eslint执行命令。通过保持这个文件作为一个单独的应用程序,它允许其他人在另外的 Node.js 程序中有效的调用 ESLint,就好像是在命令行上操作的一样。它最重要的函数是cli.execute()。它也扮演着读取文件、遍历目录,输入和输出的角色。
- lib/cli-engine/:这个模块是 CLIEngine 类,它查找源代码文件和配置文件,然后使用 Linter 类进行代码验证。这里面包括了配置文件、解析器、插件和格式化程序的加载逻辑。
- lib/linter/ - 这个模块是基于配置选项进行代码验证的核心 Linter 类。这个文件不与控制台交互,没有 I/O。对于其他需要验证 JavaScript 文本的 Node.js 程序,他们将能够直接使用此接口。
- lib/rule-tester/ - 这个模块是 RuleTester 类,它是 Mocha 的包装器,因此可以对规则进行单元测试。这个类让我们可以为每个已实现的规则编写格式一致的测试,并确信每个规则都有效。 RuleTester 接口以 Mocha 为模型,与 Mocha 的全局测试方法一起使用。 RuleTester 也可以修改为与其他测试框架一起使用。
- lib/source-code/ - 这个模块是 SourceCode 类,用于表示解析后的源代码。它接收源代码和代表代码的 AST 节点。
- lib/rules - 包含了内置规则
如果想要深入了解 ESLint 的工作原理,那么 AST 毫无疑问是极其重要的一部分。AST 是源代码语法结构的一种抽象表示,它以树状的形式表现编程语言的语法结构,树上的每个节点都表示源代码的一种结构。
AST如何生成JavaScript 执行的第一步是读取文件中的字符流,然后通过词法分析生成 token,之后再通过语法分析( Parser )生成 AST,最后生成机器码执行。
整个解析过程主要分为以下两个步骤:
- 分词:将整个代码字符串分割成最小语法单元数组
- 语法分析:在分词基础上建立分析语法单元之间的关系
JS Parser 是 JavaScript 语法解析器,它可以将 JavaScript 源码转成 AST,常见的 Parser 有 Esprima[7]、Acorn[8]。
词法分析词法分析,也称之为扫描(scanner),简单来说就是调用 next() 方法,一个一个字母的来读取字符,然后与定义好的 JavaScript 关键字符做比较,生成对应的 Token。Token 是一个不可分割的最小单元:
例如var这三个字符,它只能作为一个整体,语义上不能再被分解,因此它是一个 Token。
词法分析器里,每个关键字是一个 Token ,每个标识符是一个 Token,每个操作符是一个 Token,每个标点符号也都是一个 Token。除此之外,还会过滤掉源程序中的注释和空白字符(换行符、空格、制表符等。
最终,整个代码将被分割进一个 tokens 列表(或者说一维数组)。
语法分析语法分析会将词法分析出来的 Token 转化成有语法含义的抽象语法树结构。同时,验证语法,语法如果有错的话,抛出语法错误。
AST Explorer[9] 是一个工具网站,它能查看代码被解析成 AST 的样子。
编写 ESLint 插件介绍了原理之后,接下来就是我们的实战部分了。有些时候,已有的 Lint 规则并不能满足项目需求,我们可以根据需求创建自己的规则。
接下来我们以一个简单的需求为例,开发一个属于我们自己的 ESLint 插件。
需求:使用const声明基本类型的变量时,变量名不能出现小写字母。
初始化项目想要创建一个 ESLint rule,首先需要创建一个 ESLint 插件。我们在 plugins 部分有提到过,插件是一个以eslint-plugin开头的 npm 模块,这是 ESLint 官方规定的。
首先初始化package.json,内容如下:
{
"name": "eslint-plugin-awesome-rules"
"version": "1.0.0"
"main": "index.js"
"author": "dapangmao"
"license": "MIT"
}
复制代码
创建规则
之后在根目录创建一个index.js文件,用来存放 rule 的具体逻辑。
const hasLowerCase = (str) => /[a-z]/.test(str);
module.exports = {
rules: {
'constant-capitalization': {
meta: {
type: 'suggestion'
docs: {
description: 'disallow lowercase alphabets in constants declaration'
}
}
create: function (context) {
return {
VariableDeclarator: function (node) {
if (
node.parent.kind === 'const' &&
hasLowerCase(node.id.name) &&
node.init.type === 'Literal'
) {
context.report(
node
'Please use capitalized casing for constants'
);
}
}
};
}
}
}
};
复制代码
其实到这里一个基本的插件我们就创建完成了,只包含两个文件:package.json和index.js。这个插件只提供了一个规则:constant-capitalization。下面来详细介绍一下 rule 部分的具体内容。
插件中的每个规则都必须包含两条属性:meta和create。
- meta:元数据,包含了规则的通用信息,比如规则的类型,以及一些用来用来描述规则的信息。
- create:一个函数,它将逐个节点访问整个代码的语法树,并让我们对节点进行操作。参数context包含与规则上下文相关的信息,这个函数返回一个对象,对象的属性是 AST 中的选择器,ESLint 会收集这些选择器,在 AST 遍历过程中会执行所有监听该选择器的回调。
回到我们的规则本身,为了找到符合条件的节点,我们需要观察代码解析成 AST 的结果,下面的截图是在 AST Explorer[10] 中输入const foo = '123';得到的 AST:
通过观察 AST 可以发现通过node.parent.kind === 'const' && hasLowerCase(node.id.name) && node.init.type === 'Literal'就可以过滤出符合条件的节点。对于符合条件的节点,调用context.report来发布警告或错误(取决于你所使用的配置)。该方法只接收一个参数,是个对象。
测试了解了规则的实现后,让我们通过一个实际的例子,来测试一下我们编写的规则。
首先在当前插件的根目录执行yarn link,你会看到下面类似的输出。
测试项目我们可以继续使用在「安装与使用」章节创建的项目,首先运行yarn link "eslint-plugin-awesome-rules",把这个模块链接到我们编写的的本地插件。之后运行yarn add eslint-plugin-awesome-rules@link:1.0.0把插件添加到package.json中。
// 在 eslint-plugin-awesome-rules 根目录中
eslint-plugin-awesome-rules % yarn link
// 在测试项目中
eslint-start % yarn link "eslint-plugin-awesome-rules"
eslint-start % yarn add eslint-plugin-awesome-rules@link:1.0.0
复制代码
我们在前面的 plugins 部分提到过,安装好插件之后,还需要在 ESLint 的配置文件中进行配置。配置好的.eslintrc.json文件长这样:
{
"env": {
"browser": true
"es2021": true
}
"plugins": ["awesome-rules"]
"parserOptions": {
"ecmaVersion": "latest"
"sourceType": "module"
}
"rules": {
"awesome-rules/constant-capitalization": "error"
}
}
复制代码
之后我们把index.js文件中的内容改成下面的内容:
const myName = 'dapangmao';
复制代码
运行命令:yarn run eslint index.js,就会在控制台看到我们期望的输出:
至此,一个最简单的 ESLint 插件就创建完成了。
使用 Yeoman generator上面我们通过手动创建项目来编写了一个插件,是为了让示例尽量精简,只专注在规则本身。但是如果我们想把编写的插件发布到 npm,更推荐大家使用 Yeoman generator[11]。
Yeoman generator 是 ESLint 官方为我们开发 eslint 插件提供的脚手架,用于生成包含指定框架结构的工程化目录结构。
首先全局安装yo和generator-eslint:
npm install -g yo generator-eslint
复制代码
创建项目目录,使用命令行初始化项目:
mkdir eslint-plugin-awesome-rules-yo
cd eslint-plugin-awesome-rules-yo
yo eslint:plugin # 生成项目骨架
复制代码
命令行会要求你输入一些插件相关的信息,之后会生成一些必要的文件。
如果要创建一个自定义规则,还需要键入下面这个命令,来添加一些创建 rule 相关的文件。
yo eslint:rule
复制代码
最终的文件结构长这样:
lib/rules/constant-capitalization.js文件的内容长这样(删掉了不必要的注释):
/**
* @fileoverview disallow lowercase alphabets in constants declaration
* @author dapangmao
*/
"use strict";
/**
* @type {import('eslint').Rule.RuleModule}
*/
module.exports = {
meta: {
type: null // `problem` `suggestion` or `layout`
docs: {
description: "disallow lowercase alphabets in constants declaration"
category: "Fill me in"
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
return {
// visitor functions for different types of nodes
};
}
};
复制代码
可以发现,和我们手动创建插件的文件内容很像,这个文件就是我们编写 rule 逻辑代码的地方。
使用yo eslint:rule创建规则时,在docs和tests/lib文件夹中各有一个和 rule 同名的文件,这是我们写规则文档和测试的地方,如果我们要发布到 npm,文档和完整的测试还是很有必要的。
总结本篇文章到这里就结束了,我们一步步由浅入深,介绍了 ESLint 的基本使用、工作原理、AST以及如何编写一个插件等,希望这篇文章给你带来了一些收货,让你对 ESLint 有了一个更深入的了解!
掘金@大胖猫, https://juejin.cn/post/7111575564015632397
文章同时收录于小程序-互联网小兵,技术人小程序,收各平台优质热门的技术文章(后端、移动端、算法、人工智能...)!