自定義 ESLint 規(guī)則,讓代碼持續(xù)美麗
背景
“一段真實的代碼發(fā)展歷史
很久很久以前,有一個需求,然后產(chǎn)出了一段代碼,代碼優(yōu)雅而簡潔
export?const?getConfig?=?(param1,?param2)?=>?{
??return?...
};
不久又來了個需求,加個參數(shù)擴展,so easy!
export?const?getConfig?=?(param1,?param2,?param3)?=>?{
??return?...
};
經(jīng)過多次產(chǎn)品需求迭代后,現(xiàn)在的代碼
export?const?getConfig?=?(param1,?param2,?param3,?param4,?param5,?param6,?param7……)?=>?{
??return?...
};
在產(chǎn)品迭代過程中,上面的 case 一個函數(shù)的參數(shù)從 2 個發(fā)展到了 7 個,優(yōu)雅的代碼逐漸變?yōu)椴豢删S護。這是什么問題?這歸咎于日益增長的需求,快速響應(yīng)和代碼質(zhì)量之間的矛盾。
那如何避免呢?
制定代碼規(guī)范
靠開發(fā)同學(xué)的自我修養(yǎng)
進行 Code Review
工具提示
發(fā)版控制,不允許發(fā)版
制定代碼規(guī)范肯定是需要的,那如何約束代碼呢?規(guī)范文檔宣講,再憑借開發(fā)同學(xué)的自我修養(yǎng)?答案是:無法保證。
Code Review ?但難免也有落網(wǎng)之魚。發(fā)版控制?能有效解決但是開發(fā)體驗不好。
如果我們在開發(fā)者寫代碼的時候就及時給到提示和建議,那開發(fā)體驗就很棒了,而 ESLint 的自定義規(guī)則就可以實現(xiàn)在開發(fā)過程中給開發(fā)同學(xué)友好的提示。
ESLint 原理
ESLint 是一個代碼檢查工具,通過靜態(tài)的分析,尋找有問題的模式或者代碼。默認使用 Espree (https://github.com/eslint/espree) 解析器將代碼解析為 AST 抽象語法樹,然后再對代碼進行檢查。
看下最簡單的一段代碼使用 espree 解析器轉(zhuǎn)換成的抽象語法樹結(jié)構(gòu),此處可以使用 astexplorer (https://astexplorer.net/) 快速方便查看解析成 AST 的結(jié)構(gòu):
代碼片段:
var?a?=?1;
轉(zhuǎn)換出的結(jié)果:
{
??"type":?"Program",
??"start":?0,
??"end":?10,
??"range":?[
????0,
????10
??],
??"body":?[
????{
??????"type":?"VariableDeclaration",
??????"start":?0,
??????"end":?10,
??????"range":?[
????????0,
????????10
??????],
??????"declarations":?[
????????{
??????????"type":?"VariableDeclarator",
??????????"start":?4,
??????????"end":?9,
??????????"range":?[
????????????4,
????????????9
??????????],
??????????"id":?{
????????????"type":?"Identifier",
????????????"start":?4,
????????????"end":?5,
????????????"range":?[
??????????????4,
??????????????5
????????????],
????????????"name":?"a"
??????????},
??????????"init":?{
????????????"type":?"Literal",
????????????"start":?8,
????????????"end":?9,
????????????"range":?[
??????????????8,
??????????????9
????????????],
????????????"value":?1,
????????????"raw":?"1"
??????????}
????????}
??????],
??????"kind":?"var"
????}
??],
??"sourceType":?"module"
}
代碼轉(zhuǎn)換為 AST 后,可以很方便的對代碼的每個節(jié)點對代碼進行檢查。
自定義 ESLint 規(guī)則開發(fā)
怎么自定義
語法樹分析
對目標代碼進行語法樹解析,可使用 astexplorer (https://astexplorer.net/)

編寫規(guī)則
下面是一個規(guī)則簡單的結(jié)構(gòu)(官方 API 文檔說明:https://eslint.org/docs/developer-guide/working-with-rules#rule-basics)
module.exports?=?{
??meta:?{
????docs:?{
??????description:?"最多參數(shù)允許參數(shù)",
????},
??},
??create:?function?(context)?{
????return?{
??????FunctionDeclaration:?(node)?=>?{
????????if?(node.params.length?>?3)?{
??????????context.report({
????????????node,
????????????message:?"參數(shù)最多不能超過3個",
??????????});
????????}
??????},
????};
??},
};
meta(對象)包含規(guī)則的元數(shù)據(jù) create ( function ) 返回一個對象,其中包含了 ESLint 在遍歷 JavaScript 代碼的抽象語法樹 AST ( ESTree 定義的 AST ) 時,用來訪問節(jié)點的方法
context.report ( ) ?用來發(fā)布警告或錯誤,并能提供自動修復(fù)功能(取決于你所使用的配置)
最簡單的示例(只使用 node 和 message 參數(shù)):
context.report({
????node,
????message:?"參數(shù)最多不能超過3個",
});
node 節(jié)點的提示,如果需要更精確的錯誤或警告提示,我們可以使用 loc 參數(shù),API 文檔說明 (https://eslint.org/docs/developer-guide/working-with-rules#context-report)。
如何使用自定義規(guī)則
使用自定義的 ESLint 規(guī)則,你需要自定義一個 ESLint 的插件,然后將規(guī)則寫到自定義的 ESLint 插件中,然后在業(yè)務(wù)代碼中添加 ESLint 配置,引入 ESLint 插件。
ESLint 插件
創(chuàng)建
創(chuàng)建一個 ESLint plugin,并創(chuàng)建 一個 ESLint rule。
基于 Yeoman generator (https://yeoman.io/authoring/) ,可以快速創(chuàng)建 ESLint plugin 項目。
npm?i?-g?yo
npm?i?-g?generator-eslint
//?創(chuàng)建一個plugin
yo?eslint:plugin
//?創(chuàng)建一個規(guī)則
yo?eslint:rule
創(chuàng)建好的項目目錄結(jié)構(gòu):
rules文件夾存放的是各個規(guī)則文件tests文件夾存放單元測試文件package.json 是你的 ESLint插件 npm 包的說明文件,其中的name屬性就是你的ESLint?插件的名稱,命名規(guī)則:帶前綴eslint-plugin-

示例代碼:
lib/rules/max-params.js
module.exports?=?{
??meta:?{
????docs:?{
??????description:?"最多參數(shù)",
????},
??},
??create:?function?(context)?{
????/**
?????*?獲取函數(shù)的參數(shù)的開始、結(jié)束位置
?????*?@param?{node}?node?AST?Node?
?????*/
????function?getFunctionParamsLoc(node)?{
??????const?paramsLength?=?node.params.length;
??????return?{
????????start:?node.params[0].loc.start,
????????end:?node.params[paramsLength?-?1].loc.end,
??????};
????}
????return?{
??????FunctionDeclaration:?(node)?=>?{
????????if?(node.params.length?>?3)?{
??????????context.report({
????????????loc:?getFunctionParamsLoc(node),
????????????node,
????????????message:?"參數(shù)最多不能超過3個",
??????????});
????????}
??????},
????};
??},
};
補充測試用例
/tests/lib/rules/max-params.js
var?ruleTester?=?new?RuleTester();
ruleTester.run("max-params",?rule,?{
??valid:?["function?test(d,?e,?f)?{}"],
??invalid:?[
????{
????????code:?"function?test(a,?b,?c,?d)?{}",
????????errors:?[{
????????????message:?"參數(shù)最多不能超過3個",
????????}]
????},
??],
});
ESLint 插件安裝
在需要的業(yè)務(wù)代碼中安裝你的 ESLint 插件。(eslint-plugin-my-eslist-plugin 是你的 ESLint 插件 npm 包的包名)
npm?install?eslint-plugin-my-eslist-plugin?
如果你的 npm 包還未發(fā)布,需要進行本地調(diào)試:
可使用 npm link 本地調(diào)試,npm link 的使用 (https://www.baidu.com/s?ie=UTF-8&wd=npm%20link)。
配置
添加你的 plugin 包名(eslint-plugin- 前綴可忽略) 到 .eslintrc 配置文件的 plugins 字段。
.eslintrc 配置文件示例:
{
????"plugins":?[
????????"zoo"?//?你的?ESLint?plugin?的名字
????]
}
rules 中再將 plugin 中的規(guī)則導(dǎo)入。?? ESlint更新后,需要重啟 vsCode,才能生效。( vsCode ?重啟快捷方式:CTRL +SHITF + P,輸入 Reload Window )
此處涉及 ESLint 的規(guī)則設(shè)置(參考說明:https://eslint.org/docs/user-guide/configuring#configuring-rules)
{
????"rules":?{
????????"zoo/rule-name":?2
????}
}
效果

實際應(yīng)用案例
函數(shù)、方法的入?yún)€數(shù)控制,其實已經(jīng)在 ESLint 的規(guī)則中了。在業(yè)務(wù)場景中,我們需要對我們的業(yè)務(wù)規(guī)則編寫自定義的 ESLint 規(guī)則。
一個簡單的業(yè)務(wù)場景:業(yè)務(wù)中通常會出現(xiàn)跳轉(zhuǎn)到很多不同的業(yè)務(wù)域名的操作,不同的環(huán)境有不同的域名,我們需要從配置中取出域名使用,而不是采取硬編碼域名的方案。
由此我們產(chǎn)生出了一個規(guī)則:禁止硬編碼業(yè)務(wù)域名。
規(guī)則為:
module.exports?=?{
??meta:?{
????type:?"suggestion",
????docs:?{
??????description:?"不允許硬編碼業(yè)務(wù)域名",
????},
????fixable:?"code",
??},
??create:?function?(context)?{
????const?sourceCode?=?context.getSourceCode();
????function?checkDomain(node)?{
??????//?匹配硬編碼的業(yè)務(wù)域名的正則
??????const?Reg?=?/^(http:\/\/|https:\/\/|\/\/)(.*.){0,1}zcygov(.com|cn)(.*)/;
??????const?content?=
????????(node.type?===?"Literal"?&&?node.value)?||
????????(node.type?===?"TemplateLiteral"?&&?node.quasis[0].value.cooked);
??????const?domainNode?=
????????(node.type?===?"Literal"?&&?node)?||
????????(node.type?===?"TemplateLiteral"?&&?node.quasis[0]);
??????if?(Reg.test(content))?{
????????context.report({
??????????node,
??????????//?錯誤/警告提示信息
??????????message:?"不允許硬編碼業(yè)務(wù)域名",
??????????//?修復(fù)
??????????fix(fixer)?{
????????????
????????????const?fixes?=?[];
????????????
????????????let?domainKey?=?content.match(Reg)[2];
????????????domainKey?=?domainKey
????????????????domainKey.substr(0,?domainKey.length?-?1)
??????????????:?"";
????????????if?(node.type?===?"Literal")?{
??????????????fixes.push(
????????????????fixer.replaceTextRange(
??????????????????[domainNode.start?+?1,?domainNode.end?-?1],
??????????????????content.replace(Reg,?`$4`)
????????????????)
??????????????);
????????????}
????????????if?(node.type?===?"TemplateLiteral")?{
??????????????fixes.push(
????????????????fixer.replaceTextRange(
??????????????????[domainNode.start,?domainNode.end],
??????????????????content.replace(Reg,?`$4`)
????????????????)
??????????????);
????????????}
?????????????
????????????if?(
??????????????node.type?===?"Literal"?&&
??????????????node.parent.type?===?"JSXAttribute"
????????????)?{
??????????????fixes.push(fixer.insertTextBefore(node,?"{"));
??????????????fixes.push(fixer.insertTextAfter(node,?"}"));
????????????}
????????????fixes.push(
??????????????fixer.insertTextBefore(
????????????????node,
????????????????`window.getDomain('${domainKey}')?+?`
??????????????)
????????????);
????????????return?fixes;
??????????},
????????});
??????}
????}
????return?{
??????//?文本
??????Literal:?checkDomain,
??????//?模板字符串
??????TemplateLiteral:?checkDomain,
????};
??},
};
補充測試用例
/tests/lib/rules/no-zcy-domain.js
var?rule?=?require("../../../lib/rules/no-zcy-domain"),
????RuleTester?=?require("eslint").RuleTester;
var?ruleTester?=?new?RuleTester();
ruleTester.run("no-zcy-domain",?rule,?{
??valid:?[
????"bar",
????"baz",
????`
??var?s?=?{
????x:?"zcygov"
??};
??`,
??],
??invalid:?[
????{
??????code:?`
??????????????var?s?=?"http://zcygov.cn"
????????????`,
??????errors:?[
????????{
??????????message:?"不允許硬編碼業(yè)務(wù)域名",
????????},
??????],
????},
????{
??????code:?`
????????????var?s?=?{
??????????????x:?"http://bidding.zcygov.cn"
????????????};
????????????`,
??????errors:?[
????????{
??????????message:?"不允許硬編碼業(yè)務(wù)域名",
????????},
??????],
????},
??],
});

更多的應(yīng)用場景
除了上面說的硬編碼的場景,還可以將沉淀出的最佳實踐和業(yè)務(wù)規(guī)范通過自定義 ESLint 的方式來提示開發(fā)者,這對于多人協(xié)助、代碼維護、代碼風(fēng)格的一致性都會有很大的幫助。
更多的應(yīng)用場景有:
Input 必須要有
maxlength屬性,防止請求的后端接口數(shù)據(jù)庫異常代碼中不能出現(xiàn)加減乘除等計算,如果需要計算應(yīng)該引入工具函數(shù),來控制由于前端浮點數(shù)計算引起的 Bug
規(guī)范限制,單位元的兩邊的括號要用英文括號,不能用中文括號,來達到交互展示統(tǒng)一的效果
代碼中不能使用 OSS 地址的靜態(tài)資源路徑,應(yīng)該使用 CDN 地址的資源路徑
...
參考文獻
https://developer.mozilla.org/zh-CN/docs/Mozilla/Projects/SpiderMonkey/Parser_API
https://eslint.org/docs/developer-guide/working-with-rules
看完兩件事

