当我们希望团队内使用统一的代码风格样式时,往往会加入eslint到项目中来约束一些样式。然而并不是所有现成的eslint规则能够满足我们对代码格式的要求。这时侯就可以考虑自定义eslint规则了。
在过去版本的eslint中,使用的配置文件是 .eslintrc.js
这个文件,这个文件十分不利于我们配置本地的自定义规则,如果你还在使用这个配置文件,十分建议你升级你的eslint并将配置文件修改为 eslint.config.js
这种的。
本文使用的eslint是9.30.0
版本的。
一、给定需求
没有需求瞎编代码没有意义,为了带出本文内容,我们假定一个需求,这里我们提出一个需求:
如果JavaScript源码中定义了一个对象,而该对象整个定义字符串的字符长度如果少于100,那么将这个对象建议写在一行,不要换行编写。
好了,现在我们需求有了,基于这个需求我们可以大致感受下源码应该长什么样子:
假设我们有源码index.js
(不符合我们的要求)如下:
// index.js
var obj = {
name: "Jack",
age: 20
};
很明显,这个对象的定义所花费的字符长度是不足100的,根据我们上面的规则定义,这个对象应该被修复为下面这样才正确:
// index.js
var obj = { name: "Jack", age: 20 };
那么这个时候,我们可以开始我们自定义规则的编写,来实现这个需求。
二、准备实现
在开始编写代码实现自定义规则前,我们需要先将 eslint
的环境准备好。如果你的项目里还没有eslint,那么你可以先安装一下,eslint一般安装为dev依赖,它毕竟只是用在开发阶段的东西。
npm i eslint@9.30.0 --save-dev
现在你有eslint了。我们可以开始建立规则实现的源码文件了。在项目的根目录新建一个custom_rules
文件夹,我们计划这里面每一个文件就是一条我们自定义的规则,而上述就我们提出的需求而言,我们新建一个文件叫做 inlineObj.js
。
然后将我们的这个规则配置到eslint.config.js
中就可以了,如果你还没有这个文件,你可以手动新建,配置文件内容如下:
// eslint.config.js
module.exports = {
// 这表示我们的eslint要对这些文件进行处理,其它的文件不会处理
files: ['src/**/*.js'],
plugins: {
// 这里就是我们自定义的插件了,自定义规则是实现在自定义插件中的。
// 严格来讲,这里这个对象整体应该由我们的代码提供,而不是直接写
// 在这个配置文件里,但是本文只说明插件中的自定义规则如何实现
// 索性就直接写了吧~
"custom_rules": {
// 这个 rules 表示我们的自定义eslint插件中提供了哪些规则
// 很明显,目前只有下面一个。
rules: {
"inlineObj": require("./custom_rules/inlineObj")
}
}
},
rules: {
// 这行的作用就表示,在eslint进行检查的过程中,要使用这条规则,并且如果发现
// 规则不符合时,以报错的形式标红。
// 其中“custom_rules” 这一段和 plugins 里面的需要保持一致。
// “inlineObj” 这一段应该和 上面的plugins里面的rules里定义的保持一致。
// 这就表示要使用自定义插件custome_rules里面的 inlineObj 规则
"custom_rules/inlineObj": `error`
}
}
配置文件中的注释详细描述了各个字段的作用,当然可能你的配置文件并不只有这些内容,可能预先就有一大堆内容在里面,不过你可以根据上面的注释,来对你的配置文件进行调整以达到目的。
为了方便调试,我们还要在 package.json
中的 scripts
节点中添加一个快捷命令,这样方便运行eslint从而来快速检测我们的自定义规则是否有效。那么我将添加一个快捷规则如下:
// package.json
{
"name": "*****_project",
...
"scripts": {
...
"eslint_fix": "eslint . --fix"
},
...
}
如果你的package.json中已经有一个eslint检查的快捷命令的话,你可以不用添加。现在我们再准备一个需要被检测的源码文件,由于配置文件中会去检测 src 目录下的js文件,因此我在src目录下新建了一个index.js 文件,内容就如同第一节中的:
// src/index.js
var obj = {
name: "Jack",
age: 20
};
现在要检测的源码有了,规则文件有了,只需要实现规则了。
三、实现自定义规则
好了,一切准备工作都已完善,现在打开文件 inlineObj.js
开始编写我们的自定义规则吧~,在 文件 inlineObj.js
中,写上如下代码:
// inlineObj.js
module.exports = {
meta: {
type: "layout",
docs: {
description: "如果一个对象内所有属性、值加在一起的代码长度不超过100,则将该对象编写为一行"
},
fixable: "whitespace", // 表示规则支持自动修复,且自动修复只会删除或新增空白字符,比如空格、换行符这些。
schema: [],
messages: {
inlineSmallObj: `对象内所有属性、值加在一起的代码长度不超过100,则将该对象编写为一行`,
},
},
create: function(context) {
return {
// 每当检测到源码中出现定义一个对象的字面量时,这个函数就会被调起。
ObjectExpression(node) {
// 这就得到源码中定义对象的源码字符长度
let charLength = node.end - node.start;
// 如果字符长度小于100,并且定义对象的第一行源码和最后一行源码不在同一行时,表名源码不符合我们的规则,我们的规则建议这个对象应该源码中写在同一行。
if(charLength <= 100 && node.loc.start.line !== node.loc.end.line) {
context.report({
node: node,
messageId: "inlineSmallObj"
});
}
}
}
}
}
可能猛的一看这么多代码不好理解,实际上你现在先不需要理解,当我们保存好代码后,先运行一下 npm run eslint_fix
看下效果吧~。
如果不出意外,你应该可以看到控制台已经给出了我们的自定义规则要求报错的错误提示了:
四、代码理解
上面这么大一段代码就为实现这么个提示,从理解上是有点困难的,整个结构都不懂,更别说具体逻辑了。现在我们来好好理解一下这些代码在干啥。
首先看整个文件导出的一个对象结构,这个对象有2个属性:
module.exports = {
// 一些规则说明、参数配置、提示信息的配置
meta: {},
create: function (context) {
return {}
}
}
别害怕,这就是一个自定义规则的必须格式,meta
部分是用来提供规则说明、参数配置、提示信息的配置。从报错内容来看,可以很容易理解到这部分的作用就是起到一个说明和介绍。
最重磅的就是 create
函数了,这个函数要求必须返回一个对象,而这个对象的作用就是:对象中的每个属性其实就是eslint在校验整个源码文件过程中遇到的各种代码片段类型的监听器,(实际上还支持节点选择器和事件监听,但现在我们先大事化小~,或者你可以去官方看)。从源码中可看到:
module.exports = {
...
create: function (context) {
return {
ObjectExpression:(node) {
// ObjectExpression 表示解析到源码定义了一个对象,也就对应源码中对象 { 开始符号,到 } 结束符号的全部源码部分内容,然后把这部分源码内容以 node 变量传给了你,我们可以判断node中的各个属性来判断源代码是如何编写的,这段代码是出现在哪行或者在整个源码的哪个字符串区间。
}
}
}
}
好好好,我知道你有问题,你会说,我怎么知道这里边有哪些代码类型啊?很好,非常好,这就不得不引出一个十分牛逼plus的东西了。eslint在解析源码的时候时会先将源码解析为一个AST树,每个符号、表达式都会被AST抽象为一个 token
然后多个token结合形成一个 node
。在AST树的体系中,我们可以清晰的看到整个源码的各种表达式类型,点击此处跳转到AST在线浏览页面,在这个界面里,左边是被解析的源代码,右边是解析出来的AST树数据结构,这样一来你就知道你的目标规则应该在那个类型的代码片段下去进行判断校验了。(这里还有个 eslint 的 AST 在线解析网址:Code Explorer)
将我们被检测的源码粘贴进去,让它给我们分析分析,可以看到:
我们要的检测的就是这个对象在源码中应不应该写到同于一行,于是在AST树中,我们找到这段代码的类型是ObjectExpression
,于是我们在自定义规则的 create
函数中返回的对象里面就可以写一个 ObjectExpression(node){}
函数,这个函数在解析到定义对象时,就会调用啦。我们就只需要判断下这片代码长度是不是少于100,是不是这片代码写在了好几行,通过node我们就可以轻松判断了,判断出来不符合规则的话,我们就可以通过context.report()
函数将提示信息进行报告。至于 context 和 node 变量里面又有些啥,我们先不细说,等会儿会有说明的。
五、实现自动修复
以上还只是实现了规则校验出来后给出提示,并没有实现自动修复的功能。提示功能我们通过context.report()
进行报告的。实际上 context.report()
同时也支持自动修复的功能,我们只需要在report的时候加入一个修复逻辑就可以了,代码如下:
module.exports = {
meta: {...},
create: function(context) {
return {
// 每当检测到源码中出现定义一个对象的字面量时,这个函数就会被调起。
ObjectExpression(node) {
// 这就得到源码中定义对象的源码字符长度
let charLength = node.end - node.start;
// 如果字符长度小于100,并且定义对象的第一行源码和最后一行源码不在同一行时,表名源码不符合我们的规则,我们的规则建议这个对象应该源码中写在同一行。
if(charLength <= 100 && node.loc.start.line !== node.loc.end.line) {
// 获取到定义对象的代码。
let originCode = context.sourceCode.getText(node);
// 将其换行符去除。然后各个属性间加个空格,美观些。
let newcode = originCode.split(/[\r?\n]/).map(line => line.trim()).join(" ");
context.report({
node: node,
messageId: "inlineSmallObj",
// 提供修复版本的代码
fix(fixer) {
return fixer.replaceTextRange(node.range, newcode);
}
});
}
}
}
}
}
现在,我们再运行一下eslint_fix
,你就会发现,如果源码不符合将会自动修复源码,不会报错了。运行完成后,点开 index.js
文件,发现我们定义的对象变成了一行了:
// index.js
var obj = { name: "Jack", age: 20 };
大功告成。
六、context 和 node 对象实例
1、context:
这个实例提供了很多工具辅助函数,可以帮我们获取到源码中的各个片段代码数据。它包含下面的属性和函数:
context.sourceCode
:整个源码内容对象,这里面又有很多工具函数。跳转官方了解详情context.options
:适用于这条规则的配置,这里我们没用到,它配合meta.schema来用。context.report(descriptor)
: 报告不符合规则,跳转官方了解详情
关于 context 对象实例的更多详细:官方文档。
2、node
尴尬,我没在文档里找到,每次我要看这里面有啥,我都是console.log 的。。。不过常用的有:
{
"type": "ObjectExpression", // 节点类型
"start": 10, // 从源码的开始位置
"end": 38, // 到源码的结束位置
"loc": { // 二维版本的定位
"start": { line: 1, column: 10 }, // 开始的行号和列偏移, line 是从1开始的
"end": {line: 1, column: 38}, // 结束的行号和列偏移, line 是从1开始的
},
"range": [10, 38], // 节点所在源码的区间
... // 其它的就要看不同节点类型有不同的内容了。都是AST树里面的
}
七、💥重要提示💥
做自动修复时,一定要考虑到要修改的源码中是否包含有注释内容,如果把要执行的代码给搞到注释后面去了,那可就炸了~~
可以通过这些方法获取注释:
sourceCode.getAllComments()
sourceCode.getCommentsBefore(node)
sourceCode.getCommentsAfter(node)
sourceCode.getCommentsInside(node)
一般如果node节点中有注释我就不管合不合规了。😂