Eslint從入門到實(shí)戰(zhàn)

開始

前面已經(jīng)寫了兩篇關(guān)于eslint的文章了,想必都對(duì)eslint應(yīng)該有一個(gè)簡(jiǎn)單的認(rèn)識(shí)了,在平常的項(xiàng)目中使用應(yīng)該是問(wèn)題不大,面試應(yīng)該也是問(wèn)題不大的,大家有興趣可以看看前面兩篇文章:

接下來(lái)我們更深入的了解一下eslint,我們直接結(jié)合demo創(chuàng)建一個(gè)我們自己的plugin。

我們還是用前面我們的eslint-demo項(xiàng)目,代碼已經(jīng)上傳github了大家可以直接clone一份,我們?cè)趀slint-demo項(xiàng)目根目錄直接創(chuàng)建一個(gè)plugins目錄,然后創(chuàng)建一個(gè).eslintrc.json文件,

.eslintrc.json:

{
  "root": true
}

為了不跟根目錄的配置文件沖突,我們直接加上了“root”:true的標(biāo)識(shí)。

env

env前面文章有介紹過(guò),我就不在這里介紹了,比如我們有一個(gè)叫“fox”的運(yùn)行環(huán)境,然后有一個(gè)“fox”的全局變量,還記得我們之前demo的做法嗎?我們直接在config的globals變量中定義了一個(gè)叫fox的變量,這樣我們的eslint就能識(shí)別fox變量了,

{
  ...
   "globals": {
    "fox": "readonly"
  },
  ...
}

這一次我們通過(guò)自定義env把它當(dāng)成一個(gè)fox環(huán)境,讓它變成下面這樣的效果:

{
  "env":{
    "@fox/fox":true
  }
}

首先我們?cè)趐lugins目錄創(chuàng)建一個(gè)fox-plugin目錄,然后在fox-plugin目錄執(zhí)行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"
}

可以看到,我們創(chuàng)建了一個(gè)帶@fox命名空間的插件,然后我們?cè)趂ox-plugin目錄創(chuàng)建一個(gè)入口文件index.js,

index.js:

module.exports = {
    environments: {
        fox: require("./env/fox")
    }
};

可以看到,我們直接導(dǎo)出了一個(gè)對(duì)象,然后里面包含environments字段,environments對(duì)象中又包含一個(gè)fox屬性,fox屬性就是我們的自定義fox運(yùn)行環(huán)境,我們?cè)谀夸浀紫聞?chuàng)建一個(gè)env目錄,然后在env目錄創(chuàng)建一個(gè)fox.js文件,

plugins/fox-plugin/env/fox.js:

const eslintEnv = require("eslint/conf/environments"); //獲取eslint的所有環(huán)境變量
module.exports = {
    globals: {
        ...eslintEnv.get("es2020").globals, //合并eslint的es2020變量
        ...eslintEnv.get("browser").globals, //合并eslint的browser變量
        ...{ //合并自定義的環(huán)境變量
            fox: false,
        }
    },
    parserOptions: {
        ecmaVersion: 11 //設(shè)置語(yǔ)法為es2020
    }
};

可以看到,我們?cè)谏厦娲a中合并了eslint中的原始es2020跟browser環(huán)境變量,然后還自定義了一個(gè)全局fox變量,并且設(shè)置es的語(yǔ)法版本為11,也就是說(shuō)我們的fox環(huán)境=(es2020+browser+自定義fox)。

ok,我們已經(jīng)定義完plugin了,然后也定義好了我們的env,那么項(xiàng)目中該怎么使用呢?

首先我們直接在我們根目錄執(zhí)行npm install把我們的插件引入進(jìn)來(lái):

npm install -D xxx/eslint-demo/plugins/fox-plugin //注意xxx是你本地的絕對(duì)路徑

執(zhí)行完畢后我們會(huì)在根目錄的node-modules目錄下面找到我們的插件:

在這里插入圖片描述

然后在配置文件中引入我們的插件和環(huán)境

plugins/.eslintrc.json:

{
  "root": true,
  "env": {
    "@fox/fox": true
  },
  "plugins": [
    "@fox"
  ],
  "extends": [
    "eslint:recommended"
  ]
}

同時(shí)為了看效果,我們加入了eslint的recommended規(guī)則。

接下來(lái)我們創(chuàng)建一個(gè)測(cè)試文件來(lái)測(cè)試一下,我們?cè)趐lugins的src目錄中創(chuàng)建一個(gè)demo1.js文件,

demo1.js:

document.write("hello plugin");
fox.say("hello world");

然后我們?cè)诟夸泩?zhí)行一下eslint命令:

?  eslint-demo git:(v0.0.1) ? npx eslint ./plugins/src/*                                                      
?  eslint-demo git:(v0.0.1) ? 

可以看到,沒(méi)有任何報(bào)錯(cuò),那我們把我們的fox環(huán)境去掉試試,

plugins/.eslintrc.json:

{
  "root": true,
  "env": {
  },
  "plugins": [
    "@fox"
  ],
  "extends": [
    "eslint:recommended"
  ]
}

再次運(yùn)行:

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直接報(bào)錯(cuò)了,說(shuō)“document”跟“fox”變量找不到。

processors

前面有介紹過(guò)processors,關(guān)于eslint的基礎(chǔ)配置跟源碼部分我們這一節(jié)不說(shuō)明了,小伙伴可以去看前面的兩篇文章,

比如我們現(xiàn)在有一個(gè).fox后綴的文件,然后我們通過(guò)自定義processors來(lái)預(yù)解析文件,我們直接copy一個(gè)demo1.js文件取名字叫demo-processors.fox,

plugins/src/demo-processors.fox:

document.write("hello plugin");
fox.say("hello world");

首先我們?cè)趂ox-plugin目錄創(chuàng)建一個(gè)processors目錄,然后在processors目錄中創(chuàng)建一個(gè)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)
};

我們直接導(dǎo)入了一個(gè)對(duì)象,然后里面包含了preprocess跟postprocess方法,在preprocess方法中,我直接字符串替換掉了“plugin”為“yasin”,然后返回一個(gè)數(shù)組,數(shù)組里面包含的是需要eslint校驗(yàn)的代碼塊,在postprocess方法中我們對(duì)eslint處理過(guò)后的message做一次封裝,把所有的message都帶有一個(gè)“(found by Yasin!)”的說(shuō)明。

接下來(lái)我們?cè)诓寮那鍐挝募袑?dǎo)出這個(gè)processor,

plugins/fox-plugin/index.js:

module.exports = {
    environments: {
        fox: require("./env/fox")
    },
    processors: {
        ".fox": require("./processors/fox")
    }
};

可以看到,我們聲明了一個(gè)“processors”字段,然后在processors中聲明了一個(gè)".fox"字段指向我們自定義的processor,

注意:“.fox”是你要作用的文件的后綴,也就是說(shuō)我們的processor只會(huì)作用我們的“.fox”后綴文件。

在插件中導(dǎo)出processors后是不需要在配置文件中申明的,eslint會(huì)根據(jù)插件自動(dòng)找到".fox"的processor。

plugins/.eslintrc.json:

{
  "root": true,
  "env": {
    "@fox/fox": true
  },
  "plugins": [
    "@fox"
  ],
  "extends": [
    "eslint:recommended"
  ],
  "rules": {
    "semi": ["error","always"]
  }
}

在根目錄運(yùn)行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文件中的內(nèi)容,并且替換了字符串“eslint”為“yasin”,然后沒(méi)有報(bào)錯(cuò),我們?cè)囍薷南麓a讓它報(bào)錯(cuò),我們直接去掉一個(gè)分號(hào)“;”,

plugins/src/demo-processors.fox:

document.write("hello plugin");
fox.say("hello world")

再次運(yùn)行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) ? 

可以看到,我們的報(bào)錯(cuò)信息出來(lái)了,并且加上了我們?cè)趐rocessor自定義的內(nèi)容“(found by Yasin!) ”。

rules

自定義rule算是我們插件中最重要也是最復(fù)雜的模塊了,這意味著我們需要去操作ast對(duì)象了,操作ast對(duì)象還是有一定難度的,因?yàn)槟阈枰獙?duì)ast有比較深入的了解,這就需要我們?nèi)ゲ榭匆恍﹑arser的源碼了,廢話不多說(shuō),我們直接開干!

開始之前我們得先有一個(gè)需求,我們直接copy一份demo-processors.fox然后命名為demo-rule.js,

demo-rule.js:

document.write("hello plugin");
fox.say("hello world");

我們的需求就是把“hello plugin” 改成“hello yasin”,好啦! 有需求后我們直接開擼。

我們先看一下rule中必須要有的一些信息:

meta(對(duì)象)包含規(guī)則的元數(shù)據(jù):

  • type(string) 指示規(guī)則的類型,值為"problem""suggestion"或"layout"

    • "problem" 指的是該規(guī)則識(shí)別的代碼要么會(huì)導(dǎo)致錯(cuò)誤,要么可能會(huì)導(dǎo)致令人困惑的行為。開發(fā)人員應(yīng)該優(yōu)先考慮解決這個(gè)問(wèn)題。
    • "suggestion" 意味著規(guī)則確定了一些可以用更好的方法來(lái)完成的事情,但是如果代碼沒(méi)有更改,就不會(huì)發(fā)生錯(cuò)誤。
    • "layout" 意味著規(guī)則主要關(guān)心空格、分號(hào)、逗號(hào)和括號(hào),以及程序中決定代碼外觀而不是執(zhí)行方式的所有部分。這些規(guī)則適用于AST中沒(méi)有指定的代碼部分。
  • docs

    (object) 對(duì) ESLint 核心規(guī)則來(lái)說(shuō)是必需的:

    • description (字符串) 提供規(guī)則的簡(jiǎn)短描述在規(guī)則首頁(yè)展示
    • category (string) 指定規(guī)則在規(guī)則首頁(yè)處于的分類
    • recommended (boolean) 配置文件中的 "extends": "eslint:recommended"屬性是否啟用該規(guī)則
    • url (string) 指定可以訪問(wèn)完整文檔的 url。

    在自定義的規(guī)則或插件中,你可以省略 docs 或包含你需要的任何屬性。

  • fixable重要:如果沒(méi)有 fixable 屬性,即使規(guī)則實(shí)現(xiàn)了 fix 功能,ESLint 也不會(huì)進(jìn)行修復(fù)。如果規(guī)則不是可修復(fù)的,就省略 fixable 屬性。

    源碼中有說(shuō)明

    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只需要有值就可以了,具體有啥含義我也不是很清楚哈,不過(guò)看官方一般都是以下這幾個(gè)值“true”、"whitespace" 、"code"。

  • schema (array) 指定該選項(xiàng) 這樣的 ESLint 可以避免無(wú)效的規(guī)則配置

  • deprecated (boolean) 表明規(guī)則是已被棄用。如果規(guī)則尚未被棄用,你可以省略 deprecated 屬性。

  • replacedBy (array) 在不支持規(guī)則的情況下,指定替換的規(guī)則

create (function) 返回一個(gè)對(duì)象,其中包含了 ESLint 在遍歷 JavaScript 代碼的抽象語(yǔ)法樹 AST (ESTree 定義的 AST) 時(shí),用來(lái)訪問(wèn)節(jié)點(diǎn)的方法。

因?yàn)橐僮鱝st對(duì)象了,我們先看一下我們的demo-rule.js轉(zhuǎn)換成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"
}

所以我們只需要監(jiān)聽(tīng)“Literal”類型的節(jié)點(diǎn),然后修改掉當(dāng)前節(jié)點(diǎn)的value值就可以了,

我們?cè)趀slint-demo目錄創(chuàng)建一個(gè)rules目錄,然后里面創(chuàng)建一個(gè)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: "錯(cuò)誤的字符串'plugin'"
        }
    },
    create(context){
      //獲取規(guī)則中配置的第一個(gè)參數(shù),默認(rèn)為“yasin”
        const msg = context.options[0] || "yasin";

        function checkLiteral(node) {
            let fix, messageId;
            //如果當(dāng)前節(jié)點(diǎn)的value里面包含plugin字符串的話就進(jìn)行校驗(yàn)
            if (node.value.indexOf("plugin") !== -1) {
                //設(shè)置messageId為我們meta中的"errorStr"類型
                messageId = "errorStr";
                //告訴eslint該怎么修復(fù)代碼
                fix = (fixer) => {
                    return {
                        range: node.range, //節(jié)點(diǎn)在代碼中的的范圍
                        text: `"${node.value.replace(/(\w)*(plugin)/g, `$1${msg}`)}"` //當(dāng)前節(jié)點(diǎn)的源碼內(nèi)容
                    };
                };
                //報(bào)告eslint錯(cuò)誤信息
                context.report({
                    node,
                    messageId,
                    fix,
                });
            }
        }

        return {
            Literal: checkLiteral //監(jiān)聽(tīng)ast的Literal節(jié)點(diǎn)
        };
    }
};

好啦,我們的rule定義完畢了,然后我們?cè)趐lugin的清單文件中導(dǎo)出這個(gè)規(guī)則,

plugins/fox-plugin/index.js:

module.exports = {
    environments: {
        fox: require("./env/fox")
    },
    processors: {
        ".fox": require("./processors/fox")
    },
    rules: {
        "plugin2yasin": require("./rules/plugin2yasin")
    }
};

然后我們?cè)谖覀兊呐渲梦募惺褂靡幌挛覀兊囊?guī)則,

plugins/.eslintrc.json:

{
  "root": true,
  "env": {
    "@fox/fox": true
  },
  "plugins": [
    "@fox"
  ],
  "extends": [
    "eslint:recommended"
  ],
  "rules": {
    "semi": ["error","always"],
    "@fox/plugin2yasin": ["error","你好呀"]
  }
}

可以看到,我們配置了一個(gè)"@fox/plugin2yasin": ["error","你好呀"]規(guī)則,然后傳遞了一個(gè)“你好呀”參數(shù)給我們的自定義規(guī)則。

我們?cè)诟夸涍\(yùn)行我們的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  錯(cuò)誤的字符串'plugin'  @fox/plugin2yasin

xxx/eslint-demo/plugins/src/demo1.js
  1:16  error  錯(cuò)誤的字符串'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) ? 

可以看到,有三處地方報(bào)錯(cuò)了,我們一個(gè)個(gè)看一下對(duì)不對(duì)

xxx/eslint-demo/plugins/src/demo-rule.js:

document.write("hello plugin"); //1:16  error  錯(cuò)誤的字符串'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  錯(cuò)誤的字符串'plugin'     @fox/plugin2yasin
// 1:31  error  Missing semicolon  semi

我們可以確定,我們的自定義規(guī)則生效了,而且位置都是正確的,

接下來(lái)我們測(cè)試一個(gè)我們自定義rule的fix功能

我們?cè)诟夸涍\(yùn)行我們的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) ? 

可以看到,我們?cè)诮Y(jié)尾加了一個(gè)--fix選項(xiàng),然后終端中沒(méi)有任何報(bào)錯(cuò),我們直接打開我們的demo-rule.js文件看看eslint有沒(méi)有幫我們修改掉代碼,

plugins/src/demo-rule.js

document.write("hello 你好呀");
fox.say("hello world");

可以看到,eslint已經(jīng)幫我們把“plugin”修改成了“你好呀”字符串了。

Configs

你可以在一個(gè)插件中在 configs 鍵下指定打包的配置。當(dāng)你想提供不止代碼風(fēng)格,而且希望提供一些自定義規(guī)則來(lái)支持它時(shí),會(huì)非常有用。每個(gè)插件支持多配置。注意不可能為給定的插件指定默認(rèn)配置,當(dāng)用戶想要使用一個(gè)插件時(shí),必須在配置文件中指定。

當(dāng)我們用eslint原生、vue、react、typescript等第三方插件的時(shí)候,我們都會(huì)用到extends屬性,每個(gè)插件都有個(gè)“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用的時(shí)候就可以這樣了:

{
  "plugins":[
    "vue"
  ],
  "extends":[
    "vue/recommended"
  ] 
}

這樣就可以把vue的recommended給繼承過(guò)來(lái)了,所以我們也在我們的fox插件中創(chuàng)建一個(gè)recommended配置信息。

首先我們?cè)趂ox-plugin目錄底下創(chuàng)建一個(gè)configs目錄,然后在configs目錄下面創(chuàng)建一個(gè)recommended.js文件,

recommended.js:

module.exports = {
    "env": {
        "@fox/fox": true
    },
    "plugins": [
        "@fox"
    ],
    "extends": [
        "eslint:recommended"
    ],
    "rules": {
        "semi": ["error","always"],
        "@fox/plugin2yasin": ["error","你好呀"]
    }
};

可以看到我們直接把.eslintrc.json內(nèi)容直接copy過(guò)來(lái)了,然后我們?cè)诓寮那鍐挝募袑?dǎo)出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"
  ]
}

然后運(yùn)行我們的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  錯(cuò)誤的字符串'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內(nèi)容就已經(jīng)全部結(jié)束了,我們從eslint的入門--->eslint的源碼分析-->自定義plugin,基本上是把eslint的全部?jī)?nèi)容全部擼了一遍,不得不說(shuō),eslint還是挺牛逼的,eslint在日常開發(fā)中還是起到了不可限量的作用的,很多人一開始都是很反感這玩意的,但是對(duì)于一個(gè)團(tuán)隊(duì)來(lái)說(shuō),良好的代碼規(guī)范能夠讓其他人更好的看懂你的代碼的,最后,歡迎志同道合的童鞋一起學(xué)習(xí)一起交流,有啥問(wèn)題都可以聯(lián)系我,后面還會(huì)持續(xù)更新框架系列的文章,敬請(qǐng)期待?。?/p>

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容