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

60 篇原创好文~
本文首发于政采云前端团队博客:自定义 ESLint 规则,让代码持续美丽
javascript

背景

一段真实的代码发展历史

好久好久之前,有一个需求,而后产出了一段代码,代码优雅而简洁前端

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

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

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

通过屡次产品需求迭代后,如今的代码imagenode

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

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

那如何避免呢?github

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

制定代码规范确定是须要的,那如何约束代码呢?规范文档宣讲,再凭借开发同窗的自我修养?答案是:没法保证。数据库

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

若是咱们在开发者写代码的时候就及时给到提示和建议,那开发体验就很棒了,而 ESLint 的自定义规则就能够实如今开发过程当中给开发同窗友好的提示。json

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 地址的资源路径
  • ...

参考文献

招贤纳士

政采云前端团队(ZooTeam),一个年轻富有激情和创造力的前端团队,隶属于政采云产品研发部,Base 在风景如画的杭州。团队现有 50 余个前端小伙伴,平均年龄 27 岁,近 3 成是全栈工程师,妥妥的青年风暴团。成员构成既有来自于阿里、网易的“老”兵,也有浙大、中科大、杭电等校的应届新人。团队在平常的业务对接以外,还在物料体系、工程平台、搭建平台、性能体验、云端应用、数据分析及可视化等方向进行技术探索和实战,推进并落地了一系列的内部技术产品,持续探索前端技术体系的新边界。

若是你想改变一直被事折腾,但愿开始能折腾事;若是你想改变一直被告诫须要多些想法,却无从破局;若是你想改变你有能力去作成那个结果,却不须要你;若是你想改变你想作成的事须要一个团队去支撑,但没你带人的位置;若是你想改变既定的节奏,将会是“5 年工做时间 3 年工做经验”;若是你想改变原本悟性不错,但老是有那一层窗户纸的模糊… 若是你相信相信的力量,相信平凡人能成就非凡事,相信能遇到更好的本身。若是你但愿参与到随着业务腾飞的过程,亲手推进一个有着深刻的业务理解、完善的技术体系、技术创造价值、影响力外溢的前端团队的成长历程,我以为咱们该聊聊。任什么时候间,等着你写点什么,发给 ZooTeam@cai-inc.com

相关文章
相关标签/搜索