1. ESlint是什么
ESLint 是一种静态代码分析工具,用于在编写 JavaScript 和 TypeScript 代码时识别和报告问题。主要有两个功能:代码质量检查、代码格式化。
代码质量检查:可以发现代码中存在的可能错误,如使用未声明变量、声明而未使用的变量、修改 const 变量等等
代码格式化:可以用来统一团队的代码风格,比如加不加分号、使用 tab 还是空格等等。但ESLint 10可能会完全废除所有格式化规则,代码格式化可交给Prettier处理
小结:未来的ESlint只负责一件事,进行代码质量检查
2. 自定义 ESLint 插件场景
虽然@eslint/js中包含了常见的 JavaScript 编程规则,例如对使用console.log进行告警提示。但某些情况下需自定义团队的编程规则。
例如,setTimeout、setInterval的第二个参数,避免直接使用数字,而是通过定义变量加注释的方式使用。如果直接使用数字,ESLint报错提示,防止项目中出现大量不明意义的时间间隔。
3. 使用模板初始化项目
Node版本:^18.18.0、^20.9.0、>=21.1.0
1. 安装
npm i -g yo generator-eslint
yo:Yeoman 是一个用于生成项目骨架的工具,它通过各种生成器(generators)来帮助开发者快速搭建项目结构。
generator-eslint:是一个专门为 ESLint 项目设计的 Yeoman 生成器。它可以帮助你快速生成和配置 ESLint 项目。
2. 新建eslint-plugin-utils文件夹
3. 在新建文件夹中,使用命令行初始化ESLint项目
yo eslint:plugin
交互命令如下所示
第一行:作者名称
第二行:插件ID,例如这里输入wjcao-utils,最终生成的package.json的name为eslint-plugin-wjcao-utils
第三行:插件描述
第四行:这个插件是否包含ESlint规则,选Yes
第五行:这个插件是否包含一个或多个处理器。本篇文章主要讲自定义ESlint插件规则,选No
生成的 lib/rules 目录存放自定义规则,tests/lib/rules 目录存放规则对应的单元测试代码。
4. 使用命令行新建一条ESLint规则
yo eslint:rule
交互命令如下所示
第一行:作者名称
第二行:规则将在哪里发布,这里选择ESLint Plugin
第三行:规则的id
第四行:规则描述
第五行:输入一个失败例子的代码,这里直接回车
完成上述操作后,会生成两个文件,分别是 lib/rules/set-timeout.js 和 tests/lib/rules/set-timeout.js
4. 文件分析
1. lib/rules/set-timeout.js
"use strict";
module.exports = {
meta: {
type: null, // `problem`, `suggestion`, or `layout`
docs: {
description: "setTimeout第二个参数不能直接使用数字",
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
messages: {}, // Add messageId and message
},
create(context) {
return {
// visitor functions for different types of nodes
};
},
};
meta:规则的元数据信息
type:规则类型,problem表示报错、suggestion表示建议修改、layout表示该规则关注的是代码格式
docs:文档信息
-
description:表示规则描述
-
recommended:表示规则是否被推荐,为true说明该规则是重要的,建议在多数项目中启用
-
url:表示该规则的文档页面。
fixable:是否可自动修复。当值为code时,还需在create函数中提供自动修复的代码
schema:定义规则所需要的外部参数
messages:定义规则报告错误时使用的消息 ID 和消息内容
create:规则的主要入口点,它接收一个 context 上下文对象,返回一个对象,该对象的属性名表示节点类型,在向下遍历树时,当遍历到和属性名匹配的节点时,ESLint 会调用属性名对应的函数。
2. tests/lib/rules/set-timeout.js
"use strict";
const rule = require("../../../lib/rules/typeof-limit"),
RuleTester = require("eslint").RuleTester;
const ruleTester = new RuleTester();
ruleTester.run("set-timeout", rule, {
valid: [
// give me some code that won't trigger a warning
],
invalid: [
{
code: "",
errors: [{ messageId: "Fill me in.", type: "Me too" }],
},
],
});
run第一个参数:表示规则名称,也就是之前使用命令行创建ESLint规则时的rule ID
run第二个参数:表示具体的规则,也就是lib/rules/set-timeout.js文件内容
run第三个参数:是一个对象,valid包含不会触发警告的代码片段,invalid包含会触发警告的代码片段
5. AST抽象语法树
ESLint 使用 Espree 作为默认的 JavaScript 解析器。解析器将源代码转换成一个抽象语法树(AST)。访问AST转换平台:astexplorer.net/,将以下函数输入,右侧将出现转换结果
setTimeout(()=>{},1000)
右侧JSON结构:
{
"type": "Program",
"start": 0,
"end": 24,
"body": [
{
"type": "ExpressionStatement",
"start": 0,
"end": 23,
"expression": {
"type": "CallExpression",
"start": 0,
"end": 23,
"callee": {
"type": "Identifier",
"start": 0,
"end": 10,
"name": "setTimeout"
},
"arguments": [
{
"type": "ArrowFunctionExpression",
"start": 11,
"end": 17,
"id": null,
"expression": false,
"generator": false,
"async": false,
"params": [],
"body": {
"type": "BlockStatement",
"start": 15,
"end": 17,
"body": []
}
},
{
"type": "Literal",
"start": 18,
"end": 22,
"value": 1000,
"raw": "1000"
}
],
"optional": false
}
}
],
"sourceType": "module"
}
分析AST抽象语法树主要看type。
Program:程序的根节点
ExpressionStatement:一个表达式语句
CallExpression:函数表达式
Identifier:标识符,可以简单理解为变量名,函数名,属性名
ArrowFunctionExpression:箭头函数表达式
BlockStatement:一个代码块
Literal:一个字面量,可以理解为一个具体的值
这段AST抽象语法树表示:一个名为setTimeout的函数表达式,有两个参数,第一个参数是一个箭头函数,第二个参数是一个具体的值。
6. ESLint运行流程
ESLint运行流程可以简单概括为:解析、遍历、触发回调
解析:ESLint使用JavaScript解析器Espree把JS代码解析成AST
遍历:对AST抽象语法树进行遍历
触发回调:每当匹配到对应的节点,触发rule监听器上的回调
我们主要的工作是定义规则,编写create函数
7. setTimeout第二个参数检测
- 修改lib/.../set-timeout.js
/**
* @fileoverview setTimeout第二个参数不能直接使用数字
* @author wjcao
*/
"use strict";
/** @type {import('eslint').Rule.RuleModule} */
module.exports = {
meta: {
type: "problem", //报错提示
docs: {
description: "setTimeout第二个参数不能直接使用数字",
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
messages: {
setTimeoutError: "setTimeout第二个参数不能直接使用数字",
},
},
create(context) {
return {
//匹配函数调用
CallExpression: (node) => {
if (node.callee.name === "setTimeout") {
const timeNode = node.arguments && node.arguments[1]; // 获取第二个参数
if (timeNode) {
// 检测报错第二个参数是数字 报错
if (timeNode.type === "Literal" && typeof timeNode.value === "number") {
context.report({
node,
messageId: "setTimeoutError",
});
}
}
}
},
};
},
};
在元信息meta中,定义提示类型type为problem,定义提示消息messages,messages是一个对象,key值是messages的ID,value值是具体的提示消息
在create函数中,匹配CallExpression函数调用,函数名为setTimeout则进一步判断第二个参数是否为number类型,如果是,则使用context.report抛出错误提示。
2. 修改tests/.../set-timeout.js,进行单元测试
"use strict";
const rule = require("../../../lib/rules/set-timeout"),
RuleTester = require("eslint").RuleTester;
const ruleTester = new RuleTester();
ruleTester.run("set-timeout", rule, {
valid: [
{
code: "const delay = 1000; setTimeout(()=>{},delay)",
},
],
invalid: [
{
code: "setTimeout(()=>{},1000)",
errors: [{ messageId: "setTimeoutError", type: "CallExpression" }],
},
],
});
valid中定义合法的单元测试,invalid定义不合法的测试。
写好单元测试后,执行“npm test”命令即可运行测试,如果看到如图所示的输出,则表示单元测试通过了。
8. setInterval第二个参数检测
1. 使用命令行新建一条ESLint规则
yo eslint:rule
交互命令如下所示
2. 参照set-timeout.js,修改lib/rules/set-interval.js
"use strict";
//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------
/** @type {import('eslint').Rule.RuleModule} */
module.exports = {
meta: {
type: "problem", // `problem`, `suggestion`, or `layout`
docs: {
description: "setInterval第二个参数不能是数字",
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
messages: {
setIntervalError: "setInterval第二个参数不能直接使用数字",
}, // Add messageId and message
},
create(context) {
return {
//匹配函数调用
CallExpression: (node) => {
if (node.callee.name === "setInterval") {
const timeNode = node.arguments && node.arguments[1]; // 获取第二个参数
if (timeNode) {
// 检测报错第二个参数是数字 报错
if (timeNode.type === "Literal" && typeof timeNode.value === "number") {
context.report({
node,
messageId: "setIntervalError",
});
}
}
}
},
};
},
};
3. 修改tests/.../set-interval.js,进行单元测试
"use strict";
const rule = require("../../../lib/rules/set-interval"),
RuleTester = require("eslint").RuleTester;
const ruleTester = new RuleTester();
ruleTester.run("set-interval", rule, {
valid: [
{
code: "const delay = 1000; setInterval(()=>{},delay)",
},
],
invalid: [
{
code: "setInterval(()=>{},1000)",
errors: [{ messageId: "setIntervalError", type: "CallExpression" }],
},
],
});
写好单元测试后,执行“npm test”命令即可运行测试
9. Plugins与Extends
在使用自定义插件前,需明白Plugins与Extends的使用场景
1. Plugins
Plugins负责加载插件,以扩展ESLint的功能。例如校验React Hooks,则使用eslint-plugin-react-hooks。
plugins: { "react-hooks": reactHooks, },
Plugins只是加载了插件,还需在rules中开启对应的规则。例如开启eslint-plugin-react-hooks推荐的全部规则
rules: { ...reactHooks.configs.recommended.rules, },
2. extends
extends 用于继承现有的配置文件,简单理解为是plugins与rules的结合。例如开启ESLint JavaScript代码推荐规则
extends: [js.configs.recommended],
10. 在React项目中使用自定义ESLint规则
在发布插件前,先通过link的方式进行软件包测试
-
确保VSCode安装了ESLint插件
-
在eslint-plugin-utils项目下运行
npm link
- 使用vite,新搭建一个React项目,项目模板自带了ESLint
npm create vite\@latest vite-react-ts-seed -- --template react-ts
- 在根目录下执行下面的命令,这会在 node_modules 目录下创建一个软链接
npm link eslint-plugin-wjcao-utils
link后面的eslint-plugin-wjcao-utils为eslint-plugin-utils项目中的package.json的name值
- 修改项目的eslint.config.js
import js from "@eslint/js";
import globals from "globals";
import reactHooks from "eslint-plugin-react-hooks";
import reactRefresh from "eslint-plugin-react-refresh";
import tseslint from "typescript-eslint";
import myLint from "eslint-plugin-wjcao-utils"; //新增
export default tseslint.config(
{ ignores: ["dist"] },
{
extends: [js.configs.recommended, ...tseslint.configs.recommended],
files: ["**/*.{ts,tsx}"],
languageOptions: {
ecmaVersion: 2020,
globals: globals.browser,
},
plugins: {
"react-hooks": reactHooks,
"react-refresh": reactRefresh,
"my-limit": myLint, //新增
},
rules: {
...reactHooks.configs.recommended.rules,
"react-refresh/only-export-components": ["warn", { allowConstantExport: true }],
"my-limit/set-timeout": "error", //新增
"my-limit/set-interval": "error", //新增
},
}
);
- 测试:修改App.tsx
import { useEffect } from "react";
function App() {
useEffect(() => {
setTimeout(() => {
console.log("setTimeout");
}, 1000);
setInterval(() => {
console.log("setInterval");
}, 2000);
}, []);
return <></>;
}
export default App;
编辑器直接飘红报错提示:
- 运行 npm run lint,也能在终端中得到报错信息
11. 在Vue项目中使用自定义ESLint规则
在发布插件前,先通过link的方式进行软件包测试
-
确保VSCode安装了ESLint插件
-
在eslint-plugin-utils项目下运行
npm link
- 使用vite,新搭建一个Vue项目
npm create vite@latest vite-vue-ts-seed -- --template vue-ts
- 项目模板目前没有自带ESLint,需手动安装
npm init @eslint/config
根据提示进行安装
- 在根目录下执行下面的命令,这会在 node_modules 目录下创建一个软链接
npm link eslint-plugin-wjcao-utils
link后面的eslint-plugin-wjcao-utils为eslint-plugin-utils项目中的package.json的name值
- 修改项目的eslint.config.js
import globals from "globals";
import pluginJs from "@eslint/js";
import tseslint from "typescript-eslint";
import pluginVue from "eslint-plugin-vue";
import myLint from "eslint-plugin-wjcao-utils"; //新增
export default [
{ files: ["**/*.{js,mjs,cjs,ts,vue}"] },
{ languageOptions: { globals: globals.browser } },
pluginJs.configs.recommended,
...tseslint.configs.recommended,
...pluginVue.configs["flat/essential"],
{ files: ["**/*.vue"], languageOptions: { parserOptions: { parser: tseslint.parser } } },
//新增
{
plugins: {
"my-limit": myLint,
},
rules: {
"my-limit/set-timeout": "error",
"my-limit/set-interval": "error",
},
},
];
- 测试:修改App.vue
<template>
<div></div>
</template>
<script setup lang="ts">
setTimeout(() => {
console.log("setTimeout");
}, 1000);
setInterval(() => {
console.log("setInterval");
}, 2000);
</script>
编辑器直接飘红报错提示:
- 修改package.json,在script中新增一条命令
"lint": "eslint src"
运行 npm run lint,也能在终端中得到报错信息
12. 插件规则集成
之前需要手动一条一条配置规则,例如
"my-limit/set-timeout": "error", //新增
"my-limit/set-interval": "error", //新增
如果自定义规则多了之后,这样一条条引入肯定不现实
- 修改lib/index.js,将规则放到recommended字段中
"use strict";
const requireIndex = require("requireindex");
const plugin = {
configs: {},
rules: requireIndex(__dirname + "/rules"),
};
Object.assign(plugin.configs, {
recommended: {
plugins: {
"my-limit": plugin,
},
rules: {
"my-limit/set-timeout": "error",
"my-limit/set-interval": "error",
},
},
});
module.exports = plugin;
- 修改项目的eslint.config.js(React项目)
//...
import myLint from "eslint-plugin-wjcao-utils"; //新增
export default tseslint.config(
{ ignores: ["dist"] },
{
extends: [
js.configs.recommended,
...tseslint.configs.recommended,
myLint.configs.recommended], //新增一个recommended
//...
}
);
- 修改项目的eslint.config.js(Vue项目)
//...
import myLint from "eslint-plugin-wjcao-utils"; //新增
export default [
{ files: ["**/*.{js,mjs,cjs,ts,vue}"] },
{ languageOptions: { globals: globals.browser } },
pluginJs.configs.recommended,
myLint.configs.recommended, //新增
//...省略
];
使用VSCode重新打开项目,使ESLint校验规则生效
13. 发包
发包比较简单,两个命令npm login与npm publish
之前介绍过发包过程,具体可参考:发布到npm
结尾
本篇文章限于篇幅只介绍了ESLint部分用法,如果对ESLint感兴趣,可在评论区留言。如果感兴趣的小伙伴多的话,我再继续更新ESlint相关的文章。
创作不易,欢迎点赞+收藏支持!!!