文章正文

自定义 ESLint 规则,让代码持续美丽

2020-07-12 发布于 · 阅读量:409

背景

一段真实的代码发展历史

很久很久以前,有一个需求,然后产出了一段代码,代码优雅而简洁

export const getConfig = (param1, param2) => {
  return ...
};

不久又来了个需求,加个参数扩展,so easy!

export const getConfig = (param1, param2, param3) => {
  return ...
};

经过多次产品需求迭代后,现在的代码image

export const getConfig = (param1, param2, param3, param4, param5, param6, param7……) => {
  return ...
};

在产品迭代过程中,上面的 case 一个函数的参数从 2 个发展到了 7 个,优雅的代码逐渐变为不可维护。 这是什么问题?这归咎于日益增长的需求,快速响应和代码质量之间的矛盾。

那如何避免呢?

  • 制定代码规范
  • 靠开发同学的自我修养
  • 进行 Code Review
  • 工具提示
  • 发版控制,不允许发版

制定代码规范肯定是需要的,那如何约束代码呢?规范文档宣讲,再凭借开发同学的自我修养?答案是:无法保证。

Code Review ?但难免也有落网之鱼。发版控制?能有效解决但是开发体验不好。

如果我们在开发者写代码的时候就及时给到提示和建议,那开发体验就很棒了,而 ESLint 的自定义规则就可以实现在开发过程中给开发同学友好的提示。

ESLint 原理

ESLint 是一个代码检查工具,通过静态的分析,寻找有问题的模式或者代码。默认使用 Espree 解析器将代码解析为 AST 抽象语法树,然后再对代码进行检查。

看下最简单的一段代码使用 espree 解析器转换成的抽象语法树结构,此处可以使用 astexplorer 快速方便查看解析成 AST 的结构:

代码片段:

var a = 1;

转换出的结果:

{
  "type": "Program",
  "start": 0,
  "end": 10,
  "range": [
    0,
    10
  ],
  "body": [
    {
      "type": "VariableDeclaration",
      "start": 0,
      "end": 10,
      "range": [
        0,
        10
      ],
      "declarations": [
        {
          "type": "VariableDeclarator",
          "start": 4,
          "end": 9,
          "range": [
            4,
            9
          ],
          "id": {
            "type": "Identifier",
            "start": 4,
            "end": 5,
            "range": [
              4,
              5
            ],
            "name": "a"
          },
          "init": {
            "type": "Literal",
            "start": 8,
            "end": 9,
            "range": [
              8,
              9
            ],
            "value": 1,
            "raw": "1"
          }
        }
      ],
      "kind": "var"
    }
  ],
  "sourceType": "module"
}

代码转换为 AST 后,可以很方便的对代码的每个节点对代码进行检查。

自定义 ESLint 规则开发

怎么自定义

语法树分析

对目标代码进行语法树解析,可使用 astexplorer

编写规则

下面是一个规则简单的结构(官方API文档说明

module.exports = {
  meta: {
    docs: {
      description: "最多参数允许参数",
    },
  },
  create: function (context) {
    return {
      FunctionDeclaration: (node) => {
        if (node.params.length > 3) {
          context.report({
            node,
            message: "参数最多不能超过3个",
          });
        }
      },
    };
  },
};
  • meta(对象)包含规则的元数据
  • create ( function ) 返回一个对象,其中包含了 ESLint 在遍历 JavaScript 代码的抽象语法树 AST ( ESTree 定义的 AST ) 时,用来访问节点的方法
  • context.report ( ) 用来发布警告或错误,并能提供自动修复功能(取决于你所使用的配置)

最简单的示例(只使用 node 和 message 参数):

context.report({
    node,
    message: "参数最多不能超过3个",
});

使用上面的这个规则,结合编辑器就有了对整个 node 节点的提示,如果需要更精确的错误或警告提示,我们可以使用 loc 参数, API 文档说明

image

如何使用自定义规则

使用自定义的 ESLint 规则,你需要自定义一个 ESLint 的插件,然后将规则写到自定义的 ESLint 插件中,然后在业务代码中添加 ESLint 配置,引入 ESLint 插件。

ESLint 插件

创建

创建一个 ESLint plugin,并创建 一个 ESLint rule

基于 Yeoman generator ,可以快速创建 ESLint plugin 项目。

npm i -g yo
npm i -g generator-eslint
// 创建一个plugin
yo eslint:plugin
// 创建一个规则
yo eslint:rule

创建好的项目目录结构:

  • rules 文件夹存放的是各个规则文件
  • tests 文件夹存放单元测试文件
  • package.json 是你的 ESLint 插件 npm 包的说明文件,其中的 name 属性就是你的 ESLint 插件的名称,命名规则:带前缀 eslint-plugin-

示例代码:

lib/rules/max-params.js

module.exports = {
  meta: {
    docs: {
      description: "最多参数",
    },
  },
  create: function (context) {
    /**
     * 获取函数的参数的开始、结束位置
     * @param {node} node AST Node 
     */
    function getFunctionParamsLoc(node) {
      const paramsLength = node.params.length;
      return {
        start: node.params[0].loc.start,
        end: node.params[paramsLength - 1].loc.end,
      };
    }
    return {
      FunctionDeclaration: (node) => {
        if (node.params.length > 3) {
          context.report({
            loc: getFunctionParamsLoc(node),
            node,
            message: "参数最多不能超过3个",
          });
        }
      },
    };
  },
};

补充测试用例

/tests/lib/rules/max-params.js

var ruleTester = new RuleTester();
ruleTester.run("max-params", rule, {
  valid: ["function test(d, e, f) {}"],
  invalid: [
    {
        code: "function test(a, b, c, d) {}",
        errors: [{
            message: "参数最多不能超过3个",
        }]
    },
  ],
});

ESLint 插件安装

在需要的业务代码中安装你的 ESLint 插件。(eslint-plugin-my-eslist-plugin 是你的 ESLint 插件 npm 包的包名)

npm install eslint-plugin-my-eslist-plugin 

如果你的 npm 包还未发布,需要进行本地调试:

可使用 npm link 本地调试,npm link 的使用

配置

添加你的 plugin 包名(eslint-plugin- 前缀可忽略) 到 .eslintrc 配置文件的 plugins 字段。

.eslintrc 配置文件示例:

{
    "plugins": [
        "zoo" // 你的 ESLint plugin 的名字
    ]
}

rules 中再将 plugin 中的规则导入。 ⚠️ ESLint更新后,需要重启 vscode,才能生效。( vscode 重启快捷方式:CTRL +SHITF + P,输入 Reload Window

此处涉及 ESLint 的规则设置(参考说明

{
    "rules": {
        "zoo/rule-name": 2
    }
}

效果

image

实际应用案例

函数、方法的入参个数控制,其实已经在 ESLint 的规则中了。在业务场景中,我们需要对我们的业务规则编写自定义的 ESLint 规则。

一个简单的业务场景:业务中通常会出现跳转到很多不同的业务域名的操作,不同的环境有不同的域名,我们需要从配置中取出域名使用,而不是采取硬编码域名的方案。

由此我们产生出了一个规则:禁止硬编码业务域名。

规则为:

module.exports = {
  meta: {
    type: "suggestion",
    docs: {
      description: "不允许硬编码业务域名",
    },
    fixable: "code",
  },

  create: function (context) {
    const sourceCode = context.getSourceCode();

    function checkDomain(node) {
      // 匹配硬编码的业务域名的正则
      const Reg = /^(http:\/\/|https:\/\/|\/\/)(.*.){0,1}zcygov(.com|cn)(.*)/;
      const content =
        (node.type === "Literal" && node.value) ||
        (node.type === "TemplateLiteral" && node.quasis[0].value.cooked);

      const domainNode =
        (node.type === "Literal" && node) ||
        (node.type === "TemplateLiteral" && node.quasis[0]);

      if (Reg.test(content)) {
        context.report({
          node,
          // 错误/警告提示信息
          message: "不允许硬编码业务域名",
          // 修复
          fix(fixer) {

            const fixes = [];

            let domainKey = content.match(Reg)[2];
            domainKey = domainKey
              ? domainKey.substr(0, domainKey.length - 1)
              : "";

            if (node.type === "Literal") {
              fixes.push(
                fixer.replaceTextRange(
                  [domainNode.start + 1, domainNode.end - 1],
                  content.replace(Reg, `$4`)
                )
              );
            }

            if (node.type === "TemplateLiteral") {
              fixes.push(
                fixer.replaceTextRange(
                  [domainNode.start, domainNode.end],
                  content.replace(Reg, `$4`)
                )
              );
            }

            if (
              node.type === "Literal" &&
              node.parent.type === "JSXAttribute"
            ) {
              fixes.push(fixer.insertTextBefore(node, "{"));
              fixes.push(fixer.insertTextAfter(node, "}"));
            }

            fixes.push(
              fixer.insertTextBefore(
                node,
                `window.getDomain('${domainKey}') + `
              )
            );

            return fixes;
          },
        });
      }
    }
    return {
      // 文本
      Literal: checkDomain,
      // 模板字符串
      TemplateLiteral: checkDomain,
    };
  },
};

补充测试用例

/tests/lib/rules/no-zcy-domain.js

var rule = require("../../../lib/rules/no-zcy-domain"),
    RuleTester = require("eslint").RuleTester;

var ruleTester = new RuleTester();
ruleTester.run("no-zcy-domain", rule, {
  valid: [
    "bar",
    "baz",
    `
  var s = {
    x: "zcygov"
  };
  `,
  ],
  invalid: [
    {
      code: `
              var s = "//zcygov.cn"
            `,
      errors: [
        {
          message: "不允许硬编码业务域名",
        },
      ],
    },
    {
      code: `
            var s = {
              x: "http://bidding.zcygov.cn"
            };
            `,
      errors: [
        {
          message: "不允许硬编码业务域名",
        },
      ],
    },
  ],
});

结合 vscode 保存自动修复 ESLint 错误的功能,效果如下:

更多的应用场景

除了上面说的硬编码的场景,还可以将沉淀出的最佳实践和业务规范通过自定义 ESLint 的方式来提示开发者,这对于多人协助、代码维护、代码风格的一致性都会有很大的帮助。

更多的应用场景有:

  • Input 必须要有 maxlength 属性,防止请求的后端接口数据库异常
  • 代码中不能出现加减乘除等计算,如果需要计算应该引入工具函数,来控制由于前端浮点数计算引起的 Bug
  • 规范限制,单位元的两边的括号要用英文括号,不能用中文括号,来达到交互展示统一的效果
  • 代码中不能使用 OSS 地址的静态资源路径,应该使用 CDN 地址的资源路径
  • ...

参考文献

❉ 作者介绍 ❉