天天看點

前端工具-定制ESLint 插件以及了解ESLint的運作原理

這篇文章目的是介紹如何建立一個ESLint插件和建立一個

ESLint

rule

,用以幫助我們更深入的了解ESLint的運作原理,并且在有必要時可以根據需求建立出一個完美滿足自己需求的Lint規則。

插件目标

禁止項目中

setTimeout

的第二個參數是數字。

PS: 如果是數字的話,很容易就成為魔鬼數字,沒有人知道為什麼是這個數字, 這個數字有什麼含義。

使用模闆初始化項目:

1. 安裝NPM包

ESLint官方為了友善開發者開發插件,提供了使用Yeoman模闆(

generator-eslint

)。

對于Yeoman我們隻需知道它是一個腳手架工具,用于生成包含指定架構結構的工程化目錄結構。

npm install -g yo generator-eslint
           

2. 建立一個檔案夾:

mkdir eslint-plugin-demo
cd eslint-plugin-demo
           

3. 指令行初始化ESLint插件的項目結構:

yo eslint:plugin
           

下面進入指令行互動流程,流程結束後生成ESLint插件項目架構和檔案。

? What is your name? OBKoro1
? What is the plugin ID? korolint   // 這個插件的ID是什麼
? Type a short description of this plugin: XX公司的定制ESLint rule // 輸入這個插件的描述
? Does this plugin contain custom ESLint rules? Yes // 這個插件包含自定義ESLint規則嗎?
? Does this plugin contain one or more processors? No // 這個插件包含一個或多個處理器嗎
// 處理器用于處理js以外的檔案 比如.vue檔案
   create package.json
   create lib/index.js
   create README.md
           

現在可以看到在檔案夾内生成了一些檔案夾和檔案,但我們還需要建立規則具體細節的檔案。

4. 建立規則

上一個指令行生成的是ESLint插件的項目模闆,這個指令行是生成ESLint插件具體規則的檔案。
yo eslint:rule // 生成 eslint rule的模闆檔案
           

建立規則指令行互動:

? What is your name? OBKoro1
? Where will this rule be published? (Use arrow keys) // 這個規則将在哪裡釋出?
❯ ESLint Core  // 官方核心規則 (目前有200多個規則)
  ESLint Plugin  // 選擇ESLint插件
? What is the rule ID? settimeout-no-number  // 規則的ID
? Type a short description of this rule: setTimeout 第二個參數禁止是數字  // 輸入該規則的描述
? Type a short example of the code that will fail:  占位  // 輸入一個失敗例子的代碼
   create docs/rules/settimeout-no-number.md
   create lib/rules/settimeout-no-number.js
   create tests/lib/rules/settimeout-no-number.js
           

加了具體規則檔案的項目結構

.
├── README.md
├── docs // 使用文檔
│   └── rules // 所有規則的文檔
│       └── settimeout-no-number.md // 具體規則文檔
├── lib // eslint 規則開發
│   ├── index.js 引入+導出rules檔案夾的規則
│   └── rules // 此目錄下可以建構多個規則
│       └── settimeout-no-number.js // 規則細節
├── package.json
└── tests // 單元測試
    └── lib
        └── rules
            └── settimeout-no-number.js // 測試該規則的檔案
           

4. 安裝項目依賴

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

, 清理一下檔案,删掉不必要的選項:

module.exports = {
    meta: {
        docs: {
            description: "setTimeout 第二個參數禁止是數字",
        },
        fixable: null,  // 修複函數
    },
   // rule 核心
    create: function(context) {
       // 公共變量和函數應該在此定義
        return {
            // 傳回事件鈎子
        };
    }
};
           

删掉的配置項,有些是ESLint官方核心規則才是用到的配置項,有些是暫時不必了解的配置,需要用到的時候,可以自行查閱ESLint 文檔

create方法-監聽選擇器

上文ESLint原理第三部中提到的:在深度周遊的過程中,生效的每條規則都會對其中的某一個或多個選擇器進行監聽,每當比對到選擇器,監聽該選擇器的rule,都會觸發對應的回調。

create

傳回一個對象,對象的屬性設為選擇器,ESLint會收集這些選擇器,在AST周遊過程中會執行所有監聽該選擇器的回調。

// rule 核心
create: function(context) {
    // 公共變量和函數應該在此定義
    return {
        // 傳回事件鈎子
        Identifier: (node) => {
            // node是選中的内容,是我們監聽的部分, 它的值參考AST
        }
    };
}
           

觀察AST:

建立一個ESLint

rule

需要觀察代碼解析成AST,選中你要檢測的代碼,然後進行一些判斷。

以下代碼都是通過astexplorer.net線上解析的。

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

rule完整檔案

lib/rules/settimeout-no-number.js

:

module.exports = {
    meta: {
        docs: {
            description: "setTimeout 第二個參數禁止是數字",
        },
        fixable: null,  // 修複函數
    },
    // rule 核心
    create: function (context) {
        // 公共變量和函數應該在此定義
        return {
            // 傳回事件鈎子
            'CallExpression': (node) => {
                if (node.callee.name !== 'setTimeout') return // 不是定時器即過濾
                const timeNode = node.arguments && node.arguments[1] // 擷取第二個參數
                if (!timeNode) return // 沒有第二個參數
                // 檢測報錯第二個參數是數字 報錯
                if (timeNode.type === 'Literal' && typeof timeNode.value === 'number') {
                    context.report({
                        node,
                        message: 'setTimeout第二個參數禁止是數字'
                    })
                }
            }
        };
    }
};
           

context.report():這個方法是用來通知ESLint這段代碼是警告或錯誤的,用法如上。在這裡檢視

context

context.report()

的文檔。

規則寫完了,原理就是依據

AST

解析的結果,做針對性的檢測,過濾出我們要選中的代碼,然後對代碼的值進行邏輯判斷。

可能現在會有點懵逼,但是不要緊,我們來寫一下測試用例,然後用

debugger

來看一下代碼是怎麼運作的。

測試用例:

測試檔案

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

/**
 * @fileoverview setTimeout 第二個參數禁止是數字
 * @author OBKoro1
 */
"use strict";
var rule = require("../../../lib/rules/settimeout-no-number"), // 引入rule
    RuleTester = require("eslint").RuleTester;

var ruleTester = new RuleTester({
    parserOptions: {
        ecmaVersion: 7, // 預設支援文法為es5 
    },
});
// 運作測試用例
ruleTester.run("settimeout-no-number", rule, {
    // 正确的測試用例
    valid: [
        {
            code: 'let someNumber = 1000; setTimeout(()=>{ console.log(11) },someNumber)'
        },
        {
            code: 'setTimeout(()=>{ console.log(11) },someNumber)'
        }
    ],
    // 錯誤的測試用例
    invalid: [
        {
            code: 'setTimeout(()=>{ console.log(11) },1000)',
            errors: [{
                message: "setTimeout第二個參數禁止是數字", // 與rule抛出的錯誤保持一緻
                type: "CallExpression" // rule監聽的對應鈎子
            }]
        }
    ]
});
           

下面來學習一下怎麼在VSCode中調試node檔案,用于觀察

rule

是怎麼運作的。

實際上打

console

的形式,也是可以的,但是在調試的時候打console實在是有點慢,對于node這種節點來說,資訊也不全,是以我還是比較推薦通過

debugger

的方式來調試

rule

在VSCode中調試node檔案

  1. 點選下圖中的設定按鈕, 将會打開一個檔案

    launch.json

  2. 在檔案中填入如下内容,用于調試node檔案。
  3. rule

    檔案中打

    debugger

    或者在代碼行數那裡點一下小紅點。
  4. 點選圖中的開始按鈕,進入

    debugger

{
    // 使用 IntelliSense 了解相關屬性。 
    // 懸停以檢視現有屬性的描述。
    // 欲了解更多資訊,請通路: https://go.microsoft.com/fwlink/?linkid=830387
    "version": "0.2.0",
    "configurations": [
        {
            "type": "node",
            "request": "launch",
            "name": "啟動程式", // 調試界面的名稱
            // 運作項目下的這個檔案:
            "program": "${workspaceFolder}/tests/lib/rules/settimeout-no-number.js",
            "args": [] // node 檔案的參數
        },
        // 下面是用于調試package.json的指令 之前可以用,貌似vscode出了點bug導緻現在用不了了
        {
            "name": "Launch via NPM",
            "type": "node",
            "request": "launch",
            "runtimeExecutable": "npm",
            "runtimeArgs": [
                "run-script", "dev"    //這裡的dev就對應package.json中的scripts中的dev
            ],
            "port": 9229    //這個端口是調試的端口,不是項目啟動的端口
        },
    ]
}
           

運作測試用例進入斷點

  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. 正常的方法:

    引入插件一條條寫入規則

// .eslintrc.js
module.exports = {
  plugins: [ 'korolint' ],
  rules: { 
    "korolint/settimeout-no-number": "error"
 }
}
           
  1. extends

    繼承插件配置:

當規則比較多的時候,使用者一條條去寫,未免也太麻煩了,是以ESLint可以繼承插件的配置:

修改一下

lib/rules/index.js

檔案:

'use strict';
var requireIndex = require('requireindex');
const output = {
  rules: requireIndex(__dirname + '/rules'), // 導出所有規則
  configs: {
    // 導出自定義規則 在項目中直接引用
    koroRule: {
      plugins: ['korolint'], // 引入插件
      rules: {
        // 開啟規則
        'korolint/settimeout-no-number': 'error'
      }
    }
  }
};
module.exports = output;
           

使用方法:

使用

extends

來繼承插件的配置,

extends

不止這種繼承方式,即使你傳入一個npm包,一個檔案的相對路徑位址,eslint也能繼承其中的配置。

// .eslintrc.js
module.exports = {
  extends: [ 'plugin:korolint/koroRule' ] // 繼承插件導出的配置
}
           

PS : 這種使用方式, npm的包名不能為

eslint-plugin-xx-xx

,隻能為

eslint-plugin-xx

否則會有報錯,被這個問題搞得頭疼o(╥﹏╥)o

擴充:

以上内容足夠開發一個插件,這裡是一些擴充知識點。

周遊方向:

上文中說過: 在拿到AST之後,ESLint會以"從上至下"再"從下至上"的順序周遊每個選擇器兩次。

我們所監聽的選擇器預設會在"從上至下"的過程中觸發,如果需要在"從下至上"的過程中執行則需要添加

:exit

,在上文中

CallExpression

就變為

CallExpression:exit

注意:一段代碼解析後可能包含多次同一個選擇器,選擇器的鈎子也會多次觸發。

fix函數:自動修複rule錯誤

修複效果:

// 修複前
setTimeout(() => {

}, 1000)
// 修複後 變量名故意寫錯 為了讓使用者去修改它
const countNumber1 = 1000
setTimeout(() => {

}, countNumber2)
           
  1. 在rule的meta對象上打開修複功能:
// rule檔案
module.exports = {
  meta: {
    docs: {
      description: 'setTimeout 第二個參數禁止是數字'
    },
    fixable: 'code' // 打開修複功能
  }
}
           
  1. context.report()

    上提供一個

    fix

    函數:

把上文的

context.report

修改一下,增加一個

fix

方法即可,更詳細的介紹可以看一下文檔。

context.report({
    node,
    message: 'setTimeout第二個參數禁止是數字',
    fix(fixer) {
        const numberValue = timeNode.value;
        const statementString = `const countNumber = ${numberValue}\n`
        return [
        // 修改數字為變量
        fixer.replaceTextRange(node.arguments[1].range, 'countNumber'),
        // 在setTimeout之前增加一行聲明變量的代碼 使用者自行修改變量名
        fixer.insertTextBeforeRange(node.range, statementString),
        ];
    }
});
           

項目位址:

eslint-plugin-korolint

呼~ 這篇部落格斷斷續續,寫了好幾周,終于完成了!

大家有看到這篇部落格的話,建議跟着部落格的一起動手寫一下,動手實操一下比你mark一百篇文章都來的有用,花不了很長時間的,希望各位看完本文,都能夠更深入的了解到ESLint的運作原理。

覺得我的部落格對你有幫助的話,就關注一下/點個贊吧!

前端進階積累、公衆号、GitHub、wx:OBkoro1、郵箱:[email protected]

基友帶我飛

ESLint插件是向基友yeyan1996學習的,在遇到問題的時候,也是他指點我的,特此感謝。

參考資料:

建立規則

ESLint 工作原理探讨

GitHub:https://github.com/OBKoro1,

wx:OBkoro1,

郵箱:[email protected]