阅读 1625

自定义 Eslint 开发

对于前端开发者来说,ESLint 是比较常用的代码规范和错误检查工具,ESLint 非常强大不仅提供了大量的常用 rules,还有许多实用的 ESLint 插件可以满足各样需求。但随着项目不断迭代发展,可能会遇到已有 ESLint 插件不能满足现在团队开发的情况。那么这时候,我们就需要自定义 Eslint 开发 ESLint Shareable ConfigESLint Plugins

ESLint Shareable Config 开发

可分享的扩展配置(eslint-config-<config-name> 是一个 ESLint 配置对象 npm 包,模块名称以 eslint-config-<config-name>@<scope>/eslint-config-<config-name> 命名,创建比较简单导出配置规则即可。

创建扩展配置

创建扩展配置非常简单,创建一个新的 index.js 文件并 export 一个包含配置的对象即可:

module.exports = {

    globals: {
        MyGlobal: true
    },

    rules: {
        semi: [2, "always"]
    }

}
复制代码

更多配置字段,参考 Configuring ESLint

使用扩展配置

npm 发布扩展包,引入 ESLint 配置:

module.exports = {
// extends: ['antife', 'myconfig'],
  extends: ['eslint-config-antife', 'eslint-config-myconfig'],
  globals: {
    'EVENT': true,
    'PAGE': true,
    'SCENE': true,
    'AlipayJSBridge': true,
  },
  plugins: [
    'babel',
    // 'html',  // eslint-plugin-html 从 <script> 标记中提取内容,eslint-plugin-vue 需要 <script> 标记和<template> 标记,两者同时存在会冲突
    'vue',
  ]
}
复制代码

Eslint plugin 开发

插件(eslint-plugin-<plugin-name> 是一个命名格式为 eslint-plugin-<plugin-name> 的 npm 包,模块名称以 eslint-plugin-<plugin-name>@<scope>/eslint-plugin-<plugin-name> 命名。

Eslint plugin 目录

我们可以利用 yeomangenerator-eslint 来构建插件的目录结构进行开发,这里我们选用自定义目录,如下:

├── README.md
├── _tests__
├── docs
├── index.js
└── rules
    └── my-rule.js
复制代码

插件主入口组成部分

  • Rules - 插件必须输出一个 rules对象,包含规则 ID 和对应规则的一个键值对。

  • Environments - 插件可以暴露额外的环境以在 ESLint 中使用。

  • Processors - 定义插件如何处理校验的文件。

  • Configs - 可以通过配置指定插件打包、编译方式,还可提供多种风格校验配置。

module.exports = {
    rules: {
        "my-rules": {
            create: function (context) {
                // rule implementation ...
            }
        }
    },
    env: {
        jquery: {
            globals: {
                $: false
            }
        }
    },
    configs: {
        myConfig: {
            parser: require.resolve('vue-eslint-parser'),
            parserOptions: {
                ecmaVersion: 2018,
                sourceType: 'module',
                ecmaFeatures: {
                    jsx: true
                }
            },
            plugins: ["myPlugin"],
            env: ["browser"],
            rules: {
                "myPlugin/my-rule": "error",
            }
        },
        myOtherConfig: {
            plugins: ["myPlugin"],
            env: ["node"],
            rules: {
                "myPlugin/my-rule": "off",
            }
        }
    },
    processors: {
        '.vue': {
            // takes text of the file and filename
            preprocess: function(text, filename) {
                // here, you can strip out any non-JS content
                // and split into multiple strings to lint

                return [string];  // return an array of strings to lint
            },

            // takes a Message[][] and filename
            postprocess: function(messages, filename) {
                // `messages` argument contains two-dimensional array of Message objects
                // where each top-level array item contains array of lint messages related
                // to the text that was returned in array from preprocess() method

                // you need to return a one-dimensional array of the messages you want to keep
                return messages[0];
            },

            supportsAutofix: true // (optional, defaults to false)
        }
    }
}
复制代码

Rules 创建

在开始编写新规则之前,请阅读官方的 ESLint指南,了解下 ESLint 的特点:

  • ESLint 使用 Espree 进行JavaScript解析。
  • ESLint 使用 AST 评估校验代码。
  • ESLint 是完全可插入的,每个规则都可以是一个插件。
  • ESLint 每条规则相互独立,可以设置禁用off、警告warn⚠️和报错error❌,当然还有正常通过不用给任何提示。

我们可以通过使用 astexplorer.net, 去了解 ESLint 如何使用 AST 评估校验代码,astexplorer.net 非常强大,还支持 Vue 模板。

规则组成部分

  • meta 对象包含规则的元数据

    • type 属性表示规则的类型,这是一个"problem""suggestion""layout"
    • docs 属性是ESLint的核心规则所必需的描述类信息
    • fixable 属性是"code""whitespace" ,如果规则不可修复,请省略fixable属性
    • schema 指定 options 以便ESLint可以防止无效的 规则配置
    • deprecated 属性指示规则是否已被弃用
    • replacedBy 属性表示在不建议使用的规则的情况下,指定替换规则
  • create 函数返回 ESLint 调用方法对象,通过该方法访问 JavaScript 代码的抽象语法树(由ESTree定义的AST)节点

  • context 对象包含与规则上下文相关的信息

    • 属性:

      • parserOptions - 插件配置的解析器选项

      • id - 规则ID。

      • options - 规则的 配置选项

      • settings- 配置中的 共享设置

      • parserPath parser - from配置的名称

      • parserServices - 包含解析器为规则提供的服务的对象

    • 方法:

      • getAncestors() - 返回当前遍历的节点的祖先数组,从AST的根部开始,一直到当前节点的直接父级

      • getCwd() - 将 cwd 传递的内容返回给Linter, 为当前工作目录

      • getDeclaredVariables - 返回给定节点声明的

      • getFilename() - 返回与源关联的文件名

      • getScope() - 返回当前遍历的节点的 scope ,用于跟踪对变量的引用

      • getSourceCode() - 返回一个 SourceCode 对象,可以使用该对象来处理传递给ESLint的源

      • markVariableAsUsed(name) - 在当前作用域中使用给定名称标记变量

      • report(descriptor) - 报告代码中的问题

"use strict";

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

module.exports = {
    meta: {
        type: "suggestion",

        docs: {
            description: "disallow unnecessary semicolons",
            category: "Possible Errors",
            recommended: true,
            url: "https://eslint.org/docs/rules/no-extra-semi"
        },
        fixable: "code",
        schema: [] // no options
    },
    create: function(context) {
        return {
          Identifier(node) {
              if (node.name === "foo") {
                  context.report({
                      node,
                      messageId: "avoidName",
                      data: {
                          name: "foo",
                      }
                  })
              }
            },
            ExportDefaultDeclaration(node){
              context.report({
                node,
                message: "test",
              })
            }
        }
    }
}
复制代码

若我们需要校验 Vue 模板,这里要注意由于Vue中的单个文件组件不是普通的 JavaScript,因此无法使用默认解析器,因此引入了新的解析器 vue-eslint-parser

要了解更多 vue AST 知识,可以查看

自定义 Processors

ESLint 插件开发,支持自定义处理器来处理 JavaScript 之外的文件,自定义处理器含有两个过程:preprocesspostprocess。自定义处理器大体结构如下:

module.exports = {
    processors: {

        // assign to the file extension you want (.js, .jsx, .html, etc.)
        ".ext": {
            // takes text of the file and filename
            preprocess: function(text, filename) {
                // here, you can strip out any non-JS content
                // and split into multiple strings to lint

                return [string];  // return an array of strings to lint
            },

            // takes a Message[][] and filename
            postprocess: function(messages, filename) {
                // `messages` argument contains two-dimensional array of Message objects
                // where each top-level array item contains array of lint messages related
                // to the text that was returned in array from preprocess() method

                // you need to return a one-dimensional array of the messages you want to keep
                return messages[0];
            },

            supportsAutofix: true // (optional, defaults to false)
        }
    }
};
复制代码

插件测试

ESLint 提供了 RuleTester 实用工具可以轻松地测试你插件中的规则,在 peerDependency 指向 ESLint 0.8.0 或之后的版本。

{
    "peerDependencies": {
        "eslint": ">=0.8.0"
    }
}
复制代码

peerDependencies 目的是提示宿主环境去安装满足插件peerDependencies所指定依赖的包,然后在插件import或者require所依赖的包的时候,永远都是引用宿主环境统一安装的npm包,最终解决插件与所依赖包不一致的问题。

// in the file to lint:

var foo = 2;
//  ^ error: Avoid using variables named 'foo'

// In your tests:
var rule = require("../rules/no-avoid-name")
var RuleTester = require("eslint").RuleTester

var ruleTester = new RuleTester()
ruleTester.run("no-avoid-name", rule, {
  valid: ["bar", "baz"],  // right data
  invalid: [  // error data
    {
      code: "foo",
      errors: [
          {
            messageId: "avoidName"
          }
      ]
    }
  ]
})

复制代码

实践开发 Vue 模版 Eslint plugin

在开发之前,这里要注意由于 Vue 中的单个文件组件并不是普通的 JavaScript,导致无法使用默认解析器,因此引入了新的解析器 vue-eslint-parser

{
    "parser": "vue-eslint-parser",
    "parserOptions": {
        "parser": "babel-eslint",
        "sourceType": "module",
        "allowImportExportEverywhere": false
    }
}
复制代码

开发 vue eslint 规则

这里要注意,涉及到自定义的解析器的,需要使用context.parserServices 访问该解析器解析的抽象语法树内容。

  • Vue 插件规则,示例
module.exports = {
    meta: {
      docs: {
        description: 'disallow unnecessary `v-bind` directives',
        url: 'https://eslint.vuejs.org/rules/no-useless-v-bind.html'
      },
      fixable: 'code',
      type: 'suggestion'
    },

    create(context) {
      if (context.parserServices.defineTemplateBodyVisitor == null) {
        context.report({
          loc: { line: 1, column: 0 },
          message:
            'Use the latest vue-eslint-parser. See also https://eslint.vuejs.org/user-guide/#what-is-the-use-the-latest-vue-eslint-parser-error'
        })
        return {}
      }
  
      return context.parserServices.defineTemplateBodyVisitor({
        VElement(node){
          if(node.name === "template"){
            context.report({
              node,
              message: "template标签",
            })
          }
        },
  
        Identifier(node){
          console.error('Identifier.name', node.name)
        }
  
      })
    }
  }
复制代码

若是校验 js,无需通过 context.parserServices.defineTemplateBodyVisitor 获取语法树信息

  • Vue 插件入口文件示例:
module.exports = {
    configs: {
      base: {
        parser: require.resolve('vue-eslint-parser'),
        plugins: ['boilerplate'],
        rules: {
          'boilerplate/no-avoid-name': 'error',
          'boilerplate/no-useless-v-bind': 'error'
        }
      }
    },
    env: {
      browser: true,
      es6: true
    },
    rules: {
      'no-avoid-name': require('./rules/no-avoid-name'),
      'no-useless-v-bind': require('./rules/no-useless-v-bind'),
    }
  }
复制代码
  • 配置使用
module.exports = {
    parser: 'vue-eslint-parser',
    parserOptions: {
      parser: 'babel-eslint',
        ecmaVersion: 2018,
        sourceType: 'module'
    },
    // vue 插件要放在 extend 前面,防止出现覆盖,"eslint:recommended" 是默认推荐的规则
    extends: ['plugin:vue/recommended', 'plugin:boilerplate/base', "eslint:recommended"],
    plugins: [
      'babel',
    ],
  }
复制代码

了解更多 vue AST 知识,可以查看

eslint-plugin-boilerplate

eslint-plugin-boilerplate —— 快速开发 eslint plugins 模版样例。

写到最后 - 招贤纳士

蚂蚁国际事业群深圳无线前端团队大量社招岗位 hc,详细请关注 Alipay Payment Services Hong Kong Limited-高级前端研发工程师/专家,小伙伴们,可以加我微信 gluuu1 内推,机会更大~~

Other Resource

Eslint

The ESLint Vue Plugin Developer Guide

Working with Rules

开发一个 plugin 插件

Shareable Configs

vue-eslint-parser AST docs

【AST篇】教你如何动手写 Eslint 插件

文章分类
前端
文章标签