教你手写个 eslint 插件

起因

之前需要 Vue 全局错误管理,但是每个人的代码风格都不一样,导致有些错误无法报告到全局,所以就去看了一下 eslint 相关的规则,发现没有可以直接用的,然后就自己写了一个插件,下面写一下 eslint 插件的简单开发流程。

自定义规则

假设在 vue 中我们想尽可能的用同步写法代替回调写法,从 $nextTick 开始

例如:

// vue 某个方法中
async xxx (){
    this.$nextTick(() => {
        // do something
    })
}

我们想变成

// vue 某个方法中
async xxx (){
    await this.$nextTick()
    // do something
}

// 异步函数调用注意:
// 咱们调用 xxx 方法的时候,假设该方法是 async 函数,或者返回的是 promise
// 这个时候我们直接执行 xxx(),是无法捕获错误的,
// 需要 await xxx(),才可以捕获错误

开始

eslint 官方推荐使用 yeoman 配合 generator-eslint(eslint 官方开发的基于 yeoman 的生成器)生成一个 eslint 插件的开发模板,yeoman 后面有时间会单独讲下,是个好东西。

安装依赖

// 安装 yeoman
npm i -g yo

// 安装 eslint 插件模板生成器
npm i -g generator-eslint

初始化自己的 eslint 插件

eslint 插件的名字必须是这种格式:eslint-plugin-(插件名字)

// 先创建一个文件夹,然后进去

// 初始化 eslint 插件模板
yo eslint:plugin

image.png

初始化 eslint 规则

yo eslint:rule

image.png

项目结构

image.png

eslint 运行原理

简单来说,eslint 会把代码解析成 AST 语法树,然后我们通过 AST 去实现我们的自定义规则!

image.png

AST

AST 在线转换网站

简单来说 AST 就是描述代码的一个抽象语法树,代码或者框架可能有各种各样的写法,但是我们只要将其转换为 AST 语法树,就可以通过操作 AST 语法树去随意操控我们的代码。

简单的例子:

image.png

开发注意

官方创建 eslint 规则文档

下图是 eslint 规则输出的基本格式,这个 create 函数是精髓

image.png

书写 await-to-next-tick 规则

image.png

// 对照一下上面的 AST 语法树,下面就是一个简单的判断,我们的规则就 ok 了。
module.exports = {
    ...,
    create: function(context) {
        return {
            'CallExpression' (node) {
                if (
                    node?.arguments?.length && // 判断调用 $nextTick 是否传入回调
                    node?.callee?.object?.type === 'ThisExpression' &&
                    node?.callee?.property?.name === '$nextTick'
                    ){
                        // 报告错误
                        ctx.report({
                            node,
                            message: '使用 await 调用该函数'
                        })
                }
            }
        }
    }
};

如何调试 eslint 规则

1.修改一波入口文件

/**
 * @fileoverview 佐助的自定义规则
 * @author 佐助
 */
"use strict";

//------------------------------------------------------------------------------
// Requirements
//------------------------------------------------------------------------------

var requireIndex = require("requireindex");

//------------------------------------------------------------------------------
// Plugin Definition
//------------------------------------------------------------------------------

module.exports = {
  // 引入所有的规则
  rules: requireIndex(__dirname + "/rules"),
  configs: {
    // 自定义配置
    recommed: {
      plugins: ['zz-rule'],
      rules: {
        'zz-rule/await-to-next-tick': 2
      }
    }
  },
}

2.eslint 插件的根目录下

// eslint 插件的根目录下
npm link

3.在 vue2 工程中使用 eslint 插件

// Vue2 工程根目录下
npm link eslint-plugin-zz-rule

4.引入咱们的插件

// eslint config 文件中
extends: [
  // 对应咱们上面的插件名称
  "plugin:zz-rule/recommed"
],

注意

我以 vscode 举例(首先得安装 eslint 扩展程序),然后进入 vue2 工程,你点击这个就可以看到 eslint 插件到时候的输出

如果你更改了 eslint 的代码,记得重新打开 vue2 工程才会生效

image.png

image.png

规则效果

鼠标触摸上去就会有咱们的错误提示~!

image.png

如何自动修复我们的错误

1.看下 eslint 官方创建规则的文档

image.png

2.写一个修复函数

/**
 * @fileoverview 使用 await 调用该函数
 * @author 佐助
 */
"use strict";

//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------

module.exports = {
    meta: {
        docs: {
            description: "使用 await 调用该函数",
            category: "Fill me in",
            recommended: false
        },
        fixable: 'code',  // 要修复的话就把这个改为 code
        schema: [
            // fill in your schema
        ]
    },

    create: function(ctx) {
        return {
            'CallExpression' (node) {
                if (
                    node?.arguments?.length &&
                    node?.callee?.object?.type === 'ThisExpression' &&
                    node?.callee?.property?.name === '$nextTick'
                ) {
                    // 返回一个SourceCode对象,你可以使用该对象处理传递给 ESLint 的源代码。
                    const sourceCode = ctx.getSourceCode()
                    
                    // node.arguments 就是我们 this.$nextTick(我是参数函数) 里面的参数
                    // .body 就是参数函数体
                    // .body.body 就是参数函数体里面的具体代码
                    let content = `\n${Array.from(node.arguments[0].body.body)
                        .map(item => sourceCode.getText(item))
                        .join('\n')}`
                 
                    ctx.report({
                        node,
                        message: '使用 await 调用该函数',
                        fix(fixer) {
                            return [
                                // 给函数前面加个 await
                                fixer.insertTextBefore(node, 'await '),
                                // 删除 this.$nextTick(我是参数函数) 里面的参数
                                fixer.remove(node.arguments[0]),
                                // 把用户写的代码放到函数下面去
                                fixer.insertTextAfter(node , content)
                            ]
                        }
                    })
                }
            }
        }
    }
};

3.效果

image.png

image.png

正式发布 npm

// eslint 插件根目录下

npm login

npm publish 
// Vue2 工程中

npm i -D eslint-plugin-zz-rule

最后注意

1.记得修改测试文件,每次发布之前 npm run test,保证规则的稳定和正确

image.png

2.该 demo 没有进行严谨的测试,只给大家提供开发思路,大家开发的时候记得要多测试!

3. demo 地址