外观
03.ESLint
12396字约41分钟
2024-05-31
从这一章我们进入到 ESLint 的学习。
linter 发展史
首先和大家来聊一聊关于 linter 的发展史。
静态代码分析
早在 1978 年,Stephen C. Johnson 在 Debug 自己的 C 语言项目时,突然想到为什么不做一个工具来提示自己写的代码哪里有问题呢? 这个工具也被称为 Linter。
Linter 本意指的是衣服上多出来的小球、绒毛和纤维等,如果你刚把晾晒好的衣服收下来就会发现这些小玩意。以前如果想把这些多出来的“残渣”去掉,最简单的方法就是找一个单面胶粘一下再撕开,后来有的人发明了这个神器,一滚就能清除掉:
这就是 Linter 的由来,不过区别是神器重点在 “清除”,而 Linter 重点在 “上报错误”。
Linter 想要提示错误,那首先就得阅读代码,这也是为什么 Linter 也被称为静态代码分析的工具。阅读完之后,再加上我们人为自定义好的一些规则,那么 Linter 就拥有了提示错误的能力了。
JSLint
在 2002 年,Douglas Crockford 就为 JavaScript 写了第一个 Linter 工具:JSLint。
你现在也可以在这个网站上粘贴你的 JavaScript 代码来检查有没有问题。
JSLint 的优点就是 开箱即用,不需要配置太多的东西,相当于拎包入住。但优点也是缺点,就是 规则太严格,完全不可扩展和自定义配置,连配置文件都没有。
JSHint
在 JSLint 的基础上,在 2010 年的时候 Anton Kovalyov 跟其它人就 fork 了一份 JSLint 然后改造成了 JSHint。
你可以在这个网站访问到 JSHint
这个工具与 JSLint 的思路正好相反,它的默认规则非常松散,自由度非常高。但是也同样带来了问题:你需要非常了解这些规则才能配出一个好用的规则表。因为规则太不严格,过于自由,所以单纯靠默认的规则跟没有配置 Linter 一样。
JSCS
前面的 JSLint 和 JSHint 主要功能都是检查代码质量问题的,JSCS (JavaScript Coding Style) 则是一个代码风格检查器。
它有超过 90 条规则,你也能自己创建规则,不过这些规则主要是和代码风格、代码格式化有关,它不会报任何和 JS 代码质量相关的错误。
尽管 JSCS 在其活跃时期非常受欢迎,但它已于 2016 年被宣布停止维护,并建议用户迁移到 ESLint。ESLint 是一个更强大、更灵活的工具,它不仅可以检查代码风格,还可以发现潜在的错误和代码质量问题。另一个流行的代码格式化工具是 Prettier,它专注于自动格式化代码,而不提供任何代码质量检查。
虽然 JSCS 不再被维护,但它的一些功能和理念已经被 ESLint 和 Prettier 等现代工具所采纳。如果你正在寻找一个代码风格检查器和格式化器,建议使用 ESLint 和 Prettier 来替代 JSCS。这两个工具可以很好地协同工作,ESLint 负责检查代码质量,而 Prettier 负责自动格式化。
ESLint
接下来就是我们的主角 ESLint 了。
2013 年,一个叫 JSChecker 的小项目被改名成我们如今非常熟悉的 ESLint。
ES6 上线了之后,JSHint 受不了直接投降了,因为它不支持这些 ES6 新语法。而 ESLint 正好异军突起,马上用 Esprima (一个高性能的 ECMAScript parser)支持所有 ES6 新语法,并对新语法做好了校验。
除了基础的 ES6 代码质量校验,ESLint 还支持代码风格的规则。开发者不仅可以自定义项目要用哪些规则,也能直接无脑使用社区上制定的规则(比如 eslint-config-airbnb)。
这一波操作也让 ESLint 成为现在 JavaScript 的一个标准的 Linter 了。然而,关于 Linter 的故事还没结束。
2012 年微软公布了第一版的 TypeScript,随之而来的还有一个叫 TSLint 的 Linter。
在那段时间里,TSLint 是 TypeScript 的标准 Linter 工具,ESLint 则为 JavaScript 标准 Linter。它们各有自身特色:ESLint 有 TSLint 所没有的一些语法特性支持,而 TSLint 可以对代码进行静态分析和类型检查。
可是,一份代码还要两个 Linter 并行检查属实有点让人不爽。TSLint 也经常和 ESLint 的人探讨应该用哪个作为主力 Linter。TS 的社区也有很多声音希望优先满足 JSer 的需求,毕竟 TS 是 JS 的超集嘛,还是以 ESLint 为主。
最终,在 2019 年 TSLint 宣告不再维护,以后就是 ESLint 的天下了。
ESLint 核心概念
接下来我们来了解一下 ESLint 的核心概念,这个部分很重要,因为我们后期的学习就是围绕着这几个方面展开的。
ESLint 的核心概念包括以下几点:
规则 (Rules):规则是 ESLint 的核心,它们是独立的脚本,用于检查代码中的特定问题。ESLint 有许多内置规则,这些规则可以覆盖各种编码风格和潜在错误。规则是可配置的,每个规则可以被启用或禁用,并可以设置为警告或错误级别。
配置 (Configuration):ESLint 允许通过配置文件自定义规则的启用和设置。配置文件可以是 .eslintrc.* 格式的文件或 package.json 文件里的 eslintConfig 字段。配置可以继承其他配置,这使得可以轻松地共享和组合规则集。共享配置通常是一个 *npm* 包,可以被多个项目使用。
插件 (Plugins):插件是可扩展 ESLint 功能的方式,它们包含一组自定义规则和/或处理器(见下文)。这使得 ESLint 可以适应不同的编码风格和技术栈。插件可以通过 npm 安装并在配置文件中引用。
处理器 (Processors):处理器是一个可选的插件特性,它可以对非 JavaScript 文件进行预处理,以便 ESLint 可以检查这些文件中嵌入的 JavaScript 代码。例如,HTML 文件中的 <script> 标签或 Markdown 文件中的代码块。
命令行接口 (CLI):ESLint 提供了一个命令行接口,用于在终端中执行 linting 操作。CLI 允许用户指定一个或多个文件、目录或 glob 模式以进行检查。CLI 还支持许多选项,这些选项可以覆盖配置文件中的设置,如禁用特定规则、规定输出格式等。
ESLint 快速上手
首先创建一个 eslint-demo 的项目,使用 pnpm init 进行格式化,安装 eslint
pnpm add eslint -D
接下来在项目根目录下面创建一个 src/index.js,代码如下:
const hello = 'world';
console.log(hello);
function sayHello(name) {
console.log('Hello, ' + name + '!');
}
sayHello('world');
上面随便写了一些代码,接下来在项目根目录下面创建一个 eslint 的配置文件 .eslintrc,里面会书写一些配置信息:
{
"env": {
"browser": true,
"es2021": true
},
"extends": "eslint:recommended",
"parserOptions": {
"ecmaVersion": 12,
"sourceType": "module"
},
"rules": {
"indent": ["error", 2],
"quotes": ["error", "single"],
"semi": ["error", "always"]
}
}
env:主要是定义预设的全局变量
- browser:这份配置适用于浏览器环境,预定义了诸如 window、document 之类的浏览器才会有的全局变量
- es2021: 表示我们使用的是 ES 2021 的标准,肯定会预定义一些新版本的全局变量,Promise、Symbol 这些全局变量是支持的
extends:这里我们所设置的值为 eslint:recommended,这其实是 ESLint 团队推荐的一组核心规则,你可以将其视为最佳实践
parserOptions:和解析器相关的配置
- ecmaVersion:使用的 ECMAScript 的版本,12 也就是 2021
- sourceType:模块类型,这里设置为 module,表示我们使用的 ESM 模块规则,支持 import 和 export 语法
rules:定义代码风格,功能类似于 prettier
- indent:缩进,我们这里设置的是两个空格,如果不符合要求,会报 error 类型的错误
- quotes:引号的设置,这里我们设置的是单引号,如果不符合要求,会报 error 类型的错误
- semi:每一条语句添加分号,如果不符合要求,会报 error 类型的错误
最后修改 package.json,添加如下的 script 脚本命令:
"scripts": {
// ...
"lint": "eslint ."
},
上面的脚本命令表示对当前项目所有的 js 文件进行 lint 检查。
使用 ESlint 进行代码检查的时候,是支持自动修复的,但是并非所有的错误都能够自动修复,只能够修复一部分。
要自动进行修复,只需要添加命令行参数 --fix 即可
"scripts": {
// ...
"lint": "eslint --fix ."
},
检查规则
这节课我们主要学习 ESLint 里面的规则相关的知识。规则是 ESLint 中一个比较重要的核心概念之一,因为究竟报不报错,是由规则来确定的。
规则的重要性
在 ESLint 中,本身可以配置规则的重要性,总共分为三个级别:
- off 或者 0: 关闭这条规则
- warn 或者 1:这条规则的级别为警告级别
- error 或者 2:这条规则的级别为错误级别
例如:
{
"rules": {
"no-undef": "error",
"semi": ["warn", "always"]
}
}
在上面的规则配置中,semi 对应的值为一个数组,数组的第一项是上面所说的规则重要性,第二项则是该条规则配置可选项,关于这个配置可选项,不同的规则能够填入的值是不一样的。关于具体能够填写的值,那么就要去这条规则的说明页面去查阅。
接下来我们就针对 semi 这条规则做一个介绍,semi 可配置值如下:
- always:这是默认值,代表语句结束需要插入分号
- never: 在没有 ASI 风险情况下,不需要插入分号
ASI 英语全称叫做 automatic semicolon insertion,这个翻译成中文就是自动分号插入。所谓 ASI 风险,是指由于有这个机制,可能会导致意外的行为或者错误。
function example() {
return;
{
message: 'Hello, world!';
}
}
在上面的代码中,我们本意是要返回一个对象,但是由于 ASI 机制,这里就会产生意外的行为,导致这个函数返回一个 undefined 而非预期的对象。
如果值为 always,那么还可以配置一个额外的对象:
- omitLastInOneLineBlock:配置为 true,表示禁止在单行代码块中的最后一个语句使用分号
- omitLastInOneLineClassBody:配置为 true,表示禁止在单行类里面的最后一个语句使用分号
如果值为 never,那么也是可以配置一个额外的名为 beforeStatementContinuationChars 的对象:
- "beforeStatementContinuationChars": "any"(默认):如果下一行以[, (, /, +, 或 -]开始,则忽略语句末尾的分号(或缺少分号)
let a = 1 + 1; // 正确:分号被忽略
let b = 2;
+2; // 正确:分号也可以
- "beforeStatementContinuationChars": "always":如果下一行以[, (, /, +, 或 -]开始,则要求在语句末尾使用分号
let a = 1 + 1; // 错误:要求在语句末尾使用分号
let b = 2;
+2; // 正确:添加了分号
- "beforeStatementContinuationChars": "never":即使下一行以[, (, /, +, 或 -]开始,只要没有引起 ASI(Automatic Semicolon Insertion,自动分号插入)的风险,也禁止在语句末尾使用分号
let a = 1 + 1; // 正确:没有 ASI 风险,不需要分号
let b = 2;
+2; // 错误:不允许在没有 ASI 风险的情况下使用分号
规则注释
在具体的代码文件里面,可以以注释的方式来配置规则
/* eslint eqeqeq: "off", curly: "error" */
/* eslint eqeqeq: 0, curly: 2 */
/* eslint quotes: ["error", "double"], curly: 2 */
规则注释的优先级会高于配置文件里面的规则。
一般在如下的场景中可能会涉及到使用注释规则:
- 针对特定的文件或者代码片段需要指定特殊规则,比如我们针对某一个代码片段去禁用 ESLint 检查
/* eslint-disable */
console.log('Hello');
/* eslint-enable */
或者只禁用某一个规则
/* eslint-disable semi */
console.log('Hello');
/* eslint-enable semi */
- 指定某个文件的特殊配置,有时我们需要针对某个文件指定和其他文件不同的 ESLint 配置,这种情况下也可以使用注释的形式,这样就不需要去修改主要的配置文件
/* eslint-env node, mocha */
在上面的注释汇总,我们声明 ESLint 的检查环境为 node 和 mocha,这就意味着在检查该文件的时候,ESLint 会预设一些 node 和 mocha 中的全局变量,比如 process、describe、it。
- 临时禁用某条规则
// eslint-disable-next-line no-unused-vars
const tempVariable = 'Temporarily not used';
在上面的注释中,我们使用了 eslint-disable-next-line,代表只禁用下一行的代码检查,后面跟上了具体的规则,表示禁用下一行代码的某一条规则的检查,不影响之后的代码。
另外在配置文件中,有如下的配置选项:
- noInlineConfig:禁止行内注释形式的规则
- reportUnusedDisableDirectives:用于是否报告有未使用的 eslint-disable 指令
例如:
/* eslint-disable-next-line no-console */
console.log('Hello');
上面的代码是可以正常工作的,eslint-disable-next-line no-console 这条行内注释规则是有用的,但是如果我们把下面的 console 注释调用:
/* eslint-disable-next-line no-console */
// console.log('Hello');
那么上面的这一条行内注释规则就变成了一条无用的注释规则
更多关于行内注释规则,可以参阅官网资料:https://eslint.org/docs/latest/use/configure/rules#using-configuration-comments
规则参照表
你可以在 https://eslint.org/docs/latest/rules/ 看到 ESLint 里面的所有规则
在官方的规则参照表中,每一条规则后面有三个符号,对应的含义如下:
✅ | 🔧 | 💡 |
---|---|---|
在配置文件中的 "extends": "eslint:recommended" 属性会启用此规则。 | 此规则报告的一些问题可以通过 --fix 参数自动修复。 | 此规则报告的一些问题可以通过编辑器建议手动修复。 |
配置文件 part1
首先需要注意,在谢老师讲课的当前的这个节点,配置文件系统处于一个更新期,存在两套配置文件系统,旧的配置文件系统适用于 v9.0.0 之前的版本,而新的配置文件系统适用于 v9.0.0 之后的版本,但是目前就讲课的这个节点,还处于 v8.x.x 的大版本。
配置文件格式
在 ESLint 中,支持如下格式的配置文件:
- JavaScript:使用 .eslintrc.js 并且导出一个包含你配置的对象
- JavaScript(ESM):在 v9.0.0 之前 ESLint 是不支持 ESM 风格模块化的,假设我们的源码使用的 ESM 模块化风格,并且我们在 pacakge.json 中明确配置了 type: module,这个时候就需要将 ESLint 的配置文件命名为 .eslintrc.cjs(也就是说要使用 CommonJS 风格来命令 ESLint 的配置文件)
- YAML:使用 .eslintrc.yaml 或者 .eslintrc.yml
- JSON:使用 .eslintrc.json 来配置 ESLint
- package.json:在 pacakge.json 中,可以创建一个名为 eslintConfig 的属性,然后对 ESLint 进行配置
如果在项目的同一目录下存在多种格式的配置文件,那么这些配置文件之间是有一个优先级顺序的。顺序如下:
.eslintrc.js
.eslintrc.cjs
.eslintrc.yaml
.eslintrc.yml
.eslintrc.json
package.json
在早期的时候(v7.0.0 之前),ESLint 支持使用 .eslintrc 文件来作为 ESLint 的配置文件,但是从 v7.0.0 开始,官方就已经明确废弃掉这种用法,从 v7.0.0 之后,就建议使用上述的格式来作为 ESLint 的配置文件。但是为了兼容性,之前的 .eslintrc 格式的配置文件依然能够使用,但是还是建议最好使用官方推荐的格式来进行配置。
使用配置文件
想让我们的配置文件生效,有两种方式:
- 在项目中创建上述的配置文件,ESLint 在做检查的时候会自动寻找配置文件并应用里面的配置
- 在 CLI 命令中通过 --config 选项来手动指定配置文件的位置
eslint -c myconfig.json myfiletotest.js
配置文件的层叠
在 ESLint 中支持配置文件的层叠,这是一种管理项目中多个配置文件的方式,这种特性允许你在项目中根据不同的部分应用不同的规则。
例如我们在 src/.eslintrc.js 中,有如下的配置:
module.exports = {
env: {
browser: true,
es2021: true,
node: true,
},
rules: {
semi: ['error', 'always'],
},
};
那么现在,我们就存在两份 ESLint 的配置,此时 ESLint 会在当前目录下查找配置文件,然后会一层一层往上寻找,将找到的所有的配置文件进行一个规则合并。
如果子目录下配置文件的规则和父目录下的配置文件规则发生重合,那么子目录下的配置文件规则会覆盖父目录下配置文件的同名规则。
如果我们需要就应用当前目录的配置文件,不要再往上找了,那么可以在当前的配置文件中添加一个 root:true,添加了此配置项后,表示就应用当前目录下找到的配置文件,停止继续往上搜索。
目前我们知道,要对 ESLint 进行配置有多种方式:
- 配置文件方式
- 行内注释方式
- CLI 命令行
那么有这么几种方式,优先级如何呢?优先级顺序从高到低如下:
- 行内注释配置方式
- CLI 命令行配置方式
- 配置文件的方式(虽然它的优先级是最低的,但却是用得最多的)
- 从 ESLint v8.0.0 开始,已经不再支持个人配置文件(你把你的配置文件是写在项目之外的,放在你的主目录 ~ 下面的),也就是说,如果你的电脑主目录下存在配置文件,ESLint 不会去搜索到那儿,会自动忽略那里的配置文件。
扩展配置文件
这里所谓的扩展,实际上更准确的来讲,叫做继承。
{
"extends": "eslint:recommended",
}
在上面的配置中,extends 对应的值为 eslint:recommended,表示采用 ESLint 团队推荐的规则规范。
在继承了 eslint:recommended 规则规范的基础上, 是可以进行额外的配置。
{
"extends": "eslint:recommended",
"rules" : {
"no-console": "warn"
}
}
但是在进行原有配置规则的扩张的时候,有一个细节上面的问题:
{
"extends": "eslint:recommended", // "eqeqeq": ["error", "allow-null"]
"rules" : {
"eqeqeq": "warn"
}
}
在上面的扩展中,我们修改了 eqeqeq 这条规则的重要性,从 error 修改为了 warn,当你修改规则重要性的时候,原本的配置选项会保留,也就是说,上面关于 eqeqeq 这条规则,最终会变为
"eqeqeq": ["warn", "allow-null"]
但是如果你更改的是配置选项,那么则是完全覆盖。
{
"extends": "eslint:recommended", // "quotes": ["error", "single", "avoid-escape"]
"rules" : {
"quotes": ["error", "double"]
}
}
在上面的例子中,我们修改了 quotes 规则的配置选项,改为了 double,那么新的配置选项会对旧的("single", "avoid-escape")进行完全覆盖。
另外关于 extends 对应的值还可以是一个数组:
{
"extends": [
"./node_modules/coding-standard/eslintDefaults.js",
"./node_modules/coding-standard/.eslintrc-es6",
"./node_modules/coding-standard/.eslintrc-jsx"
],
"rules": {
"quotes": "warn"
}
}
局部重写
有些时候,我们需要对配置进行更加精确的控制,例如都是在同一个目录下,不同的文件使用不同的配置,这种情况下就可以使用局部重写(overrides)
{
"rules": {
"quotes": ["error", "double"]
},
"overrides": [
{
"files": ["bin/*.js", "lib/*.js"],
"excludedFiles": "*.test.js",
"rules": {
"quotes": ["error", "single"]
}
}
]
}
例如,假设我们有如下的项目结构:
any-project/
├── .eslintrc.js
├── lib/
│ ├── util.js
│ └── other.js
└── src/
├── index.js
└── main.js
在 .eslintrc.js 配置文件中,我们书写了如下的配置代码:
{
"rules": {
"quotes": ["error", "double"]
},
"overrides": [
{
"files": ["lib/*.js"],
"rules": {
"quotes": ["error", "single"]
}
}
]
}
在上面的配置文件中,我们使用了局部重写,src 目录下面的所有 js 文件使用双引号,lib 目录下面所有的 js 文件使用单引号。
overrides 对应的值是一个数组,那么这意味着可以有多个配置项,当多个配置项之间匹配上了相同的文件,那么以后面的配置项为准。
{
"rules": {
"quotes": ["error", "double"]
},
"overrides": [
{
"files": ["**/*.js"],
"rules": {
"quotes": ["error", "single"]
}
},
{
"files": ["lib/*.js"],
"rules": {
"quotes": ["error", "double"]
}
}
]
}
overrides 是支持嵌套,例如:
{
"rules": {
"quotes": ["error", "double"]
},
"overrides": [
{
"files": ["lib/*.js"],
"rules": {
"quotes": ["error", "single"]
},
"overrides": [
{
"files": ["util.js"],
"rules": {
"quotes": ["error", "double"]
},
}
]
}
]
}
配置文件 part2
首先再次强调,在我当前讲课的这个时间节点(2023.7.21),新版本的配置文件系统还没有正式生效,因为目前最新的版本为 v8.x.x,新版本的配置文件系统要到 v9.0.0 才正式生效。
这也意味着当前我们所介绍的内容,在未来还有变化的余地,如果之后看到课程内容和官方文档有所出入,以官方为准。
配置文件的书写
从 v9.0.0 开始,官方推荐的配置文件格式为 eslint.config.js,并且支持 ESM 模块化风格,可以通过 export default 来导出配置内容
export default [
{
rules: {
semi: 'error',
'prefer-const': 'error',
},
},
];
之所以导出的是一个数组,是因为为了支持项目中不同的文件或者文件类型定义不同的规则。
例如,你的项目里面既有 JS 代码,也有 TS 代码,你可能想要针对不同的代码类型配置不同的 ESLint 检查规则,这里就可以这样写:
module.exports = [
{
files: ['*.js'],
rules: {
'no-var': 'error',
},
},
{
files: ['*.ts'],
rules: {
'@typescript-eslint/no-var': 'error',
},
},
];
如果你在 package.json 里面没有指定 type: module,那么就代表你使用的是 CommonJS 规范,那么 ESLint 配置文件在做模块导出的时候,也需要使用 CommonJS 模块规范
module.exports = [
{
rules: {
semi: 'error',
'prefer-const': 'error',
},
},
];
配置对象的选项
具体的配置选项如下:
- files - 一个含有 glob 模式的数组,指示应用配置对象的文件。如果未指定,配置对象应用于所有由任何其他配置对象匹配的文件。
- ignores - 一个含有 glob 模式的数组,指示配置对象不应用于的文件。如果未指定,配置对象应用于所有由 files 匹配的文件。
- languageOptions - 一个包含与 JavaScript 的 lint 设置有关的设置对象。。
- ecmaVersion - 支持的 ECMAScript 版本。可能是任何年份(例如,2022)或版本(例如,5)。设置为 "latest" 表示最近支持的版本。(默认:"latest")
- sourceType - JavaScript 源码的类型。可能的值为 "script" 表示传统脚本文件,"module" 表示 ECMAScript 模块(ESM),以及 "commonjs" 表示 CommonJS 文件。(默认情况下 "module" 对应 .js 和 .mjs 文件,"commonjs" 对应 .cjs 文件)
- globals - 一个对象,指定在 linting 过程中应添加到全局作用域的额外对象。
- parser - 包含 parse( ) 方法或 parseForESLint( ) 方法的对象。(默认值为 espree)
- parserOptions - 一个对象,指定直接传递给 parser 上的 parse( ) 或 parseForESLint( ) 方法的额外选项。可用的选项依赖于解析器。
- linterOptions - 包含与 linting 相关配置的对象。
- noInlineConfig - 布尔值,指示是否允许内联配置。
- reportUnusedDisableDirectives - 布尔值,控制是否报告未使用的 eslint-disable 指令
- processor - 包含 preprocess( ) 和 postprocess( ) 方法的对象,或者指示插件内部处理器名称的字符串(例如,"pluginName/processorName")。
- plugins - 包含插件名称到插件对象的名称-值映射的对象。当指定了 files 时,这些插件仅对匹配的文件可用。
- rules - 包含具体配置规则的对象。当指定了 files 或 ignores 时,这些规则配置仅对匹配的文件可用。
- settings - 一个包含键值对信息的对象,这些信息应对所有规则都可用。
整体来讲,上面的配置项不算多,而且很多配置项我们在前面是已经接触过的。
下面是一些之前没有接触过的配置项:
globals:该配置项位于 languageOptions 配置项下面,用于配置一些全局的设定:
export default [
{
files: ['**/*.js'],
languageOptions: {
globals: {
var1: 'writable',
var2: 'readonly',
},
},
},
];
在上面的配置中,我们指定了 var1 这个变量是可写的,但是 var2 这个变量是只读的。
假设你有如下的代码
var1 = 100;
var2 = 200; // 报错
parsers:配置解析器。解析器的作用是负责将源码解析为抽象语法树。ESLint 默认使用的解析器为 Espree,但是你可以指定其他的 parser,parser 需要是一个对象,该对象里面包含了 parse 或者 parseForESLint 方法。
import babelParser from '@babel/eslint-parser';
export default [
{
files: ['**/*.js', '**/*.mjs'],
languageOptions: {
parser: babelParser,
},
},
];
在上面的配置中,我们就指定了其他的 parser 来解析源码。
processor:这个是处理器,主要用于处理 ESLint 默认不能够处理的文件类型。举个例子,假设有一个 markdown 类型的文件,里面有一些 JS 代码,默认这些 JS 代码是不能够被 ESLint 处理的,通过添加额外的处理器,让 ESLint 能够对这些格式的文件进行 lint 检查
import markdown from 'eslint-plugin-markdown';
export default [
{
files: ['**/*.md'],
plugins: {
markdown,
},
processor: 'markdown/markdown',
settings: {
sharedData: 'Hello',
},
},
];
CLI 命令行工具
关于 CLI 命令行工具,我们在第一节课的时候就用到过一个:
eslint --fix .
在官网,我们可以看到 CLI 命令行工具的基本格式为:
eslint [options] [file|dir|glob]*
我们先来看后面的 [file|dir|glob]* , 这个部分主要是用来指定 ESLint 应该检查哪些文件:
- file:用于指定一个具体的文件名
eslint app.js # 使用 eslint 检查 app.js 这个文件
- dir:指定一个目录
eslint src/
# 检查 src 目录下面的所有文件
- glob:这个 glob 是一种模式,有点类似于正则表达式,专门用来匹配文件的路径,glob 模式下面可以使用一些特殊的字符( * ? [])来匹配文件名
eslint src/**/*.js
# 检查 src 以及下面所有子目录下的所有 js 文件
- *:表示你可以指定多个文件或者目录或者 glob 模式
学习 CLI 命令行工具,主要是 options 这一块,这一块的配置选项是相当丰富的,这里我们对众多的 options 选项进行一个分类,然后每一类选择几个典型的命令来进行介绍。
- 基本配置
- 特殊规则和插件的配置
- 自动修复
- 忽略文件
- 输出
- 缓存
基本配置
--no-eslintrc
告诉 ESLint 忽略所有的配置文件,当你使用这个 option 的时候,ESLint 只会使用内置的规则集来对匹配上的文件进行检查
eslint --no-eslintrc .
-c
, --config
允许我们指定配置文件的路径
eslint -c ~/my-eslint.json file.js
~ 在类 Unix 系统里面表示用户根目录
--env
该配置项允许我们指定一些环境,当指定了具体的环境之后,那么就会预设一些该环境下才会有的全局变量。
eslint --env browser,node file.js
eslint --env browser --env node file.js
在上面的 CLI 命令中,指定了 browser 以及 node 环境,指定了这两个环境之后,就会预设一些 window、process 之类的全局变量
--ext
允许我们指定 ESLint 要检查的文件的扩展名,默认情况下,ESLint 只检查 js 文件。
eslint . --ext .ts # 检查 ts 文件
eslint . --ext .js --ext .ts # 检查 js 和 ts 文件
eslint . --ext .js,.ts # 和上面一样,换了一种写法
--global
该配置项允许我们定义全局变量。例如我们的项目使用到了 jQuery,但是这个 ESLint 是不认识的,所以这里我们就可以使用 global 来定义这个全局变量
eslint --global jQuery:true .
--parser
这个选项允许你指定一个自定义的 JavaScript 解析器。默认情况下,ESLint 使用 Espree,但是你可以使用其他的解析器。例如,你可以使用 Babel-ESLint,如果你的项目中使用了 Babel 和 ESLint,你可以使用它来解析你的 JavaScript 代码。
特殊规则和插件的配置
--plugin
该配置项是用来指定要使用插件。
eslint --plugin jquery file.js # 指定使用 jquery 这个插件
--rule
该配置项就是指定检查的规则,一般来讲,检查规则是写到配置文件里面。但是针对某些场景下单独的一两条规则要改变,可以使用这种方式
eslint --rule 'quotes: [error, double]' .
自动修复
--fix
表示自动修复,但是需要主要,不是所有的问题 ESLint 都可以帮你修复。
--fix-type
允许你指定修复问题的类型,对应的值有 problem、suggestion、layout、directive
- problem:修复代码中的潜在错误,这种类型的问题通常是代码错误,如果不修复,可能会导致程序运行错误。
- suggestion:对代码应用改进性的修复,这种类型的问题通常不会导致程序错误,但修复它们可以改进代码,使代码更易读、更易维护,或更符合最佳实践。这些问题可能涉及到代码的优化、重构或者一些编程习惯的改进。例如,未使用的变量、复杂的表达式可以简化、不必要的代码重复等,都属于 suggestion 类型的问题。
- layout:应用不改变程序结构(抽象语法树,AST)的修复,主要涉及到代码的格式和样式。这些问题不会影响代码的功能或语义,但是修复它们可以使代码更具可读性和一致性。例如,不正确的缩进、缺失的分号、超过设定长度的行等,都属于 layout 类型的问题。
- directive:对内联指令(如 // eslint-disable)应用修复
注意上面的修复问题类型是可以同时指定多个的
eslint --fix --fix-type suggestion --fix-type problem .
忽略文件
--ignore-path
很明显是指定忽略文件的路径。
所谓忽略文件,就是指在项目中可以创建一个 .eslintignore 的文件,该文件里面记录一些文件名或者目录名,ESLint 在进行代码检查的时候,会忽略这些匹配上的文件名或者目录下面的文件
eslint --ignore-path tmp/.eslintignore file.js
--no-ignore
忽略所有的忽略指令。本来 .eslintignore 文件里面记录了 ESLint 在进行检查的时候要忽略那鞋文件,当你用了这个指令之后,相当于你的 .eslintignore 文件失效了,里面记录的那些文件都要被 ESLint 检查
eslint --no-ignore .
--ignore-pattern
简单来说,就是将原本你应该写在 .eslintignore 里面的文件或者目录,写在了命令行里面
eslint --ignore-pattern "/lib/" --ignore-pattern "/src/vendor/*" .
输出
-o
, --output-file
允许将 ESLint 的检查报告输出到一个文件里面
eslint -o report.txt .
在上面的配置中,ESLint 会将最终的检查结果报告输出到 report.txt 的文件中
-f
, --format
正常情况下,ESLint 的检查报告在控制台进行输出,那么这个指令可以配置输出的格式
"stylish"(默认):这是 ESLint 默认的格式化选项。它以易于读取的方式显示 linting 结果,对于每个文件,它会列出所有的错误和警告,然后在下面显示一个摘要,包括总的错误和警告数量。
"compact":这种格式更加简洁。它将每个错误或警告限制为一行,其中包括文件名、行号、列号和问题描述。这种格式适合那些希望尽可能节省空间的情况。
"tap" 是一个代表 "Test Anything Protocol" 的缩写,这是一个简单的文本格式,用于记录和通信测试结果。ESLint 会按照 TAP 规范来输出 linting 结果。这种格式特别适合于 CI/CD(持续集成和持续部署)环境,因为很多 CI/CD 工具都支持解析 TAP 格式的输出。
缓存
--cache
该配置项表示在进行 ESLint 检查的时候,生成一个缓存文件 .eslintcache,缓存文件默认在当前目录下面,有了缓存文件之后,下一次 ESLint 在做检查的时候速度会更快
eslint --cache .
--cache-location
我们可以指定缓存文件的位置
eslint "src/**/*.js" --cache --cache-location "/Users/user/.eslintcache/"
--cache-strategy
指定生成缓存时的缓存策略,对应的策略值有两个:
metadata
:这是默认值,使用文件的元数据(修改时间和文件大小)来判断文件是否发生了变化content
:基于文件的内容来判断文件是否发生变化
eslint "src/**/*.js" --cache --cache-strategy content
APIs
While ESLint is designed to be run on the command line, it’s possible to use ESLint programmatically through the Node.js API. The purpose of the Node.js API is to allow plugin and tool authors to use the ESLint functionality directly, without going through the command line interface.
一般在如下的场景中,我们会涉及到使用 API 来编程:
- 要将工具集成到代码编辑器或者 IDE 里面
- 自定义 linter 工具
- 一些在线的学习平台
首先我们初始化一个项目 eslint-api-demo,然后使用 pnpm init 进行一个初始化,之后安装 eslint 依赖:
pnpm add eslint
安装的时候一定要注意,这一次安装是安装为项目依赖,而非开发依赖,因为我们是使用的 API 的形式来检查其他项目,本项目类似于提供给其他项目的一个第三方库,因此在我们这个项目中,eslint 即便是在运行期间也是需要的。
接下来在 src 目录下面创建一个 eslint-integration.js 文件,这个是我们的核心逻辑文件
const { ESLint } = require('eslint');
/**
* 创建并返回 eslint 实例对象
* @param {*} overrideConfig
*/
function createESLintInstance(overrideConfig) {
new ESLint({
useEslintrc: false,
overrideConfig,
fix: true,
});
}
/**
* 向外部暴露的方法,用于检查对应的文件
* @param {*} filePaths 要做 lint 检查的文件路径
*/
function lintFiles(filePaths) {
// 创建一个配置对象
// 你可以在这里指定你的配置,也可以通过读取文件的方式从外部进行读取
const overrideConfig = {
env: {
browser: true,
es2021: true,
},
extends: 'eslint:recommended',
parserOptions: {
ecmaVersion: 12,
sourceType: 'module',
},
rules: {
indent: ['error', 2],
quotes: ['error', 'single'],
semi: ['error', 'always'],
'no-console': 'error',
},
};
// 创建一个 eslint 的实例
createESLintInstance(overrideConfig);
}
module.exports = {
lintFiles,
};
在上面的代码中,有一个 new ESLint,ESLint 是 eslint 里面提供的一个类,关于在实例化这个类的时候,配置对象提供了哪些配置项,可以参阅:https://eslint.org/docs/latest/integrate/nodejs-api#-new-eslintoptions
之后我们创建了一个名为 lintAndFix 的方法,该方法负责对具体的代码文件进行 lint 检查以及修复工作:
/**
* 该函数负责对传入的文件做 lint 检查以及修复
* @param {*} eslint
* @param {*} filePaths
*/
async function lintAndFix(eslint, filePaths) {
// 要做 lint 检查,很明显就是调用 eslint 实例对象上面的方法
const results = await eslint.lintFiles(filePaths);
console.log(results);
}
可以看到,内部实际上调用了 eslint 实例对象上面的 lintFiles 方法,关于 eslint 实例对象有哪些方法,可以参阅官方的 API 文档:
https://eslint.org/docs/latest/integrate/nodejs-api#-eslintlintfilespatterns
最后,我们要对检查的结果做一个友好的控制台输出:
/**
* 该方法负责对 lint 后的结果进行一个友好的输出
* @param {*} results
*/
function outputLintingResults(results) {
// 拿到 lint 后错误的总数(包含警告)
const problems = results.reduce(
(a, b) => a + b.errorCount + b.warningCount,
0
);
if (problems > 0) {
console.log('Linging errors found! \n');
const messages = results[0].messages;
for (let i = 0; i < messages.length; i++) {
console.error(chalk.red.bold(' FAIL ') + ' ' + messages[i].message);
}
// dim 是 chalk 库里面的一个方法,用于创建一种暗淡模式的输出
console.log('\n' + chalk.dim(results[0].filePath));
} else {
console.log('No linting errors found');
}
}
注意,上面的代码中,为了美化其输入,我们使用了 chalk 这个库,这个库的最新版本 5.0.0 使用的是 ESM 模块化风格,但是我们这里需要 CommonJS 模块,所以这里可以安装 4.0.0 的版本
ESLint 插件
ESLint 支持插件,使用插件是扩展 ESLint 功能的一种方式,你可以通过插件的方式来自定义新的规则或者处理器,你也可以自己写一个有独特功能的插件发布到 npm 上面。
使用插件能够带来的好处包括:
- 自定义规则:用于验证你的代码是否满足某种预期,如果不满足该预期,应该如何处理。
- 自定义配置:用于定义一组规则和设置,这些规则和设置可以被重复使用,不需要在每个项目中重新定义。
- 自定义环境:用于定义一组全局变量,这些变量在特定环境下(例如浏览器、Node.js、Jest 等)是预定义的。
- 自定义处理器:用于从其他类型的文件中提取 JavaScript 代码,或在进行语法检查之前预处理代码。
插件名称的规范
ESLint 中的插件,每一个插件是一个 npm 模块,命名的格式为 eslint-plugin-<插件名称>,eslint-plugin-jquery、eslint-plugin-react
插件还可以使用 scope 包的形式:
- @<scope>/eslint-plugin-<插件名称>
- @jquery/eslint-plugin-jquery
- 还可以只有前面的 scope:@jquery/eslint-plugin
在 npm 官网上面搜索 eslint-plugin 能够找到很多 ESLint 相关的插件。
使用插件的方式
使用现有的插件,方式非常简单,就分为两步:
- 安装插件
- 在配置文件中进行配置
假设你要使用一个名为 eslint-plugin-react 的插件,首先安装该插件:
pnpm add eslint-plugin-react -D
接下来在配置文件中进行配置:
{
"plugins": ["react"]
}
配置完成后,就可以在配置文件添置该插件所提供的各种规则:
{
"plugins": [
"react"
],
"rules": {
"react/jsx-uses-react": "error",
"react/jsx-uses-vars": "error",
}
}
使用插件实例演示
这里我们来演示 eslint-plugin-vue 插件的使用。
该插件对应的官网地址:https://eslint.vuejs.org/
首先我们初始化一个 vue 的项目:
npm init vue@latest
接下来控制台会出现交互式选择,我们选择安装 ESLint 以及 Prettier
eslint-plugin-vue 提供了一些预定义配置的选项,说明如下:
- "plugin:vue/base":提供设置和规则以启用正确的 ESLint 解析。
针对使用 Vue.js 3.x 的配置:
- "plugin:vue/vue3-essential":包括 "plugin:vue/base",并添加了一些规则以防止错误或意外行为。
- "plugin:vue/vue3-strongly-recommended":在上述配置的基础上,添加了一些能显著提高代码可读性和/或开发体验的规则。
- "plugin:vue/vue3-recommended":在上述配置的基础上,添加了一些强制执行社区主观默认的规则,以确保一致性。
针对使用 Vue.js 2.x 的配置:
- "plugin:vue/essential":包括 "plugin:vue/base",并添加了一些规则以防止错误或意外行为。
- "plugin:vue/strongly-recommended":在上述配置的基础上,添加了一些能显著提高代码可读性和/或开发体验的规则。
- "plugin:vue/recommended":在上述配置的基础上,添加了一些强制执行社区主观默认的规则,以确保一致性。
插件安装好了后,接下来就是这个插件具体带来了哪些规则。例如:
rules: {
'vue/html-quotes': ['error', 'double'],
'vue/html-self-closing': [
'error',
{
html: {
void: 'always',
normal: 'never',
component: 'always'
},
svg: 'always',
math: 'always'
}
]
},
这些规则你在插件的官网都是能够查询到的。
自定义 ESLint 插件
ESLint 插件主要是用来扩展 ESLint 本身没有的功能,这里包括扩展规则、扩展配置、扩展解析器。
90%的 ESLint 插件都是以扩展规则为主,所以这些插件里面会包含大量的自定义规则。
像这一类的插件,一般一条规则会对应一个 JS 文件,JS 文件里面需要导出一个对象:
module.exports = {
// 元数据信息
meta: {},
// 规则具体的实现
create: function () {
return {};
},
};
- meta
这个字段提供这条规则相应的元数据信息:
- type: 描述规则的类型。可以是以下的一个:
- "problem":表示这个规则识别的是可能导致错误的代码问题。
- "suggestion":表示这个规则识别的是可能的改进,以使代码更易于阅读和/或更具可维护性。
- "layout":表示这个规则识别的是布局问题,即风格指南中的问题,而不会影响代码的功能。
- docs:提供关于规则的文档信息,可以包含以下字段:
- description:规则的简短描述,通常用于生成文档。
- recommended:一个布尔值,表示这个规则是否在配置为 "recommended"(推荐)的情况下被启用。
- url:指向规则文档的 URL。
- fixable:说明是否可以自动修复由此规则识别的问题,以及如何修复。如果规则可以自动修复问题,此字段应为 "code" 或 "whitespace",否则应为 null 或省略。
- deprecated:一个布尔值,表示这个规则是否已被弃用。默认为 false。
- create
这个字段对应的是一个函数,该函数会返回一个对象,对象里面又是一个一个的方法,例如有如下的方法:
我们所书写的源代码最终会被解析一个抽象语法树,这个抽象语法树就是一个树结构,里面由一个一个的节点组成的,每一个节点是一个 token,工具在处理你的源码的时候,实际上就会去遍历这棵树,遍历到对应的节点,然后针对对应节点做出相应的处理。
- Program: 这个方法会在遍历抽象语法树开始时被调用。
- FunctionDeclaration:这个方法会在遍历到一个函数声明时被调用。
- VariableDeclaration:这个方法会在遍历到一个变量声明时被调用。
- ExpressionStatement:这个方法会在遍历到一个表达式语句时被调用。
- CallExpression:这个方法会在遍历到一个函数调用时被调用。
- ReturnStatement:这个方法会在遍历到一个 return 语句时被调用。
上面这些方法所对应的名称实际上都来源于 ESTree 规范里面所定义的 AST 节点类型。
这些方法里面接收一个参数,该参数是当前所遍历到的 AST 节点对象,你通过这个节点对象就可以拿到当前节点一些具体的信息以及该节点对应的子节点。
create: function(){
return {
CallExpression(node){
// ...
}
}
}
除了节点处理函数以外,create 方法还会自动传入一个 context 参数
create: function(context){
return {
CallExpression(node){
// ...
}
}
}
该参数提供了一些方法:
- context.report(descriptor):这个方法用于报告一个问题。descriptor 是一个对象,包含了问题的信息,如问题的位置、消息等。
- context.getSourceCode( ):这个方法返回一个 SourceCode 对象,你可以使用它来访问源代码的文本和 AST。
- context.getAncestors( ):这个方法返回一个包含当前节点的所有祖先节点的数组,数组中的第一个元素是最近的祖先。
- context.getScope( ):这个方法返回一个代表当前作用域的 Scope 对象。
补充:
视频中忘记说了,同学们可以在 https://eslint.org/docs/latest/extend/custom-rules 看到自定义规则的完整结构,包括 meta 完整的配置项有哪些,context 完整的方法有哪些等信息。
接下来我们来看一个插件的实战案例。
首先我们需要创建一个插件项目 eslint-plugin-<插件名称>
例如我们的项目叫做 eslint-plugin-customrules,首先使用 pnpm init 进行一个项目初始化,然后在项目根目录中创建 rules 目录,该目录用于存放我们的自定义规则。
接下来我们创建了如下的两条规则:
// 不允许有 alert 语句
// alert("xxx")
module.exports = {
meta: {
type: 'problem',
docs: {
description: 'disallow the use of alert',
category: 'Best Practices',
},
fixable: null,
},
create(context) {
return {
// 这个方法会在遍历到一个函数调用时被调用
CallExpression(node) {
if (node.callee.name === 'alert') {
// 说明当前是一个 alert 的函数调用
context.report({
node,
message: '不允许出现 alert 语句呀,兄弟',
});
}
},
};
},
};
// console.log("xxx")
module.exports = {
meta: {
type: 'problem',
docs: {
description: 'disallow the use of console.log',
category: 'Best Practices',
},
fixable: null,
},
create(context) {
return {
CallExpression(node) {
if (
node.callee.object &&
node.callee.object.name === 'console' &&
node.callee.property.name === 'log'
) {
context.report({
node,
message: '不允许出现 console.log 语句呀,兄弟',
});
}
},
};
},
};
每一条规则对应一个 JS 文件,该 JS 文件导出一个对象,该对象里面包含了基本的 meta 和 create 配置项。
下一步我们需要去 package.json 中,修改如下配置项:
"peerDependencies": {
"eslint": "^7.0.0"
}
- peerDependencies:该配置项指定了我们这个包需要和哪些其他包在同一环境中使用。例如我们上面指定了 eslint ^7.0.0,也就是告诉别人,在使用我们这个包的时候,需要安装 eslint,并且 eslint 的版本要在 7.0.0 以上。
最后是创建入口文件,用于将所有的规则导出:
// index.js
// 该文件是整个包的入口文件,用于导出所有的规则
module.exports = {
rules: {
// 规则名称 : 规则文件
'no-alert': require('./rules/no-alert'),
'no-console-log': require('./rules/no-console-log'),
},
};
至此,我们一个简单的示例插件就书写完毕了。
接下来我们需要测试这个插件。这里我们选择通过 link 的方式来进行本地链接,从而方便我们的测试。
来到插件的根目录,执行 npm link,这样的话该项目就会创建一个软链接到全局包目录里面,回头其他项目就可以通过 link 的方式来链接这个包。
之后我们创建一个测试项目,例如名字叫做 eslint-test-customplugin,使用 npm 进行初始化(因为 npm 和 pnpm 在 link 的时候执行机制有一些区别,npm link 时的速度比 pnpm 快一些),然后安装 eslint
npm i eslint -D
然后在 src/index.js 中书写一些测试代码:
alert('Hello');
console.log('World');
最后是链接对应的插件包,然后配置文件中配置该插件即可:
npm link eslint-plugin-customrules
配置文件 .eslintrc.json 中配置:
{
"plugins": ["customrules"],
"rules": {
"customrules/no-console-log": "warn",
"customrules/no-alert": "error"
}
}
集成 Prettier
目前我们所学习的两个工具:Pretter 和 ESLint,两者都有管理代码风格的功能,因此两者往往就会在代码风格的管理上面存在一些冲突。
例如举一个例子:
- ESLint 配置了单引号规则
- Prettier 配置了要使用双引号
那么现在假设你使用双引号,ESLint 会提示错误,然后我们将引号手动改为单引号,但是我们一格式化,因为会应用 Prettier 的格式化规则,又会被格式化为双引号,也就是说只要一格式化就会报错。
下面是一个具体的示例:
首先我们初始化了一个名为 eslint-prettier-demo 的项目,使用 pnpm init 进行一个初始化,之后分别安装 eslint 以及 prettier
接下来创建这两个工具的配置文件
prettier 配置文件:
{
"singleQuote": true,
"semi": false,
"printWidth": 80,
"trailingComma": "es5"
}
eslint 配置文件
module.exports = {
env: {
browser: true,
es2021: true,
node: true,
},
extends: 'eslint:recommended',
parserOptions: {
ecmaVersion: 12,
sourceType: 'module',
},
rules: {
indent: ['error', 2],
quotes: ['error', 'double'],
semi: ['error', 'always'],
},
};
src/index.js
const str = 'Helo World';
const arr = [
1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22,
23, 24, 25, 26, 27, 28, 29, 30,
];
const obj = {
name: 'John Doe',
age: 30,
address: {
city: 'New York',
state: 'NY',
},
};
console.log(str);
console.log(arr);
console.log(obj);
此时,我们就会发现两份配置之间就存在了冲突。只要一格式化(prettier),eslint 就会报错。
为了解决这个问题,有两个思路:
- 手动的将其中一个工具的配置文件进行修改,改成和另外一个工具的配置是相同的。这种方式肯定是没有问题的,但是缺点在于这种方式是手动的,如果涉及到大量的规则,那么手动操作比较繁琐
- 使用一些插件来帮助我们解决这个
- eslint-config-prettier 会关闭所有与 Prettier 冲突的 ESLint 规则
- eslint-plugin-prettier 将 Prettier 作为 ESLint 规则来运行,这样在运行 ESLint 时也会运行 Prettier。
接下来我们来安装这两个插件:
pnpm add eslint-config-prettier eslint-plugin-prettier -D
之后修改 ESLint 的配置文件,代码如下:
module.exports = {
env: {
browser: true,
es2021: true,
node: true,
},
extends: ['eslint:recommended', 'plugin:prettier/recommended'],
parserOptions: {
ecmaVersion: 12,
sourceType: 'module',
},
rules: {
'prettier/prettier': [
'warn',
{
semi: false,
},
],
},
};
在上面的配置文件中,我们在 extends 里面添加了一个 plugin:prettier/recommended 配置项目,该配置项表示应用 prettier 来作为 ESLint 的插件来运行,当遇到 ESLint 和 Prettier 冲突的规则的时候,关闭 ESLint 的然后用 Prettier 的。
我们也可以书写 rules,但是 rules 注意就不要再和 ESLint 冲突了,一般只修改规则的重要级别,不修改其他的配置项。