雖然我是eslint的忠實擁簇者,不過最近在集成prettierr的過程當中,仍是以爲賊麻煩。各類配置只知其一;不知其二,爲啥有些又要加config又要加plugin,有些加了config就不用加plugin了…諸如此類的。因此就專門拿時間看了看eslint的背後流程~html
eslint固然支持不少種格式的配置文件,我習慣用.eslintrc的json形式。vue
若是一份.eslintrc空空如也,那它固然沒法工做。經過在rules裏配置想要的規則,才能讓eslint知道你須要哪些校驗。 這也說明了,eslint自己維護了一些校驗規則。因此咱們才能方便的以rules名稱的方式選擇開啓其中的部分嘛。node
從大方向而言,你們都清楚,無非是AST分析,--fix就是替換嘛。不過真的要作AST分析,仍是以爲是一項dirty work…一份js有那麼多節點,而校驗規則又那麼多,怎麼來匹配,又怎麼確保效率呢?不可能每一個校驗規則都掃描一次全部節點吧。react
其實從eslint提供的開發者開發自定義rule的文檔接口中就能夠大體看出一點邏輯。在eslint從外到內,再從內到外的閱讀一份代碼的過程當中,提供了無數個鉤子(包括進入退出文檔、進入退出每種類型的節點…)供開發者註冊。在這些鉤子內,開發者能夠經過eslint傳入的當前node節點入參,以及全局掛載在context上的工具方法們,進行自定義校驗;若校驗失敗,則通知eslint失敗緣由。git
挑兩個最經常使用的rules看看格式就理解了~程序員
代碼地址: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
代碼地址: 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函數,以返回經矯正過的節點信息。
說到rules,那你們確定會有本身喜歡的一套rules配置,固然也會想共享一套配置。這就是extends的做用啦。extends不光能夠繼承rules,還能夠繼承.eslintrc裏的全部配置項。extends即表明了」我想用別人這套.eslintrc config的意思「。
eslint-extends包名格式爲eslint-config-xxx
,配置時在.eslintrc下的extends中加上xxx
便可使用。
Eslint裏一樣也有內置的一套默認推薦配置,也就是常見的eslint:recommended
(代碼)。因此,最簡單的基礎錯誤校驗配置方式,就是在.eslintrc裏的extends裏配上eslint:recommended
就好啦。
除了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嘛。若是有些其他的配置也必需要用戶指定怎麼辦。
因此通常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指定解析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配置拉。
即給parser傳的options。針對espree而言,咱們經常使用的通常有這麼幾項:
值得注意的一點是,使用其餘的parser時,其parserOptions得根據其parser的文檔來,好比babel-eslint的parserOptions。
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的做用是指定全局變量,避免被誤認爲unused-vars。而env的做用是指定在該環境下特有的一系列全局變量,好比node的require, es6的Promise。不一樣的env並不互斥,因此多指定幾個也不要緊。通常你們經常使用的配置爲:
{
"env": {
"browser": true,
"node": true,
"es6": true,
"commonjs": true,
}
}
複製代碼
以上,粗淺的知道了eslint的工做原理,在配置時就不會再一頭霧水了。至少知道config和plugin的原理,eslint是如何讀取到其中的配置值的~
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
撞車。
若是想要制定一套本身公司獨有的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']
}
複製代碼
其實在平常使用中, eslint配置最好要做爲腳手架初始化配置的一部分。可是總面臨着要維護沒有eslint,或是…沒有什麼用的eslint的老項目的狀況。因此我手擼了一個小腳本,方便一次性生成項目的eslint+prettier的全配置,包括.eslintrc、.prettierrc及依賴包。支持nodejs, React及Vue環境,也支持對經常使用extends的極其微小的選擇權:
使用也很方便:
npx add-eslint-script
複製代碼
默認配置地址以下,也能夠此爲base直接採用~
經過此次對eslint的詳細背調,我才瞭解到其做者Nicholas C. Zakas從年輕時開始,就常年抱恙了。他患有嚴重的萊姆病,是一種發做緣由和治療方案都不大明確的神經性疾病。如今他已沒法正常工做,亦不能正常生活了。從 I have lyme disease 能夠讀到他的自述和每一年的健康情況發展。
看完他的故過後,我不由陷入了沉思…阿,生活不易。在痛其體膚的狀況下,他依然能堅持作完這麼細緻,甚至能夠說是細枝末節的工做;寫parser,分析一棵棵AST樹…也很使人欽佩了。
Bless Eslint!!