Loading...
墨滴

Gopal

2021/10/05  阅读:24  主题:橙心

【实战】自定义 ESLint Plugin

背景

之前做过一个小分享——【优化】记一次通过工具减少 Git 冲突[1]。主要讲的是通过利用 git hooks 在代码提交之前给相关的代码排序,从而减少合代码时候的冲突。

上次同事提醒说,这个 Eslint 就可以做到。我回去查了一下,还真可以,详情见 sort-keys[2]。假如使用了这条规则,就是要求对象写法要遵循一定的顺序。比如开启这个规则的话,默认情况下下面的代码就会报错:

let obj = {a1c3b2};

应该为:

let obj = {a1b2c3};

但是其实我们的诉求中,还有一种场景,那就是对象数组。比如下面这个场景,我需要根据 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; 如下所示:

image-20210812220540504
image-20210812220540504

竟然我们知道它的结构,我们就可以直接去检测它合不合法了。

我们来讲一个重要的概念——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",
      recommendedfalse,
      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
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 插件来解决对象数组排序的问题,除此之外,我们可能还有其他的场景可以进行尝试,欢迎大家参与讨论~

参考

参考资料

[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/

[9]

ESLint 工作原理探讨: https://juejin.cn/post/6844903749886935053#heading-5

Gopal

2021/10/05  阅读:24  主题:橙心

作者介绍

Gopal

Shopee