【实战】自定义 ESLint Plugin
背景
之前做过一个小分享——【优化】记一次通过工具减少 Git 冲突[1]。主要讲的是通过利用 git hooks
在代码提交之前给相关的代码排序,从而减少合代码时候的冲突。
上次同事提醒说,这个 Eslint
就可以做到。我回去查了一下,还真可以,详情见 sort-keys[2]。假如使用了这条规则,就是要求对象写法要遵循一定的顺序。比如开启这个规则的话,默认情况下下面的代码就会报错:
let obj = {a: 1, c: 3, b: 2};
应该为:
let obj = {a: 1, b: 2, c: 3};
但是其实我们的诉求中,还有一种场景,那就是对象数组。比如下面这个场景,我需要根据 label
去决定对象在数组中顺序(注意:我们这个场景下数组的顺序对业务是没有影响的),Eslint
这个规则就无能为力了。
const FlowList = [
{ value: '5', label: 'a' },
{ value: '2', label: 'C' },
{ value: '1', label: 'B' }
];
另外,我们知道 ESLint 规则可以针对某个文件夹或者某个文件生效,那能不能只针对于某个代码块呢?
那我们如何通过 Eslint
暴露给我们的能力去实现这些点呢?
ESLint 是什么?
官方如下:
ESLint 是在 ECMAScript/JavaScript 代码中识别和报告模式匹配的工具,它的目标是保证代码的一致性和避免错误
ESLint 具有以下特点:
- 使用 Espree[3] 解析 JavaScript。
- 使用
AST
去分析代码中的模式。 - 完全插件化的。每一个规则都是一个插件,提供了足够的可拓展能力,让我们更好的定义使用规则。
讲 ESlint
我们离不开 AST
(抽象语法树),我们可以通过 astexplorer[4] 直观看到 Espree 处理后生成的 AST 的结构。比如 var a = 1;
如下所示:
竟然我们知道它的结构,我们就可以直接去检测它合不合法了。
我们来讲一个重要的概念——AST Selectors :它是一个字符串,可用于匹配抽象语法树(AST
)中的节点。这对于在代码中描述特定的语法模式非常有用。AST
选择器的语法与 CSS
选择器的语法类似。如果你以前使用过 CSS
选择器,那么 AST
选择器的语法应该很容易理解。这个在我们后面自定义规则的时候非常重要。它的语法可以看官方文档[5]。
ESlint 的原理
在开始书写我们的规则,我们看看 ESlint 具体的实现是怎么做的(这里只说明单条的 Rule 是怎么书写的,整体的 ESlint 作用流程这里不展开)。就以之前提到的 sort-keys[6] 为例。
每个规则都会有三个重要的文件:
lib/rules
目录中的是源文件,具体的校验逻辑可以在这里写。tests/lib/rules
目录中是测试文件,写具体的测试用例。docs/rules
文档目录。
在 lib/rules/sort-keys.js
中我们可以找到上面规则相应的源码。规则的源文件导出具有以下属性的对象。类似如下:
module.exports = {
// 包含规则的元数据
meta: {
// 规则类型
type: "suggestion",
// 文档
docs: {
description: "require object keys to be sorted",
category: "Stylistic Issues",
recommended: false,
url: "https://eslint.org/docs/rules/sort-keys",
},
schema: [
// 可以传的一些参数
{
enum: ["asc", "desc"],
}
],
// 提示信息
messages: {
sortKeys:
"Expected object keys to be in {{natural}}{{insensitive}}{{order}}ending order. '{{thisName}}' should be before '{{prevName}}'.",
},
},
create(context) {
return {};
},
};
meta
:代表了这条规则的元数据,如其类别,文档,可接收的参数的schema
等等,官方文档[7]对其有详细的描述,这里不做赘述。create
: meta 表达了我们想做什么,那么create
则用表达了这条 rule 具体会怎么分析代码。
create
返回的是一个对象,其中 key
就是上面提到的 AST Selector
,在 AST Selector
中,我们可以获取对应选中的内容,随后我们可以针对选中的内容作一定的判断,看是否满足我们的规则,如果不满足,可用 context.report()
抛出问题,ESLint
会利用我们的配置对抛出的内容做不同的展示。
在 AST Selector
的末尾添加 :exit
将导致在遍历过程中退出匹配节点时调用侦听器,而不是在输入匹配节点时。
自定义 ESlint 插件
基于 `Yeoman generator`[8] (一个快速帮你搭建工程的脚手架工具),可以快速创建 ESLint plugin
项目。
npm i -g yo
npm i -g generator-eslint
// 创建一个plugin
yo eslint:plugin
// 创建一个规则
yo eslint:rule
我创建的目录结构如下:
├── README.md
├── docs # 文档
│ └── rules
│ ├── array-sort-object.md
│ └── sort.md
├── lib # 源代码,规则文件
│ ├── index.js
│ └── rules
│ ├── array-sort-object.js
│ └── sort.js
├── package.json
├── tests # 单元测试文件
│ └── lib
│ └── rules
│ ├── array-sort-object.js
│ └── sort.js
└── yarn.lock
如何做到只检测部分代码?
我们知道 ESlint
的检测可以指定到文件维度,但是我们希望只针对部分的代码进行检测。要不然像对象数组顺序,假如都开了检测,将会有很多报错或者警告。
方法是有的,我们发现,ESlint 是可以通过 getCommentsInside
方法获取到某个 AST Selector 中的注释,返回给定节点内所有注释标记的数组。比如以下:
const FlowList = [
// eslint sortBy:'label'
{ value: '5', label: 'a' },
{ value: '2', label: 'C' },
{ value: '1', label: 'B' }
];
create: function (context) {
// 获取到顺序的配置,默认是升序
const order = context.options[0] || "asc";
// variables should be defined here
return {
ArrayExpression: (node) => {
console.log('getCommentsBefore:', context.getCommentsInside(node))
}
}
打印出来的结果如下,我们就可以利用这个信息进行处理。只有评论命中某个规则的时候,才去处理这段代码
image-20210812231108912实现对象数组排序
整体的实现代码如下,实现上并不难。整体思路:
是先获取到要比较的字段(比如上面例子中的
label
)。// 获取到 comment
const comment = context.getCommentsInside(node);
if (!comment) return;
// 获取到排序的字段
const field = comment[0] && comment[0].value && comment[0].value.split("'")[1];
if (!field) return;拿到数组中每一项目标字段对应的值([ 'a', 'C', 'B' ])。
// 取每一项排序对象中值
let fieldValueArr = node.elements.map(item => {
const target = (item.properties.find((prop) => {
return prop.key.name === field
}) || { value: '' });
return target.value && target.value.value;
});再对该数组进行前后顺序的检测,假如不符合我们就报错。
// 默认按照升序排序
for (let i = 1; i < fieldValueArr.length; i++) {
let reportError = false;
if (order === 'asc' && String(fieldValueArr[i]).localeCompare(String(fieldValueArr[i - 1])) < 0) {
reportError = true;
} else if (order === 'desc' && String(fieldValueArr[i]).localeCompare(String(fieldValueArr[i - 1])) > 0) {
reportError = true;
}
// 判断是否是降序
if (reportError) {
context.report({
node,
message: `数组排序不正确。请根据 ${field} 字段排序`,
});
break;
}
}
总结
Eslint
对于一个团队的代码规范是非常重要的,Eslint
自身带有很多有用的规则,本文介绍了 ESlint
的基础原理以及如何自定义 Eslint
插件来解决对象数组排序的问题,除此之外,我们可能还有其他的场景可以进行尝试,欢迎大家参与讨论~
参考
- ESLint 工作原理探讨[9]
- 自定义 ESLint 规则,让代码持续美丽
参考资料
[1]【优化】记一次通过工具减少 Git 冲突: https://juejin.cn/post/6895534290411454477
[2]sort-keys: https://eslint.org/docs/rules/sort-keys#rule-details
[3]Espree: https://github.com/eslint/espree
[4]astexplorer: https://astexplorer.net/
[5]官方文档: https://eslint.org/docs/developer-guide/selectors
[6]sort-keys: https://eslint.org/docs/rules/sort-keys#rule-details
[7]官方文档: https://link.juejin.cn?target=http%3A%2F%2Flink.zhihu.com%2F%3Ftarget%3Dhttps%3A%2F%2Feslint.org%2Fdocs%2Fdeveloper-guide%2Fworking-with-rules%23rule-basics
[8]Yeoman generator
: https://yeoman.io/authoring/
ESLint 工作原理探讨: https://juejin.cn/post/6844903749886935053#heading-5