Eslint 从入门到实战

avatar
前端工程师 @Aloudata

为什么要使用eslint

原因很简单,使用 eslint 好处 > 缺点;

优点如下:

- 有的规则可以帮我们避免错误

- 有的规则可以帮我们写出最佳实践的代码

- 有的规则可以帮我们规范变量的使用方式

- 有的规则可以帮我们规范代码格式

- 有的规则可以帮我们更合适的使用新的语法

- …

缺点:不能放飞自我,代码不能随便写;

如下图:

eslint使用

eslint只需要配置.eslintrc.js文件即可;

常见配置字段如下:(下面的配置主要是介绍字段,非正常配置)

extends: [
    'airbnb-typescript',//相当于eslint-config-airbnb-typescript ,会从这个包读取导出的配置,是对默认规则使用的配置
    'plugin:react-hooks/recommended',// 使用eslint-plugin-react-hooks这个插件包的config.recommended配置
    'plugin:prettier/recommended',//同上,从eslint-plugin-prettier 的配置中读取config.recommend配置,
],
plugins:[
    'import',//相当于添加eslint-plugin-import 中所有的自定义rule
],
parser: '@typescript-eslint/parser',//表示eslint会使用'@typescript-eslint/parser'作为解析器来解析代码,生成ast树
parserOptions:{
	
},//设置解析器选项
ignorePatterns:['pathPatterns'],//eslint会忽略匹配到的文件
rules:{
   'linebreak-style''off',//表示eslint的默认规则内 linebreak-style这条规则关闭
     'react/require-default-props''off',//表示eslint-plugin-react的自定义规则内 require-default-props这条规则关闭
}

eslint 禁用 

cn.eslint.org/docs/user-g…

eslint 原理

第一步:ESLint使用指定的JavaScript解析器把JS代码解析成AST,默认是Espree

第二步:深度遍历生成的 AST,将每一个 node 传入 nodeQueue 队列中,每个会被传入两次。(node: 节点)

第三步:遍历所有给定的规则,创建 rule 对象,执行 rule 对象的 create 方法,返回 ruleListeners 对象(这个对象里面包含了 rule 的选择器和回调函数),遍历 ruleListeners 对象(每个 rule 可以有多个选择器),为规则中所有的选择器添加监听事件。

第四步:遍历 nodeQueue 队列,触发匹配到当前 node 的选择器的监听事件,执行相应的回调函数。

 eslint 实践

0. 开发目标

目标:禁止umi history.push方法传字符串

原因:因为我们项目中的guid里有"#",用这种姿势跳转会自动把"#"encode

1. 开发准备

eslint推荐Yeoman脚手架生成器来创建项目,

第一步:安装yogenerator-eslint

npm install yo generator-eslint -g

第二步:生成脚手架

yo eslint:plugin

第三步:生成rule模板文件

把指令添加到package.json 的scripts中 "createRule": "yo eslint:rule”

npm run createRule

此时,项目的目录结构是这样的

.
├── README.md
├── docs
│   └── rules
│       └── history-push-no-string.md
├── lib
│   ├── index.js
│   └── rules
│       └── history-push-no-string.js
├── tests
│   └── lib
│       └── rules
│           └── history-push-no-string.js
├── node-modules
├── package.json
└── package-lock.json

开发

第一步:编写测例

在我们创建的 tests/lib/rules/history-push-no-string.js 模板文件中,按照模板填入自己设定的有效使用和无效使用:

ruleTester.run("history-push-no-string", rule, {
  valid: [
    {
      code: `
          const history = [];
          history.push('/some/path');
      `,
      parserOptions: { ecmaVersion6, sourceType"module" }
    },
    {
      code: `
      import {history} from 'umi';
      history.push({
            path'/some/path'
      });
      `,
      parserOptions: { ecmaVersion6, sourceType"module" }
    }

  ],

  invalid: [
    {
      code: `
        import {history as h} from 'umi';
        h.push('/some/path');
      `,
      parserOptions: { ecmaVersion6, sourceType"module" },
      errors: [{ message"umi history的[push]方法必须使用对象参数" }],
    },
    {
      code: `
        import {history} from 'umi';
        function test() {
          history.replace('/some/path');
        }
      `,
      parserOptions: { ecmaVersion6, sourceType"module" },
      errors: [{ message"umi history的[replace]方法必须使用对象参数" }],
    },
  ],
});

第二步:开发规则

我们需要把校验规则填在生成的lib/rules/history-push-no-string.js模板文件中

/**
 * @fileoverview desc
 * @author xiuji
 */
"use strict";

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

/**
 * @type {import('eslint').Rule.RuleModule}
 */
module.exports = {
  meta: {
    typenull// `problem`, `suggestion`, or `layout`
    docs: {
      description"desc",
      category"Fill me in",
      recommendedfalse,
      urlnull// URL to the documentation page for this rule
    },
    fixablenull// Or `code` or `whitespace`
    schema: [], // Add a schema if the rule has options
  },

  create(context) {
    // variables should be defined here

    //----------------------------------------------------------------------
    // Helpers
    //----------------------------------------------------------------------

    // any helper functions should go here or else delete this section

    //----------------------------------------------------------------------
    // Public
    //----------------------------------------------------------------------

    return {
      // visitor functions for different types of nodes
    };
  },
};

rule规则开发文档详情

cn.eslint.org/docs/develo…

第三步:把测例解析成AST

语法解析

eslint官方:cn.eslint.org/parser/

推荐:astexplorer.net/

如下图可以看到,history.push的关键调用类型为CallExpression,我们可以通过这个节点的type,name以及arguments的类型,来做判断

create(context) {
    // variables should be defined here

    //----------------------------------------------------------------------
    // Helpers
    //----------------------------------------------------------------------

    // any helper functions should go here or else delete this section

    //----------------------------------------------------------------------
    // Public
    //----------------------------------------------------------------------
    return {
      // visitor functions for different types of nodes
      CallExpressionfunction (node) {
        const callee = node.callee;
        const { typeobject, property } = callee;
        if (type === 'MemberExpression' && object.type === 'Identifier' && property.type === 'Identifier') {
          if (property.name==='history'&&['push''replace'].includes(property.name)) {
            if (node.arguments[0].type !== 'ObjectExpression') {
    //到这里就可以判断调用了history.push('string'),通过context.report报告问题
              context.report({
                  node,
                  message`umi history的[${property.name}]方法必须使用对象参数`
                })
            }
          }
        }
      },
    };
  },

第四步:增加history和umi的关联

思考以下场景:

import { history } from 'someApi'
import {history as xxx } from 'umi'

function (){
    history.push('abc');
    xxx.push({
    	path:'path/route'
    })
}

按照第三步的判断,一定是会报错的,所以,我们要把xxx和umi关联起来,这个时候,凭借查看AST树是没法做到的。

在上面的规则文档中,中提供了两个跟踪对变量的引用的api

1. context.getDeclaredVariables(node)

2. context.getScope()

因为我们的node节点是CallExpression类型,使用1拿不到相关数据,所以我选择了getScope(),拿到了海量深层次数据,这个时候,我们需要配置vscode来调试我们运行的程序

按照以下步骤添加调试

1. 如下图,创建launch.json (已存在可以忽略)

2. 文件中填入以下内容,name 和program路径可根据实际修改。

{
  "version""0.2.0",
  "configurations": [
    {
      "type""node",
      "request""launch",
      "name""调试history-push-no-string"// 调试界面的名称
      // 运行项目下的这个文件:
      "program""${workspaceFolder}/tests/lib/rules/history-push-no-string.js",
      "args": [] // node 文件的参数
    },
  ]
}

3. 在项目中打断点,并启动调试,如下图

通过调试,我们找到了history和  umi 的关联,通过编写一个函数,判断push("string")的调用者是不是来自于umi 的history;

create(context) {
    // variables should be defined here

    //----------------------------------------------------------------------
    // Helpers
    //----------------------------------------------------------------------

    // any helper functions should go here or else delete this section

    //----------------------------------------------------------------------
    // Public
    //----------------------------------------------------------------------
    const isDefinedByUMI = (scope, identifierName) => {
      const { block } = scope;
      const isDefined = (block) => {
        if (block.type === 'Program') {
          let body = block.body;
          for (let i = 0; i < body.length; i++) {
            let { source, specifiers } = body[i];
            if (source && source.value === 'umi') {
              for (let j = 0; j < specifiers.length; j++) {
                let { local } = specifiers[j];
                if (local.name === identifierName) {
                  return true;
                }
              }
            }
          }
          return false;
        } else {
          return isDefined(block.parent);
        }
      }
      return isDefined(block);
    }
    return {
      // visitor functions for different types of nodes
      CallExpression: function (node) {
        const callee = node.callee;
        const { type, object, property } = callee;
        if (type === 'MemberExpression' && object.type === 'Identifier' && property.type === 'Identifier') {
          if (['push', 'replace'].includes(property.name)) {
            if (node.arguments[0].type !== 'ObjectExpression') {
              // 走到这里已经确定是(someIdentifier).push('string')调用,最后判断是全局是否有umi的history对象
              const scope = context.getScope();
              const identifierName = object.name;
              const exists = isDefinedByUMI(scope, identifierName);
              if (exists) {
                context.report({
                  node,
                  message: `umi history的[${property.name}]方法必须使用对象参数`
                })
              }
            }
          }
        }
      },
    };
  },

最后,查看单测运行结果

至此,我们完成了eslint插件及规则的开发

项目接入

在我们创建的eslint插件项目中执行

npm link

在workstation项目中执行

npm link eslint-plugin-aloudata-lint

在.eslintrc.js 文件中集成插件:

// .eslintrc.js
plugins: ['aloudata-lint'],
rules: {
  ...
  'aloudata-lint/history-push-no-string': ['error'],
}

执行校验命令

npm run lint-js

参考资料

团队介绍

以上便是本次分享的全部内容,来自团队 @修己 分享,希望对你有所帮助^_^

喜欢的话别忘了 分享、点赞、收藏 。

我们是来自大应科技(Aloudata, www.aloudata.com )的前端团队,负责大应科技全线产品前端开发工作。在一家初创公司,前端团队不仅要快速完成业务迭代,还要同时建设前端工程化的落地和新技术的预研,我们会围绕产品品质、开发效率、创意与前沿技术等多方向对大前端进行探索和分享,包括但不限于性能监控、组件库、前端框架、可视化、3D 技术、在线编辑器等等。