这篇文章目的是介绍如何创建一个ESLint插件和创建一个ESLint rule,用以帮助我们更深入的理解ESLint的运行原理,并且在有必要时可以根据需求创建出一个完美满足自己需求的Lint规则。

插件目标

禁止项目中setTimeout的第二个参数是数字。

PS: 如果是数字的话,很容易就成为魔鬼数字,没有人知道为什么是这个数字, 这个数字有什么含义。

使用模板初始化项目:

1. 安装NPM包

ESLint官方为了方便开发者开发插件,提供了使用Yeoman模板(generator-eslint)。

对于Yeoman我们只需知道它是一个脚手架工具,用于生成包含指定框架结构的工程化目录结构。

  1. npm install -g yo generator-eslint

2. 创建一个文件夹:

  1. mkdir eslint-plugin-demo
  2. cd eslint-plugin-demo

3. 命令行初始化ESLint插件的项目结构:

  1. yo eslint:plugin

下面进入命令行交互流程,流程结束后生成ESLint插件项目框架和文件。

  1. ? What is your name? OBKoro1
  2. ? What is the plugin ID? korolint // 这个插件的ID是什么
  3. ? Type a short description of this plugin: XX公司的定制ESLint rule // 输入这个插件的描述
  4. ? Does this plugin contain custom ESLint rules? Yes // 这个插件包含自定义ESLint规则吗?
  5. ? Does this plugin contain one or more processors? No // 这个插件包含一个或多个处理器吗
  6. // 处理器用于处理js以外的文件 比如.vue文件
  7. create package.json
  8. create lib/index.js
  9. create README.md

现在可以看到在文件夹内生成了一些文件夹和文件,但我们还需要创建规则具体细节的文件。

4. 创建规则

上一个命令行生成的是ESLint插件的项目模板,这个命令行是生成ESLint插件具体规则的文件。

  1. yo eslint:rule // 生成 eslint rule的模板文件

创建规则命令行交互:

  1. ? What is your name? OBKoro1
  2. ? Where will this rule be published? (Use arrow keys) // 这个规则将在哪里发布?
  3. ESLint Core // 官方核心规则 (目前有200多个规则)
  4. ESLint Plugin // 选择ESLint插件
  5. ? What is the rule ID? settimeout-no-number // 规则的ID
  6. ? Type a short description of this rule: setTimeout 第二个参数禁止是数字 // 输入该规则的描述
  7. ? Type a short example of the code that will fail: 占位 // 输入一个失败例子的代码
  8. create docs/rules/settimeout-no-number.md
  9. create lib/rules/settimeout-no-number.js
  10. create tests/lib/rules/settimeout-no-number.js

加了具体规则文件的项目结构

  1. .
  2. ├── README.md
  3. ├── docs // 使用文档
  4.    └── rules // 所有规则的文档
  5.    └── settimeout-no-number.md // 具体规则文档
  6. ├── lib // eslint 规则开发
  7.    ├── index.js 引入+导出rules文件夹的规则
  8.    └── rules // 此目录下可以构建多个规则
  9.    └── settimeout-no-number.js // 规则细节
  10. ├── package.json
  11. └── tests // 单元测试
  12. └── lib
  13. └── rules
  14. └── settimeout-no-number.js // 测试该规则的文件

4. 安装项目依赖

  1. npm install

以上是开发ESLint插件具体规则的准备工作,下面先来看看AST和ESLint原理的相关知识,为我们开发ESLint rule 打一下基础。

AST——抽象语法树

AST是: Abstract Syntax Tree的简称,中文叫做:抽象语法树。

AST的作用

将代码抽象成树状数据结构,方便后续分析检测代码。

代码被解析成AST的样子

astexplorer.net是一个工具网站:它能查看代码被解析成AST的样子。

如下图:在右侧选中一个值时,左侧对应区域也变成高亮区域,这样可以在AST中很方便的选中对应的代码

AST 选择器:

下图中被圈起来的部分,称为AST selectors(选择器)。

AST 选择器的作用:使用代码通过选择器来选中特定的代码片段,然后再对代码进行静态分析。

AST 选择器很多,ESLint官方专门有一个仓库列出了所有类型的选择器: estree

下文中开发ESLint rule就需要用到选择器,等下用到了就懂了,现在知道一下就好了。


ESLint的运行原理

在开发规则之前,我们需要ESLint是怎么运行的,了解插件为什么需要这么写。

1. 将代码解析成AST

ESLint使用JavaScript解析器Espree把JS代码解析成AST。

PS:解析器:是将代码解析成AST的工具,ES6、react、vue都开发了对应的解析器所以ESLint能检测它们的,ESLint也是因此一统前端Lint工具的。

2. 深度遍历AST,监听匹配过程。

在拿到AST之后,ESLint会以"从上至下"再"从下至上"的顺序遍历每个选择器两次。

3. 触发监听选择器的rule回调

在深度遍历的过程中,生效的每条规则都会对其中的某一个或多个选择器进行监听,每当匹配到选择器,监听该选择器的rule,都会触发对应的回调。

4. 具体的检测规则等细节内容。


开发规则

规则默认模板

打开rule生成的模板文件lib/rules/settimeout-no-number.js, 清理一下文件,删掉不必要的选项:

  1. module.exports = {
  2. meta: {
  3. docs: {
  4. description: "setTimeout 第二个参数禁止是数字",
  5. },
  6. fixable: null, // 修复函数
  7. },
  8. // rule 核心
  9. create: function(context) {
  10. // 公共变量和函数应该在此定义
  11. return {
  12. // 返回事件钩子
  13. };
  14. }
  15. };

删掉的配置项,有些是ESLint官方核心规则才是用到的配置项,有些是暂时不必了解的配置,需要用到的时候,可以自行查阅ESLint 文档

create方法-监听选择器

上文ESLint原理第三部中提到的:在深度遍历的过程中,生效的每条规则都会对其中的某一个或多个选择器进行监听,每当匹配到选择器,监听该选择器的rule,都会触发对应的回调。

create返回一个对象,对象的属性设为选择器,ESLint会收集这些选择器,在AST遍历过程中会执行所有监听该选择器的回调。

  1. // rule 核心
  2. create: function(context) {
  3. // 公共变量和函数应该在此定义
  4. return {
  5. // 返回事件钩子
  6. Identifier: (node) => {
  7. // node是选中的内容,是我们监听的部分, 它的值参考AST
  8. }
  9. };
  10. }

观察AST:

创建一个ESLint rule需要观察代码解析成AST,选中你要检测的代码,然后进行一些判断。

以下代码都是通过astexplorer.net在线解析的。

  1. setTimeout(()=>{
  2. console.log('settimeout')
  3. }, 1000)

rule完整文件

lib/rules/settimeout-no-number.js:

  1. module.exports = {
  2. meta: {
  3. docs: {
  4. description: "setTimeout 第二个参数禁止是数字",
  5. },
  6. fixable: null, // 修复函数
  7. },
  8. // rule 核心
  9. create: function (context) {
  10. // 公共变量和函数应该在此定义
  11. return {
  12. // 返回事件钩子
  13. 'CallExpression': (node) => {
  14. if (node.callee.name !== 'setTimeout') return // 不是定时器即过滤
  15. const timeNode = node.arguments && node.arguments[1] // 获取第二个参数
  16. if (!timeNode) return // 没有第二个参数
  17. // 检测报错第二个参数是数字 报错
  18. if (timeNode.type === 'Literal' && typeof timeNode.value === 'number') {
  19. context.report({
  20. node,
  21. message: 'setTimeout第二个参数禁止是数字'
  22. })
  23. }
  24. }
  25. };
  26. }
  27. };

context.report():这个方法是用来通知ESLint这段代码是警告或错误的,用法如上。在这里查看contextcontext.report()的文档。

规则写完了,原理就是依据AST解析的结果,做针对性的检测,过滤出我们要选中的代码,然后对代码的值进行逻辑判断

可能现在会有点懵逼,但是不要紧,我们来写一下测试用例,然后用debugger来看一下代码是怎么运行的。

测试用例:

测试文件tests/lib/rules/settimeout-no-number.js:

  1. /**
  2. * @fileoverview setTimeout 第二个参数禁止是数字
  3. * @author OBKoro1
  4. */
  5. "use strict";
  6. var rule = require("../../../lib/rules/settimeout-no-number"), // 引入rule
  7. RuleTester = require("eslint").RuleTester;
  8. var ruleTester = new RuleTester({
  9. parserOptions: {
  10. ecmaVersion: 7, // 默认支持语法为es5
  11. },
  12. });
  13. // 运行测试用例
  14. ruleTester.run("settimeout-no-number", rule, {
  15. // 正确的测试用例
  16. valid: [
  17. {
  18. code: 'let someNumber = 1000; setTimeout(()=>{ console.log(11) },someNumber)'
  19. },
  20. {
  21. code: 'setTimeout(()=>{ console.log(11) },someNumber)'
  22. }
  23. ],
  24. // 错误的测试用例
  25. invalid: [
  26. {
  27. code: 'setTimeout(()=>{ console.log(11) },1000)',
  28. errors: [{
  29. message: "setTimeout第二个参数禁止是数字", // 与rule抛出的错误保持一致
  30. type: "CallExpression" // rule监听的对应钩子
  31. }]
  32. }
  33. ]
  34. });

下面来学习一下怎么在VSCode中调试node文件,用于观察rule是怎么运行的。

实际上打console的形式,也是可以的,但是在调试的时候打console实在是有点慢,对于node这种节点来说,信息也不全,所以我还是比较推荐通过debugger的方式来调试rule

在VSCode中调试node文件

  1. 点击下图中的设置按钮, 将会打开一个文件launch.json
  2. 在文件中填入如下内容,用于调试node文件。
  3. rule文件中打debugger或者在代码行数那里点一下小红点。
  4. 点击图中的开始按钮,进入debugger

  1. {
  2. // 使用 IntelliSense 了解相关属性。
  3. // 悬停以查看现有属性的描述。
  4. // 欲了解更多信息,请访问: https://go.microsoft.com/fwlink/?linkid=830387
  5. "version": "0.2.0",
  6. "configurations": [
  7. {
  8. "type": "node",
  9. "request": "launch",
  10. "name": "启动程序", // 调试界面的名称
  11. // 运行项目下的这个文件:
  12. "program": "${workspaceFolder}/tests/lib/rules/settimeout-no-number.js",
  13. "args": [] // node 文件的参数
  14. },
  15. // 下面是用于调试package.json的命令 之前可以用,貌似vscode出了点bug导致现在用不了了
  16. {
  17. "name": "Launch via NPM",
  18. "type": "node",
  19. "request": "launch",
  20. "runtimeExecutable": "npm",
  21. "runtimeArgs": [
  22. "run-script", "dev" //这里的dev就对应package.json中的scripts中的dev
  23. ],
  24. "port": 9229 //这个端口是调试的端口,不是项目启动的端口
  25. },
  26. ]
  27. }

运行测试用例进入断点

  1. lib/rules/settimeout-no-number.js中打一些debugger
  2. 点击开始按钮,以调试的形式运行测试文件tests/lib/rules/settimeout-no-number.js
  3. 开始调试rule

发布插件

eslint插件都是以npm包的形式来引用的,所以需要把插件发布一下:

  1. 注册:如果你还未注册npm账号的话,需要去注册一下。

  2. 登录npm: npm login

  3. 发布npm包: npm publish即可,ESLint已经把package.json弄好了。

集成到项目:

安装npm包:npm i eslint-plugin-korolint -D

  1. 常规的方法: 引入插件一条条写入规则
  1. // .eslintrc.js
  2. module.exports = {
  3. plugins: [ 'korolint' ],
  4. rules: {
  5. "korolint/settimeout-no-number": "error"
  6. }
  7. }
  1. extends继承插件配置:

当规则比较多的时候,用户一条条去写,未免也太麻烦了,所以ESLint可以继承插件的配置

修改一下lib/rules/index.js文件:

  1. 'use strict';
  2. var requireIndex = require('requireindex');
  3. const output = {
  4. rules: requireIndex(__dirname + '/rules'), // 导出所有规则
  5. configs: {
  6. // 导出自定义规则 在项目中直接引用
  7. koroRule: {
  8. plugins: ['korolint'], // 引入插件
  9. rules: {
  10. // 开启规则
  11. 'korolint/settimeout-no-number': 'error'
  12. }
  13. }
  14. }
  15. };
  16. module.exports = output;

使用方法:

使用extends来继承插件的配置,extends不止这种继承方式,即使你传入一个npm包,一个文件的相对路径地址,eslint也能继承其中的配置。

  1. // .eslintrc.js
  2. module.exports = {
  3. extends: [ 'plugin:korolint/koroRule' ] // 继承插件导出的配置
  4. }

PS : 这种使用方式, npm的包名不能为eslint-plugin-xx-xx,只能为eslint-plugin-xx否则会有报错,被这个问题搞得头疼o(╥﹏╥)o

扩展:

以上内容足够开发一个插件,这里是一些扩展知识点。

遍历方向:

上文中说过: 在拿到AST之后,ESLint会以"从上至下"再"从下至上"的顺序遍历每个选择器两次。

我们所监听的选择器默认会在"从上至下"的过程中触发,如果需要在"从下至上"的过程中执行则需要添加:exit,在上文中CallExpression就变为CallExpression:exit

注意:一段代码解析后可能包含多次同一个选择器,选择器的钩子也会多次触发。

fix函数:自动修复rule错误

修复效果

  1. // 修复前
  2. setTimeout(() => {
  3. }, 1000)
  4. // 修复后 变量名故意写错 为了让用户去修改它
  5. const countNumber1 = 1000
  6. setTimeout(() => {
  7. }, countNumber2)
  1. 在rule的meta对象上打开修复功能:
  1. // rule文件
  2. module.exports = {
  3. meta: {
  4. docs: {
  5. description: 'setTimeout 第二个参数禁止是数字'
  6. },
  7. fixable: 'code' // 打开修复功能
  8. }
  9. }
  1. context.report()上提供一个fix函数:

把上文的context.report修改一下,增加一个fix方法即可,更详细的介绍可以看一下文档

  1. context.report({
  2. node,
  3. message: 'setTimeout第二个参数禁止是数字',
  4. fix(fixer) {
  5. const numberValue = timeNode.value;
  6. const statementString = `const countNumber = ${numberValue}\n`
  7. return [
  8. // 修改数字为变量
  9. fixer.replaceTextRange(node.arguments[1].range, 'countNumber'),
  10. // 在setTimeout之前增加一行声明变量的代码 用户自行修改变量名
  11. fixer.insertTextBeforeRange(node.range, statementString),
  12. ];
  13. }
  14. });

项目地址:

eslint-plugin-korolint


呼~ 这篇博客断断续续,写了好几周,终于完成了!

大家有看到这篇博客的话,建议跟着博客的一起动手写一下,动手实操一下比你mark一百篇文章都来的有用,花不了很长时间的,希望各位看完本文,都能够更深入的了解到ESLint的运行原理。

觉得我的博客对你有帮助的话,就关注一下/点个赞吧!

前端进阶积累公众号GitHub、wx:OBkoro1、邮箱:obkoro1@foxmail.com

基友带我飞

ESLint插件是向基友yeyan1996学习的,在遇到问题的时候,也是他指点我的,特此感谢。

参考资料:

创建规则

ESLint 工作原理探讨

前端工具-定制ESLint 插件以及了解ESLint的运行原理的更多相关文章

  1. 中小型前端团队代码规范工程化最佳实践 - ESLint

    前言 There are a thousand Hamlets in a thousand people's eyes. 一千个程序员,就有一千种代码风格.在前端开发中,有几个至今还在争论的代码风格差 ...

  2. 前端规范之JS代码规范(ESLint + Prettier)

    代码规范是软件开发领域经久不衰的话题,几乎所有工程师在开发过程中都会遇到或思考过这一问题.而随着前端应用的大型化和复杂化,越来越多的前端团队也开始重视代码规范.同样,前段时间,笔者所在的团队也开展了一 ...

  3. 【AST篇】教你如何编写 Eslint 插件

    前言 虽然现在已经有很多实用的 ESLint 插件了,但随着项目不断迭代发展,你可能会遇到已有 ESLint 插件不能满足现在团队开发的情况.这时候,你需要自己来创建一个 ESLint 插件. 本文我 ...

  4. electron教程(番外篇一): 开发环境及插件, VSCode调试, ESLint + Google JavaScript Style Guide代码规范

    我的electron教程系列 electron教程(一): electron的安装和项目的创建 electron教程(番外篇一): 开发环境及插件, VSCode调试, ESLint + Google ...

  5. 一款检测代码中TODO的eslint插件

    一款检测代码中TODO的eslint插件 前言 看了我标题进来的同学应该也知道我做的是个啥东西 没错是一个eslint插件,前端魔法师们日常所使用的工具之一 什么?你不知道eslint是干嘛的--吃鲸 ...

  6. vscode安装eslint插件,代码统一自动修复

    ESlint:是用来统一JavaScript代码风格的工具,不包含css.html等. 方法和步骤: 通常情况下vue项目都会添加eslint组件,我们可以查看webpack的配置文件package. ...

  7. vscode如何安装eslint插件 代码自动修复

    ESlint:是用来统一JavaScript代码风格的工具,不包含css.html等. 方法和步骤: 通常情况下vue项目都会添加eslint组件,我们可以查看webpack的配置文件package. ...

  8. vscode的eslint插件不起作用

    最近在用vue进行开发,但是vsCode中的eslint插件装上之后不起作用 1.vsCode打开“设置”,选择"settings.json" 2.输入一段脚本 "esl ...

  9. vscode 添加eslint插件

    1. 安装vscode中的eslint插件 Ctrl + Shift + P 调出控制台,输入install,再在插件版块查找ESLint,安装 2. 安装node,安装npm 3. 全局安装ESLi ...

随机推荐

  1. 学习笔记26_MVC前台强类型参数

    *一般在MVC中,aspx后台要往前台传递参数,使用ViewData["Key"] = obj; 前台就要 <%=(ViewData["key"] as ...

  2. SasS 设计原则十二因素

    Heroku 是业内知名的云应用平台,从对外提供服务以来,他们已经有上百万应用的托管和运营经验.其创始人 Adam Wiggins 根据这些经验,发布了一个“十二要素应用宣言(The Twelve-F ...

  3. 你知道MySQL中的主从延迟吗?

    前言 在一个MySQL主备关系中,每个备库接受主库的binlog并执行. 正常情况下,只要主库执行更新生成所有的binlog,都可以传到备库并被正常的执行,这样备库就能够达到跟主库一样的状态,这就是最 ...

  4. .NET手撸绘制TypeScript类图——下篇

    .NET手撸绘制TypeScript类图--下篇 在上篇的文章中,我们介绍了如何使用.NET解析TypeScript,这篇将介绍如何使用代码将类图渲染出来. 注:以防有人错过了,上篇链接如下:http ...

  5. python中字符串常见操作(二)

    # 可迭代对象有:字典,列表,元组,字符串,集合 str1 = '192.168.1.1' str2 = 'as df gh jk' str3 = '小李子' str4 = ['aa','bb','c ...

  6. MySQL-配置环境变量及修改密码(附-mysql安装教程)

    MySQL-配置环境变量和修改密码 mysql的安装教程:链接:https://pan.baidu.com/s/1rrPT2X0yRF58kN8jZZx-Mg 密码:55dh 一. 闪退问题 1.1. ...

  7. ajax传出数组到后台

    var vote = new Array();    $("input[name='option_name']").each(function(i){        if($(th ...

  8. Javascript模块化开发2——Gruntfile.js详解

    一.grunt模块简介 grunt插件,是一种npm环境下的自动化工具.对于需要反复重复的任务,例如压缩.编译.单元测试.linting等,自动化工具可以减轻你的劳动,简化你的工作.grunt模块根据 ...

  9. redis 底层数据结构

    简单动态字符串SDS 包含字符串长度,剩余可用长度,字符数组 用于Redis中所有的string存储 字典(map) 数组+链表形式,跟hashMap很像 链地址法解决hash冲突 rehash使用新 ...

  10. lufylegend.js教程(1)

    1.图片元素如何缩小? 在LSprite类中,有两个属性:{scaleX,scaleY},这两个属性属于按比例缩放精灵对象,可以放大,可以缩小,注意这两个属性是在图片中心点位置开始缩放. 代码: Bo ...