前言

在我们代码中,经常会有和后端协商的常量,比如是‘MALE’,‘FEMALE’,这样还好理解,看上去知道分别对应的意思是什么,但如果给你的是两个数字0和1,难免会造成混淆,让我们的代码存在风险。我推荐的做法是在前端自己定义枚举值。

例如:

const SEX={
/** 男 */
male:1,
/** 女 */
female:2
}
// 使用
let sex = SEX.male // let sex = 1

sex = SEX.female // sex = 2

if(sex===SEX.female){} // if(sex===2){}

对比纯数字的写法,这种枚举往往更能提高我们代码的质量。除了数字,我觉得所有的字面量都有必要使用枚举类型,有两点我觉得也很重要。

  1. 便于维护:在迭代初期,我们无需等后端定义好值,我们直接前端先自己用一个假的值定义枚举进行开发,后面只需要修改枚举的值即可,而不用到处找使用的常量去修改,也方便如果后期维护后端需要修改值,我们直接修改我们定义的枚举值即可。
  2. 利用IDEA提示:我们可以更好的利用IDEA的提示,如上面写好注释,在使用的地方可以看到具体代表的是什么意思,也可以利用好IDEA的补全和校验,防止写错字面量。

在最近参与到一个广告投放工具项目里,对接巨量快手腾讯三个平台,而后端之前定义的全是数字,23,27,22,三个可能都还好,后面再对接几个平台,多几个数字,确定能记得清哪个是哪个?因此我定义好了枚举,并且希望通过Eslint去强制使用枚举而不是字面量,约束自己也约束团队,减少bug以及提高代码的质量。

认识Eslint

我们经常在项目中使用Eslint,常用的配置在网上也很容易找到,Eslint的原理是怎么样呢

ESLint 的工作原理可以归纳为以下步骤:

  1. 解析代码:将源代码转换为 AST。

    ESLint 使用解析器将代码解析成抽象语法树(AST)。AST 是一种树状结构,表示源代码的语法结构

  2. 应用规则:遍历 AST,检查各个节点是否符合规则。

    ESLint 包含一系列内置规则,并支持用户定义的自定义规则。这些规则会对 AST 进行检查,寻找潜在问题。当 ESLint 解析完代码并生成 AST 后,它会遍历这个 AST,检查每个节点。根据启用的规则,ESLint 会执行相应的检查。

  3. 报告问题:生成并输出问题报告。

    ESLint 在检查完所有规则后,会生成一个结果报告。这个报告可以是文本格式、JSON 格式等,具体取决于配置。

  4. 扩展功能:支持插件和共享配置。

    ESLint 支持通过插件来扩展功能。插件可以包含自定义规则、共享配置和额外的解析器。

ESLint 可以与多种开发工具集成,编辑器,构建工具,CI/CD等,共同维护整个项目的代码质量。

AST的类型

上面说到Eslint的规则主要基于AST,利用源代码各个结构部分的节点类型进行规则的匹配和校验,那么我们要自定义一个规则,了解一下AST就很有必要。

以下是常见的AST类型

Program

  • 顶级节点,表示整个程序(源文件)。

ExpressionStatement

  • 表示单个表达式的语句。

  • 属性: expression,一个表达表达式的节点。

    foo();

    这里的 foo(); 是一个 ExpressionStatement,它包含一个函数调用表达式。

Literal

  • 表示字面量,如字符串、数字、布尔值等。

  • 属性: value,字面量的值。

    42
    "hello"
    true

    每个字面量在 AST 中是一个 Literal 节点。

Identifier

  • 表示标识符(变量名、函数名、属性名等)。

  • 属性: name,标识符的名称。

    const foo = 42;

    这里 foo 是一个 Identifier 节点。

VariableDeclaration

  • 表示变量声明(varletconst)。

  • 属性:

    • declarations,一个包含所有声明的数组。
    • kind,表示使用的声明类型(varletconst)。
    const foo = 42;
    let bar;

    这里 const foo = 42; 是一个 VariableDeclaration 节点。

VariableDeclarator

  • 表示具体的变量声明。

  • 属性:

    • id,表示被声明的变量(是一个 Identifier 节点)。
    • init,表示变量的初始化值(如果有)。
    const foo = 42;

    这里的 foo = 42 是一个 VariableDeclaratoridfooinit42

FunctionDeclaration

  • 表示函数声明。

  • 属性:

    • id,函数的名称(是一个 Identifier)。
    • params,函数的参数列表(每个参数是一个 Identifier 节点)。
    • body,函数体(是一个 BlockStatement 节点)。
    function greet(name) {}

    这里整个函数声明是一个 FunctionDeclaration 节点。

Property

  • 表示对象的属性键值对。

  • 属性:

    • key,表示属性名。
    • value,表示属性值。
    { foo: 42 }

    这里的 foo: 42 是一个 Property 节点。

UnaryExpression

  • 表示一元操作符,如 !typeof+-

  • 属性:

    • operator,表示操作符。
    • argument,表示操作的对象。
    !flag

    这里的 !flag 是一个 UnaryExpression 节点。

AssignmentExpression

  • 表示赋值表达式。

  • 属性:

    • operator,表示赋值操作符(如 =+=-=)。
    • left,表示被赋值的变量。
    • right,表示赋值的值。
    x = 42

    这里的 x = 42 是一个 AssignmentExpression 节点。

BlockStatement

  • 表示一组语句的块(如函数体、if 块等)。

  • 属性: body,包含块内的语句数组。

    if(true){
    let a = 42;
    }

    这里的 { let a = 42; } 是一个 BlockStatement 节点。

ReturnStatement

  • 表示 return 语句。

  • 属性: argument,表示返回的表达式。

    return 42;

    这里的 return 42 是一个 ReturnStatement

BinaryExpression

  • 表示二元操作符表达式,如 +-*=== 等。

  • 属性:

    • leftright,表示左右操作数。
    • operator,表示操作符(如 +=== 等)。
    a + b

    这里的 a + b 是一个 BinaryExpression 节点。

CallExpression

  • 表示函数调用。

  • 属性:

    • callee,表示被调用的对象(可能是 Identifier 或其他表达式)。
    • arguments,表示调用时传入的参数数组。
    foo(42);

    这里的 foo(42) 是一个 CallExpression 节点。

MemberExpression

  • 表示对象属性或方法的访问。

  • 属性:

    • object,表示对象。
    • property,表示被访问的属性或方法。
    obj.foo
    obj['foo']

    这里的 obj.fooobj['foo'] 都是 MemberExpression 节点。

IfStatement

  • 表示 if 语句。

  • 属性:

    • test,表示条件表达式。
    • consequent,表示条件为 true 时执行的块(是 BlockStatement)。
    • alternate,表示 else 的块(如果有)。
    if (x > 0) {
    // do something
    } else {
    // do something else
    }

    这里整个 if-else 语句是一个 IfStatement 节点。

ForStatement

  • 表示 for 循环。

  • 属性:

    • init,表示初始化语句。
    • test,表示循环条件。
    • update,表示循环后的更新表达式。
    • body,表示循环体。
    for (let i = 0; i < 10; i++) {
    console.log(i);
    }

    这里整个 for 循环是一个 ForStatement 节点。

ArrayExpression

  • 表示数组字面量。

  • 属性: elements,表示数组中的各个元素。

    [1, 2, 3]

    这里是一个 ArrayExpression 节点。

ObjectExpression

  • 表示对象字面量。

  • 属性: properties,表示对象中的各个 Property

    { foo: 42 }

    这里是一个 ObjectExpression 节点。

自定义Eslint

定义规则

我们直接上文件分析:

module.exports = {
meta: {
/**
* 表示规则的类型,可以是以下三种之一:
* "problem": 表示此规则用于发现代码中的潜在问题。
* "suggestion": 表示此规则提供建议以改善代码质量或可读性。
* "layout": 表示此规则与代码的格式或布局相关。 */
type: 'suggestion',
docs: {
description: 'Enforce the use of predefined enum values instead of number literals',
/**
* category: 指定规则的类别,常见的类别包括:
"Best Practices": 最佳实践
"Possible Errors": 可能的错误
"Stylistic Issues": 风格问题
"ECMAScript 6": ECMAScript 6 特有的规则
* */
category: 'Best Practices',
recommended: true
},
fixable: 'code',
schema: [
// 配置项,可以根据需要调整
{
type: 'object',
properties: {
enumMap: {
type: 'object',
additionalProperties: { type: 'string' } // 允许的枚举值为字符串
}
},
required: ['enumMap'] // enumMap 是必需的
}
]
},

create(context) {
// 从配置中获取 enumMap
const options = context.options[0] || {}
const enumMap = options.enumMap || {}
return {
// 检查字面量中的数字
Literal(node) {
if (enumMap[node.value]) {
context.report({
node,
message: `Use enum instead of number literal: ${node.value}`,
fix(fixer) {
const enumValue = enumMap[node.value]
return fixer.replaceText(node, enumValue)
}
})
}
}
}
}
}

一个规则就是一个对象,meta主要是这个规则的信息,create是规则的具体实现。

首先了解一下meta

meta.type

表示规则的类型,可以是以下三种之一:
“problem”: 表示此规则用于发现代码中的潜在问题。
“suggestion”: 表示此规则提供建议以改善代码质量或可读性。
“layout”: 表示此规则与代码的格式或布局相关。

meta.docs

表述规则的信息,包括描述、类别和推荐。

category:

“Best Practices”: 最佳实践

“Possible Errors”: 可能的错误

“Stylistic Issues”: 风格问题

“ECMAScript 6”: ECMAScript 6 特有的规则

recommended:

表示是否推荐使用此规则。如果为true,则无需再手动启用规则。

meta.fixable

表示是否可以自动修复。
如果为true,则表示该规则可以自动修复。规则最后report时,在fixer中可以调用fixer.replaceText()方法进行修复。

context.report({
node,
message: `Use enum instead of number literal: ${node.value}`,
fix(fixer) {
const enumValue = enumMap[node.value]
return fixer.replaceText(node, enumValue)
}
})

meta.schema

表示规则的配置项。入参。类型为对象数组

 schema: [
{
type: 'object',
properties: {
enumMap: {
type: 'object',
additionalProperties: { type: 'string' } // 允许的枚举值为字符串
}
},
required: ['enumMap'] // enumMap 是必需的
}
]

create

create函数是规则的具体实现,返回一个对象,对象中包含各种AST节点的处理函数。
create函数接收一个参数context,表示当前规则的上下文。

我这个规则比较简单,因为只需要对字面量Literal这个AST节点监听即可,如果涉及与其他常用字面量混淆,可能就另外处理一下,这种情况就不太是用仅直接监听字面量了,可能需要配合其他的变量定义、操作符等等去针对特定的场景定义,我觉得大家也可以没问题就先用着,把自动fix关了,慢慢去优化规则,一开始就完美实现有点难,慢慢优化吧。。

// 检查字面量中的数字
Literal(node) {
if (enumMap[node.value]) {
context.report({
node,
message: `Use enum instead of number literal: ${node.value}`,
fix(fixer) {
const enumValue = enumMap[node.value]
return fixer.replaceText(node, enumValue)
}
})
}
}

使用自定义规则

Eslint是通过plugin是引入自定义规则的,所以我们需要通过将我们本地规则包装成一个plugin

在规则文件同层创建一个index.js入口文件

/eslint-rules/***.js

// /eslint-rules/index.js
module.exports = {
rules: {
'***': require('./***')
}
}

直接引入本地规则(V9.x)

注:这种方式只有在V9.x才有

local plugin:

// eslint.config.js
import local from "./eslint-rules/index.js";

export default [
{
plugins: {
local
},
rules: {
"local/***": "warn"
}
}
];

或者无需配置入口文件,配置一个虚拟plugin

// eslint.config.js
import myRule from "./eslint-rules/***.js";
export default [
{
plugins: {
local: {
rules: {
"my-rule": myRule
}
}
},
rules: {
"local/my-rule": "warn"
}
}
];

npm引入

这个直接将规则包装成插件,上传到npm,然后引入插件即可

本地npm引入

如果不想上传到npm,并且你得eslint版本不是V9.x的话,可以通过配置package.json的本地引入即可。

{
"name": "...",
"version": ""...",",
"description": ""...",",
"scripts": {
...
},
"dependencies": {
...
}
"devDependencies": {
...
"eslint-plugin-local-rules": "file:./eslint-rules/index"
},
...
}

需要在路径前面添加“file:”,这样包管理器就会在本地加载放到node_modules,然后我们就可以像使用第三方包那样引用了。