管中窺eslint【附贈各框架超快eslint&prettier配置腳本】

雖然我是eslint的忠實擁簇者,不過最近在集成prettierr的過程當中,仍是以爲賊麻煩。各類配置只知其一;不知其二,爲啥有些又要加config又要加plugin,有些加了config就不用加plugin了…諸如此類的。因此就專門拿時間看了看eslint的背後流程~html

.eslintrc

eslint固然支持不少種格式的配置文件,我習慣用.eslintrc的json形式。vue

rules

若是一份.eslintrc空空如也,那它固然沒法工做。經過在rules裏配置想要的規則,才能讓eslint知道你須要哪些校驗。 這也說明了,eslint自己維護了一些校驗規則。因此咱們才能方便的以rules名稱的方式選擇開啓其中的部分嘛。node

rules是如何工做的?

從大方向而言,你們都清楚,無非是AST分析,--fix就是替換嘛。不過真的要作AST分析,仍是以爲是一項dirty work…一份js有那麼多節點,而校驗規則又那麼多,怎麼來匹配,又怎麼確保效率呢?不可能每一個校驗規則都掃描一次全部節點吧。react

其實從eslint提供的開發者開發自定義rule的文檔接口中就能夠大體看出一點邏輯。在eslint從外到內,再從內到外的閱讀一份代碼的過程當中,提供了無數個鉤子(包括進入退出文檔、進入退出每種類型的節點…)供開發者註冊。在這些鉤子內,開發者能夠經過eslint傳入的當前node節點入參,以及全局掛載在context上的工具方法們,進行自定義校驗;若校驗失敗,則通知eslint失敗緣由。git

挑兩個最經常使用的rules看看格式就理解了~程序員

例一:no-console

代碼地址:eslint/no-console.js at master · eslint/eslint · GitHubes6

超級簡化版以下:github

module.exports = {
	meta: {
        type: "suggestion",

      
        messages: {
            unexpected: "Unexpected console statement."
        }
    },
	create(context){
		function isConsole(reference) {
        const id = reference.identifier;
        return id && id.name === "console";
		}

		function report(reference) {
            const node = reference.identifier.parent;
            context.report({
                node,
                loc: node.loc,
                messageId: "unexpected"
            });
        }
		return {
				// 鉤子節點
            "Program:exit"() {
            		const scope = context.getScope();
					scope.through.filter(isConsole);

                	const references = scope.through.filter(isConsole);
                references.forEach(report);
				}
        };

		}
	}
	
複製代碼

關鍵就在create函數中return的對象。該對象的每一個key都表明調用檢測函數的鉤子節點。能夠看到,no-console是在Program:exit,也就是閱讀完整份節點文檔時作的校驗;若是發現有identifier爲console的,就經過調用context.report通知到eslint。typescript

例二:no-extra-semi

代碼地址: github.com/eslint/esli…json

超級簡化版以下:

module.exports = {
    meta: {
        fixable: "code",
        messages: {
            unexpected: "Unnecessary semicolon."
        }
    },

    create(context) {
        const sourceCode = context.getSourceCode();

        function report(nodeOrToken) {
            context.report({
                node: nodeOrToken,
                messageId: "unexpected",
                fix(fixer) {
return new FixTracker(fixer, context.getSourceCode())
                        .retainSurroundingTokens(nodeOrToken)
                        .remove(nodeOrToken);
                }
            });
        }

        function checkForPartOfClassBody(firstToken) {
            for (let token = firstToken;
                token.type === "Punctuator" && !astUtils.isClosingBraceToken(token);
                token = sourceCode.getTokenAfter(token)
            ) {
                if (astUtils.isSemicolonToken(token)) {
                    report(token);
                }
            }
        }

        return {
            EmptyStatement(node) {
                const parent = node.parent,
                    allowedParentTypes = [
                        "ForStatement",
                        "ForInStatement",
                        "ForOfStatement",
                        "WhileStatement",
                        "DoWhileStatement",
                        "IfStatement",
                        "LabeledStatement",
                        "WithStatement"
                    ];

                if (allowedParentTypes.indexOf(parent.type) === -1) {
                    report(node);
                }
            },

            ClassBody(node) {
checkForPartOfClassBody(sourceCode.getFirstToken(node, 1)); 
            },

            MethodDefinition(node) {
checkForPartOfClassBody(sourceCode.getTokenAfter(node));
            }
        };
    }
複製代碼

如上,能夠看到檢測semi的時機是在EmptyStatement,ClassBody,MethodDefinition三類節點下。這裏比較有趣的是看似陌生的EmptyStatment。js裏無特殊意義的分號(好比return結尾)的節點類型就是EmptyStatement噢。(這一點我以前從未知道!)

不過看到這裏時,內心未免會嘀咕起來,這麼多node type,鬼知道什麼對應什麼阿。後來我找到了一個好辦法——去 astexplorer 上手動寫寫找下對應;至於總體的AST節點類型有哪些,能夠去 estree/es5.md 瞅瞅。它列舉了最基礎的節點類型,後續更新的es版本都會基於該版本作一些延伸。

這份rule還有一份特別之處——它支持自動fix。在調用context.report()時有傳一個fix函數,以返回經矯正過的節點信息。

extends

說到rules,那你們確定會有本身喜歡的一套rules配置,固然也會想共享一套配置。這就是extends的做用啦。extends不光能夠繼承rules,還能夠繼承.eslintrc裏的全部配置項。extends即表明了」我想用別人這套.eslintrc config的意思「。

eslint-extends包名格式爲eslint-config-xxx,配置時在.eslintrc下的extends中加上xxx便可使用。

Eslint裏一樣也有內置的一套默認推薦配置,也就是常見的eslint:recommended代碼)。因此,最簡單的基礎錯誤校驗配置方式,就是在.eslintrc裏的extends裏配上eslint:recommended就好啦。

plugins

除了eslint裏內置的基礎檢測項,人們確定還會有更多的自定義需求。比方若是我想本身寫一套rules來集成到eslint裏校驗呢?那麼就該plugins出場啦。

plugin的包名格式爲eslint-plugin-xxx,該包的導出對象包含rules字段,其值是rules名稱和對應校驗函數的大對象。

示例eslint-plugin-import/index.js:

exports.rules = {
  'no-unresolved': require('./rules/no-unresolved'),
  'named': require('./rules/named'),
	...
}
複製代碼

使用時,先在.eslintrc裏注入plugin;再在rules指定要使用的plugin裏的rule名稱。

.eslintrc:

{
	"plugins": ["xxx"],
	"rules": {
		"xxx/xxx-rule": "error"
	}
}
複製代碼

不過這時煩惱就出現了,總不能讓用戶再敲一遍rules嘛。若是有些其他的配置也必需要用戶指定怎麼辦。

plugins中集成的配置

因此通常plugin裏也會導出幾套推薦配置,可是這些推薦配置默認是不會生效的噢。須要用戶安裝plugin:xxx/configName的指定名稱去extends該config。由於這些config裏,通常也會配置本身爲plugin,因此用戶免去了須要在eslintrc下特別指定plugin的操做。不過須要注意下plugin裏導出的config有無extend其餘config包,如有,也要記得安裝上。

示例 eslint-plugin-import/index.js:

exports.rules = {	... }
exports.configs = {
	recommended: require('../config/recommended'),
	...
}
複製代碼

使用:

原(需安裝eslint-plugin-import和eslint-config-import):
{
	"plugins": ["import"],
	"extends": ["import"]
}

更方便(只需安裝eslint-plugin-import):
{
	"extends": ["plugin: import/recommended"]
}
複製代碼

parser & parserOptions & Processor

這三個我以前一直都傻傻分不太清。因此就拎到一塊兒總結了。

parser

parser指定解析ast用的解析器。eslint的默認parser是內置的espree。另外還很常見的有:

  • @typescript-eslint/parser
  • vue-eslint-parser
  • babel-eslint

前二者好理解,他們的語法樹確定有些不同,因此須要單獨parse嘛。而babel-eslint也就是babel內置使用的parser,和espree比有什麼不一樣呢?espree不也能指定es版本嗎?

雖然espree也能夠經過parserOptions.ecmaVersion指定es版本,但僅僅侷限於寫進es標準的語法,好比decorator等exprimental試驗版本就不大能支持。babel-eslint能處理的語法更爲全面,畢竟人家是babel呀。另外,只要指定parse: "babel-eslint"就啥配置都不用了,也不用額外寫parserOptions.ecmaVersion,用起來也更爲方便把。

p.s. 使用了typescript或者vue的插件或config後,就注意不要再在本身的.eslintrc內指定parser了,這樣會覆蓋掉它們集成的parser配置拉。

parserOptions

即給parser傳的options。針對espree而言,咱們經常使用的通常有這麼幾項:

  • parserOptions.sourceType: ‘script’/‘module’; 如今基本都是module了。
  • parserOptions.ecmaVersion:espree支持的es版本。
  • parserOptions.ecmaFeatures: { globalReturn’, ‘impliedStrict’, ‘jsx’} 。用jsx相關的框架時,好比React或Vue,jsx都要記得寫上。

值得注意的一點是,使用其餘的parser時,其parserOptions得根據其parser的文檔來,好比babel-eslint的parserOptions

Processor

Processor看起來就很小衆了,它的做用是:

Processors can extract JavaScript code from another kind of files, then lets ESLint lint the JavaScript code. Or processors can convert JavaScript code in preprocessing for some purpose.

也就是提供預處理某種代碼的功能。

有個身邊的例子,eslint-plugin-vue就針對.vue文件使用了本身寫的processor來進行轉換。

globals & env

globals的做用是指定全局變量,避免被誤認爲unused-vars。而env的做用是指定在該環境下特有的一系列全局變量,好比node的require, es6的Promise。不一樣的env並不互斥,因此多指定幾個也不要緊。通常你們經常使用的配置爲:

{
	"env": {
		"browser": true,
    	"node": true,
    	"es6": true,
		"commonjs": true,
	}
}
複製代碼

以上,粗淺的知道了eslint的工做原理,在配置時就不會再一頭霧水了。至少知道config和plugin的原理,eslint是如何讀取到其中的配置值的~

Prettier

Prettier是至關強大的格式化工具。Eslint雖然也能指定結尾加不加分號呀,空幾格呀,但它更偏向於「語法檢查」,只是同時附帶了一些輕微格式化的功效。而對於整個代碼的排版,不少人都是依靠代碼編輯器來的。這樣一我的開發還好,多人開發時,很容易由於互相沒協商好編輯器格式化風格,形成風格很不一致的狀況。再加上你們都很熱愛」format on save」,因此ctrl+s一下,整篇代碼的git blame都變成本身了…

因此用上了prettier以後,我每天都很開心。

雖然有人抱怨說prettier會亂折行,可是隻要按本身的喜好配好了,用起來仍是很順手的。

Prettier的配置很簡單,最基礎的就這麼幾項(不過想一想,程序員格式化界爭論來爭論去不也就這麼幾項麼)。如下是我最喜歡的.prettierrc配置:

{
  "printWidth": 100, // 每行最多字,折行時機的關鍵~
  "singleQuote": true, // 單引號
	"tabWidth": 2, // 幾個空格
  "semi": false, // 結尾分號
  "trailingComma": "es5" // 這個存在感低一點但很實用,表示對象、數組的最後一項會補上逗號
}
複製代碼

但是你們都討厭安裝重複的新東西呀。都用着eslint了,難道還要加個prettier命令麼。因此prettier也提供了集成到eslint裏的工具eslint-plugin-prettier,這就無比方便啦!eslint校驗及fix的時候,也讓prettier把本身的一份檢查順手作了~

p.s. 有一點要注意的是,爲避免和eslint自帶的format檢驗撞車,prettier在本身的基礎配置中,會先把eslint全部自帶的和格式檢查相關的rules給關掉。因此若是既要配eslint: recommended又要配plugin:prettier/recommended的話,記得plugin:prettier/recommended要放到eslint: recommended後面噢。

p.p.s. 要注意編輯器的format on save功能也須要關閉,避免和eslint的format on save撞車。

playground:寫一個本身的eslint插件把~

若是想要制定一套本身公司獨有的Eslint校驗規則,也不妨按着eslint開發者文檔一試噢。

咱們舉個很簡單的例子,不容許代碼裏的string裏出現foo,須要替換成boo。那隻要新建一個名爲eslint-plugin-myself的包,裏頭有一條名爲’foo-to-boo’的規則。

入口eslint-plugin-myself/index:js:

module.exports = {
	rules: {
		'foo-to-boo': require('./foo-to-boo')
	},
	configs: {
		// 把這份config名爲爲all
		all: {
			plugins: ['eslint-plugin-myself'],
			rules: {
				'myself/foo-to-boo': 'error'
			}
		} 
	}
}
複製代碼

foo-to-boo.js

modules.exports = {
	create: function(context) {
		function validate(node){
			const val = node.raw;
			// 校驗規則
			if(val.match && val.match('foo')) {
				 // 報告eslint
               context.report({
                   node,
                   message: 'no foo..',
					  // 返回修復後的節點內容 
                   fix: (fixer) => fixer.replaceTextRange(node.range, val.replace(/foo/, ()=>'bar');
                });
           }
		}
		return {
			// 校驗string 如'hahhfoohh'
			'Literal': validate,
			// 校驗template 如`hahhfoohh`
			'TemplateElement': validate
		}
	}
}
複製代碼

使用:

{
	"extends": ['plugin: myself/all']
}
複製代碼

Bonus: 30s生成eslint全配置~

其實在平常使用中, eslint配置最好要做爲腳手架初始化配置的一部分。可是總面臨着要維護沒有eslint,或是…沒有什麼用的eslint的老項目的狀況。因此我手擼了一個小腳本,方便一次性生成項目的eslint+prettier的全配置,包括.eslintrc、.prettierrc及依賴包。支持nodejs, React及Vue環境,也支持對經常使用extends的極其微小的選擇權:

example.gif

使用也很方便:

npx add-eslint-script
複製代碼

項目地址

默認配置地址以下,也能夠此爲base直接採用~

node項目默認配置

react項目默認配置

vue項目默認配置

碎碎念:關於eslint開發者

經過此次對eslint的詳細背調,我才瞭解到其做者Nicholas C. Zakas從年輕時開始,就常年抱恙了。他患有嚴重的萊姆病,是一種發做緣由和治療方案都不大明確的神經性疾病。如今他已沒法正常工做,亦不能正常生活了。從 I have lyme disease 能夠讀到他的自述和每一年的健康情況發展。

看完他的故過後,我不由陷入了沉思…阿,生活不易。在痛其體膚的狀況下,他依然能堅持作完這麼細緻,甚至能夠說是細枝末節的工做;寫parser,分析一棵棵AST樹…也很使人欽佩了。

Bless Eslint!!

相關文章
相關標籤/搜索