這篇文章目的是介紹如何創建一個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文件
- 點擊下圖中的設置按鈕, 將會打開一個文件
launch.json
- 在文件中填入如下內容,用於調試node文件。
- 在
rule
文件中打debugger
或者在代碼行數那里點一下小紅點。 - 點擊圖中的開始按鈕,進入
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 //這個端口是調試的端口,不是項目啟動的端口
},
]
}
運行測試用例進入斷點
- 在
lib/rules/settimeout-no-number.js
中打一些debugger
- 點擊開始按鈕,以調試的形式運行測試文件
tests/lib/rules/settimeout-no-number.js
- 開始調試
rule
。
發布插件
eslint插件都是以npm
包的形式來引用的,所以需要把插件發布一下:
-
注冊:如果你還未注冊npm賬號的話,需要去注冊一下。
-
登錄npm:
npm login
-
發布
npm
包:npm publish
即可,ESLint已經把package.json
弄好了。
集成到項目:
安裝npm
包:npm i eslint-plugin-korolint -D
- 常規的方法:
引入插件一條條寫入規則
// .eslintrc.js
module.exports = {
plugins: [ 'korolint' ],
rules: {
"korolint/settimeout-no-number": "error"
}
}
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)
- 在rule的meta對象上打開修復功能:
// rule文件
module.exports = {
meta: {
docs: {
description: 'setTimeout 第二個參數禁止是數字'
},
fixable: 'code' // 打開修復功能
}
}
- 在
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),
];
}
});
項目地址:
呼~ 這篇博客斷斷續續,寫了好幾周,終於完成了!
大家有看到這篇博客的話,建議跟着博客的一起動手寫一下,動手實操一下比你mark一百篇文章都來的有用,花不了很長時間的,希望各位看完本文,都能夠更深入的了解到ESLint的運行原理。
覺得我的博客對你有幫助的話,就關注一下/點個贊吧!
前端進階積累、公眾號、GitHub、wx:OBkoro1、郵箱:obkoro1@foxmail.com
基友帶我飛
ESLint插件是向基友yeyan1996學習的,在遇到問題的時候,也是他指點我的,特此感謝。
參考資料: