開始
前面已經寫了兩篇關於eslint的文章了,想必都對eslint應該有一個簡單的認識了,在平常的項目中使用應該是問題不大,面試應該也是問題不大的,大家有興趣可以看看前面兩篇文章:
接下來我們更深入的瞭解一下eslint,我們直接結合demo創建一個我們自己的plugin。
我們還是用前面我們的eslint-demo項目,代碼已經上傳github了大家可以直接clone一份,我們在eslint-demo項目根目錄直接創建一個plugins目錄,然後創建一個.eslintrc.json文件,
.eslintrc.json:
{
"root": true
}
爲了不跟根目錄的配置文件衝突,我們直接加上了“root”:true的標識。
env
env前面文章有介紹過,我就不在這裏介紹了,比如我們有一個叫“fox”的運行環境,然後有一個“fox”的全局變量,還記得我們之前demo的做法嗎?我們直接在config的globals變量中定義了一個叫fox的變量,這樣我們的eslint就能識別fox變量了,
{
...
"globals": {
"fox": "readonly"
},
...
}
這一次我們通過自定義env把它當成一個fox環境,讓它變成下面這樣的效果:
{
"env":{
"@fox/fox":true
}
}
首先我們在plugins目錄創建一個fox-plugin目錄,然後在fox-plugin目錄執行npm init:
cd ./plugins/fox-plugin && npm init
讓fox-plugin的入口指向index.js,
package.json:
{
"name": "@fox/eslint-plugin",
"version": "1.0.0",
"description": "自定義eslint插件",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC"
}
可以看到,我們創建了一個帶@fox命名空間的插件,然後我們在fox-plugin目錄創建一個入口文件index.js,
index.js:
module.exports = {
environments: {
fox: require("./env/fox")
}
};
可以看到,我們直接導出了一個對象,然後裏面包含environments字段,environments對象中又包含一個fox屬性,fox屬性就是我們的自定義fox運行環境,我們在目錄底下創建一個env目錄,然後在env目錄創建一個fox.js文件,
plugins/fox-plugin/env/fox.js:
const eslintEnv = require("eslint/conf/environments"); //獲取eslint的所有環境變量
module.exports = {
globals: {
...eslintEnv.get("es2020").globals, //合併eslint的es2020變量
...eslintEnv.get("browser").globals, //合併eslint的browser變量
...{ //合併自定義的環境變量
fox: false,
}
},
parserOptions: {
ecmaVersion: 11 //設置語法爲es2020
}
};
可以看到,我們在上面代碼中合併了eslint中的原始es2020跟browser環境變量,然後還自定義了一個全局fox變量,並且設置es的語法版本爲11,也就是說我們的fox環境=(es2020+browser+自定義fox)。
ok,我們已經定義完plugin了,然後也定義好了我們的env,那麼項目中該怎麼使用呢?
首先我們直接在我們根目錄執行npm install把我們的插件引入進來:
npm install -D xxx/eslint-demo/plugins/fox-plugin //注意xxx是你本地的絕對路徑
執行完畢後我們會在根目錄的node-modules目錄下面找到我們的插件:
然後在配置文件中引入我們的插件和環境
plugins/.eslintrc.json:
{
"root": true,
"env": {
"@fox/fox": true
},
"plugins": [
"@fox"
],
"extends": [
"eslint:recommended"
]
}
同時爲了看效果,我們加入了eslint的recommended規則。
接下來我們創建一個測試文件來測試一下,我們在plugins的src目錄中創建一個demo1.js文件,
demo1.js:
document.write("hello plugin");
fox.say("hello world");
然後我們在根目錄執行一下eslint命令:
➜ eslint-demo git:(v0.0.1) ✗ npx eslint ./plugins/src/*
➜ eslint-demo git:(v0.0.1) ✗
可以看到,沒有任何報錯,那我們把我們的fox環境去掉試試,
plugins/.eslintrc.json:
{
"root": true,
"env": {
},
"plugins": [
"@fox"
],
"extends": [
"eslint:recommended"
]
}
再次運行:
xxx/Study/eslint-demo/plugins/src/demo1.js
1:1 error 'document' is not defined no-undef
2:1 error 'fox' is not defined no-undef
✖ 2 problems (2 errors, 0 warnings)
➜ eslint-demo git:(v0.0.1) ✗
可以看到,eslint直接報錯了,說“document”跟“fox”變量找不到。
processors
前面有介紹過processors,關於eslint的基礎配置跟源碼部分我們這一節不說明了,小夥伴可以去看前面的兩篇文章,
比如我們現在有一個.fox後綴的文件,然後我們通過自定義processors來預解析文件,我們直接copy一個demo1.js文件取名字叫demo-processors.fox,
plugins/src/demo-processors.fox:
document.write("hello plugin");
fox.say("hello world");
首先我們在fox-plugin目錄創建一個processors目錄,然後在processors目錄中創建一個fox.js文件
plugins/fox-plugin/processors/fox.js:
module.exports = {
preprocess: function (text, filename) {
text = text.replace(/plugin/g, "yasin");
console.log("text", text);
return [text]; // return an array of strings to lint
},
postprocess: function (messages, filename) {
const problems = messages[0];
return problems.map((problem) => {
return {
...problem,
message: problem.message+"(found by Yasin!)"
};
});
},
supportsAutofix: true // (optional, defaults to false)
};
我們直接導入了一個對象,然後裏面包含了preprocess跟postprocess方法,在preprocess方法中,我直接字符串替換掉了“plugin”爲“yasin”,然後返回一個數組,數組裏麪包含的是需要eslint校驗的代碼塊,在postprocess方法中我們對eslint處理過後的message做一次封裝,把所有的message都帶有一個“(found by Yasin!)”的說明。
接下來我們在插件的清單文件中導出這個processor,
plugins/fox-plugin/index.js:
module.exports = {
environments: {
fox: require("./env/fox")
},
processors: {
".fox": require("./processors/fox")
}
};
可以看到,我們聲明瞭一個“processors”字段,然後在processors中聲明瞭一個".fox"字段指向我們自定義的processor,
注意:“.fox”是你要作用的文件的後綴,也就是說我們的processor只會作用我們的“.fox”後綴文件。
在插件中導出processors後是不需要在配置文件中申明的,eslint會根據插件自動找到".fox"的processor。
plugins/.eslintrc.json:
{
"root": true,
"env": {
"@fox/fox": true
},
"plugins": [
"@fox"
],
"extends": [
"eslint:recommended"
],
"rules": {
"semi": ["error","always"]
}
}
在根目錄運行eslint:
➜ eslint-demo git:(v0.0.1) ✗ npx eslint ./plugins/src/*
text document.write("hello yasin");
fox.say("hello world");
➜ eslint-demo git:(v0.0.1) ✗
可以看到,打印我們的demo-processors.fox文件中的內容,並且替換了字符串“eslint”爲“yasin”,然後沒有報錯,我們試着修改下代碼讓它報錯,我們直接去掉一個分號“;”,
plugins/src/demo-processors.fox:
document.write("hello plugin");
fox.say("hello world")
再次運行eslint:
➜ eslint-demo git:(v0.0.1) ✗ npx eslint ./plugins/src/*
text document.write("hello yasin");
fox.say("hello world")
xxxx/eslint-demo/plugins/src/demo-processors.fox
2:23 error Missing semicolon.(found by Yasin!) semi
✖ 1 problem (1 error, 0 warnings)
1 error and 0 warnings potentially fixable with the `--fix` option.
➜ eslint-demo git:(v0.0.1) ✗
可以看到,我們的報錯信息出來了,並且加上了我們在processor自定義的內容“(found by Yasin!) ”。
rules
自定義rule算是我們插件中最重要也是最複雜的模塊了,這意味着我們需要去操作ast對象了,操作ast對象還是有一定難度的,因爲你需要對ast有比較深入的瞭解,這就需要我們去查看一些parser的源碼了,廢話不多說,我們直接開幹!
開始之前我們得先有一個需求,我們直接copy一份demo-processors.fox然後命名爲demo-rule.js,
demo-rule.js:
document.write("hello plugin");
fox.say("hello world");
我們的需求就是把“hello plugin” 改成“hello yasin”,好啦! 有需求後我們直接開擼。
我們先看一下rule中必須要有的一些信息:
meta
(對象)包含規則的元數據:
-
type(string) 指示規則的類型,值爲"problem"“suggestion"或"layout”
"problem"
指的是該規則識別的代碼要麼會導致錯誤,要麼可能會導致令人困惑的行爲。開發人員應該優先考慮解決這個問題。"suggestion"
意味着規則確定了一些可以用更好的方法來完成的事情,但是如果代碼沒有更改,就不會發生錯誤。"layout"
意味着規則主要關心空格、分號、逗號和括號,以及程序中決定代碼外觀而不是執行方式的所有部分。這些規則適用於AST中沒有指定的代碼部分。
-
docs
(object) 對 ESLint 核心規則來說是必需的:
description
(字符串) 提供規則的簡短描述在規則首頁展示category
(string) 指定規則在規則首頁處於的分類recommended
(boolean) 配置文件中的"extends": "eslint:recommended"
屬性是否啓用該規則url
(string) 指定可以訪問完整文檔的 url。
在自定義的規則或插件中,你可以省略
docs
或包含你需要的任何屬性。 -
**
fixable
重要:**如果沒有fixable
屬性,即使規則實現了fix
功能,ESLint 也不會進行修復。如果規則不是可修復的,就省略fixable
屬性。源碼中有說明
lib/linter/linter.js:
if (problem.fix && rule.meta && !rule.meta.fixable) { throw new Error("Fixable rules should export a `meta.fixable` property."); } lintingProblems.push(problem);
fixable只需要有值就可以了,具體有啥含義我也不是很清楚哈,不過看官方一般都是以下這幾個值“true”、“whitespace” 、“code”。
-
deprecated
(boolean) 表明規則是已被棄用。如果規則尚未被棄用,你可以省略deprecated
屬性。 -
replacedBy
(array) 在不支持規則的情況下,指定替換的規則
create
(function) 返回一個對象,其中包含了 ESLint 在遍歷 JavaScript 代碼的抽象語法樹 AST (ESTree 定義的 AST) 時,用來訪問節點的方法。
因爲要操作ast對象了,我們先看一下我們的demo-rule.js轉換成ast後是什麼樣子的:
{
"type": "Program",
"start": 0,
"end": 31,
"range": [
0,
31
],
"body": [
{
"type": "ExpressionStatement",
"start": 0,
"end": 31,
"range": [
0,
31
],
"expression": {
"type": "CallExpression",
"start": 0,
"end": 30,
"range": [
0,
30
],
"callee": {
"type": "MemberExpression",
"start": 0,
"end": 14,
"range": [
0,
14
],
"object": {
"type": "Identifier",
"start": 0,
"end": 8,
"range": [
0,
8
],
"name": "document"
},
"property": {
"type": "Identifier",
"start": 9,
"end": 14,
"range": [
9,
14
],
"name": "write"
},
"computed": false
},
"arguments": [
{
"type": "Literal",
"start": 15,
"end": 29,
"range": [
15,
29
],
"value": "hello plugin",
"raw": "\"hello plugin\""
}
]
}
}
],
"sourceType": "module"
}
所以我們只需要監聽“Literal”類型的節點,然後修改掉當前節點的value值就可以了,
我們在eslint-demo目錄創建一個rules目錄,然後裏面創建一個plugin2yasin.js文件,
plugin2yasin.js:
module.exports = {
meta: {
type: "layout",
docs: {
description: "使用自定義的字符串或者'yasin'替換'plugin'",
category: "Stylistic Issues",
recommended: false,
url: "https://xxx.com.cn"
},
fixable: "code",
schema: [
{
type: "string",
}
],
messages: {
errorStr: "錯誤的字符串'plugin'"
}
},
create(context){
//獲取規則中配置的第一個參數,默認爲“yasin”
const msg = context.options[0] || "yasin";
function checkLiteral(node) {
let fix, messageId;
//如果當前節點的value裏面包含plugin字符串的話就進行校驗
if (node.value.indexOf("plugin") !== -1) {
//設置messageId爲我們meta中的"errorStr"類型
messageId = "errorStr";
//告訴eslint該怎麼修復代碼
fix = (fixer) => {
return {
range: node.range, //節點在代碼中的的範圍
text: `"${node.value.replace(/(\w)*(plugin)/g, `$1${msg}`)}"` //當前節點的源碼內容
};
};
//報告eslint錯誤信息
context.report({
node,
messageId,
fix,
});
}
}
return {
Literal: checkLiteral //監聽ast的Literal節點
};
}
};
好啦,我們的rule定義完畢了,然後我們在plugin的清單文件中導出這個規則,
plugins/fox-plugin/index.js:
module.exports = {
environments: {
fox: require("./env/fox")
},
processors: {
".fox": require("./processors/fox")
},
rules: {
"plugin2yasin": require("./rules/plugin2yasin")
}
};
然後我們在我們的配置文件中使用一下我們的規則,
plugins/.eslintrc.json:
{
"root": true,
"env": {
"@fox/fox": true
},
"plugins": [
"@fox"
],
"extends": [
"eslint:recommended"
],
"rules": {
"semi": ["error","always"],
"@fox/plugin2yasin": ["error","你好呀"]
}
}
可以看到,我們配置了一個"@fox/plugin2yasin": [“error”,“你好呀”]規則,然後傳遞了一個“你好呀”參數給我們的自定義規則。
我們在根目錄運行我們的eslint:
➜ eslint-demo git:(v0.0.1) ✗ npx eslint ./plugins/src/*
text document.write("hello yasin");
fox.say("hello world");
xxx/eslint-demo/plugins/src/demo-rule.js
1:16 error 錯誤的字符串'plugin' @fox/plugin2yasin
xxx/eslint-demo/plugins/src/demo1.js
1:16 error 錯誤的字符串'plugin' @fox/plugin2yasin
1:31 error Missing semicolon semi
✖ 3 problems (3 errors, 0 warnings)
3 errors and 0 warnings potentially fixable with the `--fix` option.
➜ eslint-demo git:(v0.0.1) ✗
可以看到,有三處地方報錯了,我們一個個看一下對不對
xxx/eslint-demo/plugins/src/demo-rule.js:
document.write("hello plugin"); //1:16 error 錯誤的字符串'plugin' @fox/plugin2yasin
fox.say("hello world");
xxx/eslint-demo/plugins/src/demo1.js:
document.write("hello plugin")
fox.say("hello world");
// 1:16 error 錯誤的字符串'plugin' @fox/plugin2yasin
// 1:31 error Missing semicolon semi
我們可以確定,我們的自定義規則生效了,而且位置都是正確的,
接下來我們測試一個我們自定義rule的fix功能
我們在根目錄運行我們的eslint:
➜ eslint-demo git:(v0.0.1) ✗ npx eslint ./plugins/src/* --fix
text document.write("hello yasin");
fox.say("hello world");
➜ eslint-demo git:(v0.0.1) ✗
可以看到,我們在結尾加了一個–fix選項,然後終端中沒有任何報錯,我們直接打開我們的demo-rule.js文件看看eslint有沒有幫我們修改掉代碼,
plugins/src/demo-rule.js
document.write("hello 你好呀");
fox.say("hello world");
可以看到,eslint已經幫我們把“plugin”修改成了“你好呀”字符串了。
Configs
你可以在一個插件中在 configs
鍵下指定打包的配置。當你想提供不止代碼風格,而且希望提供一些自定義規則來支持它時,會非常有用。每個插件支持多配置。注意不可能爲給定的插件指定默認配置,當用戶想要使用一個插件時,必須在配置文件中指定。
當我們用eslint原生、vue、react、typescript等第三方插件的時候,我們都會用到extends屬性,每個插件都有個“recommended”或者其它的config讓我們繼承,比如eslint-plugin-vue的清單文件,裏面有這些config
xxx/node_modules/eslint-plugin-vue/lib/index.js
{
configs: {
'base': require('./configs/base'),
'essential': require('./configs/essential'),
'no-layout-rules': require('./configs/no-layout-rules'),
'recommended': require('./configs/recommended'),
'strongly-recommended': require('./configs/strongly-recommended')
}
}
然後我們config用的時候就可以這樣了:
{
"plugins":[
"vue"
],
"extends":[
"vue/recommended"
]
}
這樣就可以把vue的recommended給繼承過來了,所以我們也在我們的fox插件中創建一個recommended配置信息。
首先我們在fox-plugin目錄底下創建一個configs目錄,然後在configs目錄下面創建一個recommended.js文件,
recommended.js:
module.exports = {
"env": {
"@fox/fox": true
},
"plugins": [
"@fox"
],
"extends": [
"eslint:recommended"
],
"rules": {
"semi": ["error","always"],
"@fox/plugin2yasin": ["error","你好呀"]
}
};
可以看到我們直接把.eslintrc.json內容直接copy過來了,然後我們在插件的清單文件中導出config,
plugins/fox-plugin/index.js:
module.exports = {
environments: {
fox: require("./env/fox")
},
processors: {
".fox": require("./processors/fox")
},
rules: {
"plugin2yasin": require("./rules/plugin2yasin")
},
configs: {
'recommended': require('./configs/recommended'),
},
};
然後我們修改一下我們的.eslintrc.json文件,讓它直接繼承我們的@fox/recommended配置,
plugins/.eslintrc.json:
{
"root": true,
"plugins": [
"@fox"
],
"extends": [
"plugin:@fox/recommended"
]
}
然後運行我們的eslint:
➜ eslint-demo git:(v0.0.1) ✗ npx eslint ./plugins/src/*
text document.write("hello yasin");
fox.say("hello world");
xxx/eslint-demo/plugins/src/demo-rule.js
1:16 error 錯誤的字符串'plugin' @fox/plugin2yasin
✖ 1 problem (1 error, 0 warnings)
1 error and 0 warnings potentially fixable with the `--fix` option.
➜ eslint-demo git:(v0.0.1) ✗
可以看到,跟我們之前效果都是一樣的,是的! config也就是讓你的eslint的配置文件少寫一寫配置。
ok! eslint內容就已經全部結束了,我們從eslint的入門—>eslint的源碼分析–>自定義plugin,基本上是把eslint的全部內容全部擼了一遍,不得不說,eslint還是挺牛逼的,eslint在日常開發中還是起到了不可限量的作用的,很多人一開始都是很反感這玩意的,但是對於一個團隊來說,良好的代碼規範能夠讓其他人更好的看懂你的代碼的,最後,歡迎志同道合的童鞋一起學習一起交流,有啥問題都可以聯繫我,後面還會持續更新框架系列的文章,敬請期待!!