您好,登錄后才能下訂單哦!
開發(fā) eslint 規(guī)則
前端的日常開發(fā)離不開各種 lint 的支持,使用 lint 的一種誤解是:個人能力不足,必須 lint 規(guī)范才能寫出規(guī)范的代碼,實際上規(guī)范的定義主要取決于開源項目作者的習慣,或者公司團隊編碼的習慣,即使兩個前端專家,寫出的代碼規(guī)范也會有差別。
今天主題聊聊 eslint,作為最主流的 JavaScript lint 工具深受大家喜愛,而 JSHint 卻逐漸淡出了大家的視線,使用的比較少了
常用的 eslint 擴展有 standard,airbnb 等
剖析 eslint 擴展
擴展無非就作兩個事情
原理就是利用 eslint 的繼承模式,理論上可以無限繼承并覆蓋上一級的規(guī)則
第一條不詳細介紹了,eslint 官網(wǎng)說的十分詳細,基本每一條規(guī)則都支持自定義參數(shù),覆蓋面也特別廣,基本上所有的語法都有 rule
第二條的自定義 rule 才是本文的重頭戲,因為特殊的業(yè)務(wù)場景靠 eslint 自身配置已經(jīng)無法滿足業(yè)務(wù)需求了,如:
一般特殊場景的自定義規(guī)則都使用 eslint-plugin-*
的命名,使用時可以方便的寫成
{ plugins: [ 'vue', 'react', 'jest' ] }
當然 eslint-config-* 同理,不過配置時需要寫成
{ extends: 'standard' }
下面介紹下開發(fā)流程
創(chuàng)建 eslint plugin 工程
官方推薦使用 yeoman 生成項目,感覺生成的項目比較守舊,推薦下習慣我的項目結(jié)構(gòu)
eslint-plugin-skr |- __tests__ | |- rules | |- utils | |- lib | |- rules | |- utils | |- index.js | |- jest.config.js | |- package.json | |- README.md
整體看下來發(fā)現(xiàn)多了 jest 配置文件,是的 yeoman 生成的項目默認采用 Mocha 作為測試框架,個人感覺調(diào)試起來麻煩,沒有 jest 靈活,vscode 輕松搞定調(diào)試
教程一搜一大把哈,給伸手黨一個鏈接debugging-jest-tests
關(guān)于 jest 的 config 文件也po出來一下,都是些基本的配置,復(fù)雜的用不到,下面會詳細介紹測試部分
module.exports = { testEnvironment: 'node', roots: ['__tests__'], resetModules: true, clearMocks: true, verbose: true }
自定義的規(guī)則全部在 lib/rules 下面,每條規(guī)則單獨一個文件足以
下面一個簡單的例子打通任督二脈
開發(fā)一個規(guī)則
前期準備
這個官方文檔寫的密密麻麻,好幾十個屬性,其實只是冰山一角,有很多復(fù)雜場景需要考慮
有人疑問:一定需要精通 AST?
我的回答是當然不需要,簡單了解便是,最起碼知道解析出來的語法樹大體結(jié)構(gòu)長什么樣子
那就隨便給自己一個命題寫吧!寫個超級簡單的
module.exports = { meta: { docs: { description: '禁止塊級注釋', category: 'Stylistic Issues', recommended: true } }, create (context) { const sourceCode = context.getSourceCode() return { Program () { const comments = sourceCode.getAllComments() const blockComments = comments.filter(({ type }) => type === 'Block') blockComments.length && context.report({ message: 'No block comments' }) } } } }
具體寫法官方文檔有介紹哈,就不贅述了,例子也十分簡單,調(diào)用了環(huán)境變量 context 中的方法獲取全部注釋
稍微復(fù)雜點的場景
如需要 lint bar
對象中屬性的順序,如下假設(shè)一個規(guī)則
// good const bar = { meta: {}, double: num => num * 2 } // bed const bar = { double: num => num * 2, meta: {}, }
這個第一次些會有些蒙,官網(wǎng)沒有提供具體的例子,解決辦法很簡單,推薦一個利器astexplorer
點進去別著急復(fù)制代碼查看 AST 結(jié)果,首先選擇 espree(eslint 使用的語法解析庫),如下
這短短的四行代碼會對應(yīng)著一個抽象語法樹,如下圖:
由于全展開太長了哈,感興趣的自行嘗試,會發(fā)現(xiàn)層級嵌套的特別深,找到 bar
的屬性需要 Program.body[0].declarations[0].init.properties
當然不至于每次都從最頂級的 Program
找下來,從上面的例子可以看出 create
方法的 return
返回的是一個 object,里面可以定義很多檢測類型,如官網(wǎng)的例子:
function checkLastSegment (node) { // report problem for function if last code path segment is reachable } module.exports = { meta: { ... }, create: function(context) { // declare the state of the rule return { ReturnStatement: function(node) { // at a ReturnStatement node while going down }, // at a function expression node while going up: "FunctionExpression:exit": checkLastSegment, "ArrowFunctionExpression:exit": checkLastSegment, onCodePathStart: function (codePath, node) { // at the start of analyzing a code path }, onCodePathEnd: function(codePath, node) { // at the end of analyzing a code path } } } }
這里可以使用 VariableDeclarator
類型作為檢察目標,從下面的解析樹可以分析出篩選條件
以 VariableDeclarator
對象作為當前的 node
當變量名為 bar
,即 node.id.name === 'bar'
,且值為對象,即 node.init.type === 'ObjectExpression'
,代碼如下:
module.exports = { meta: { ... }, create (context) { return { VariableDeclarator (node) { const isBarObj = node.id.name === 'bar' && node.init.type === 'ObjectExpression' if (!isBarObj) return // checker } } } }
就這樣成功取到 bar
對象后就可以檢測屬性的順序了,排序算法一大把,挑一個喜歡的用就行了,這里不啰嗦了,直接上結(jié)果:
const ORDER = ['meta', 'double'] function getOrderMap () { const orderMap = new Map() ORDER.forEach((name, i) => { orderMap.set(name, i) }) return orderMap } module.exports = { create (context) { const orderMap = getOrderMap() function checkOrder (propertiesNodes) { const properties = propertiesNodes .filter(property => property.type === 'Property') .map(property => property.key) properties.forEach((property, i) => { const propertiesAbove = properties.slice(0, i) const unorderedProperties = propertiesAbove .filter(p => orderMap.get(p.name) > orderMap.get(property.name)) .sort((p1, p2) => orderMap.get(p1.name) > orderMap.get(p2.name)) const firstUnorderedProperty = unorderedProperties[0] if (firstUnorderedProperty) { const line = firstUnorderedProperty.loc.start.line context.report({ node: property, message: `The "{{name}}" property should be above the "{{firstUnorderedPropertyName}}" property on line {{line}}.`, data: { name: property.name, firstUnorderedPropertyName: firstUnorderedProperty.name, line } }) } }) } return { VariableDeclarator (node) { const isBarObj = node.id.name === 'bar' && node.init.type === 'ObjectExpression' if (!isBarObj) return checkOrder(node.init.properties) } } } }
這里代碼有點多,耐心看完其實挺簡單的,大致解釋下
getOrderMap
方法將數(shù)組轉(zhuǎn)成 Map 類型,方面通過 get
獲取下標,這里也可以處理多緯數(shù)組,例如兩個 key
希望在相同的排序等級,不分上下,可以寫成:
const order = [ 'meta' ['double', 'treble'] ] function getOrderMap () { const orderMap = new Map() ORDER.forEach((name, i) => { if (Array.isArray(property)) { property.forEach(p => orderMap.set(p, i)) } else { orderMap.set(property, i) } }) return orderMap }
這樣 double
和 treble
就擁有相同的等級了,方便后面擴展,當然實際情況會有 n 個屬性的排序規(guī)則,也可以在這個規(guī)則上輕松擴展,內(nèi)部的 sort 邏輯就不贅述了。
開發(fā)就介紹到這里,通過上面安利的在線語法解析工具可以輕松反推出 lint 邏輯。
如果 rule 比較復(fù)雜,就需要大量的 utils 支持,不然每個 rule 都會顯得一團糟,比較考驗公共代碼提取的能力
測試
如前面所講建議使用 jest 進行測試,這里的測試和普通的單元測試還不太一樣,eslint 是基于結(jié)果的測試,什么意思呢?
lint 只有兩種情況,通過與不通過,只需要把通過和不通過的情況整理成兩個數(shù)組,剩下的工作交給 eslint 的 RuleTester
處理就行了
上面的屬性排序 rule,測試如下:
const RuleTester = require('eslint').RuleTester const rule = require('../../lib/rules/test') const ruleTester = new RuleTester({ parserOptions: { ecmaVersion: 6 } }) ruleTester.run('test rule', rule, { valid: [ `const bar = { meta: {}, double: num => num * 2 }` ], invalid: [ { code: `const bar = { double: num => num * 2, meta: {}, }`, errors: [{ message: 'The "meta" property should be above the "double" property on line 2.' }] } ] })
valid
中是希望通過的代碼, invalid
中是不希望通過的代碼和錯誤信息,到這里一個 rule 算是真正完成了。
打包輸出
最后寫好的 rules 需要發(fā)一個 npm 包,以便于在項目中使用,這里就不贅述怎么發(fā)包了,簡單聊聊怎么優(yōu)雅的把 rules 導(dǎo)出來。
直接上代碼:
const requireIndex = require('requireindex') // import all rules in lib/rules module.exports.rules = requireIndex(`${__dirname}/rules`)
這里使用了三方依賴 requireindex
,對于批量的導(dǎo)出一個文件夾內(nèi)的所有文件顯得簡潔很多。
當然前提是保證 rules 文件夾下都是 rule 文件,不要把 utils 寫進去哈
總結(jié)
行文目的是國內(nèi)外對于自定義 eslint rule 的相關(guān)資源較少,希望分享一些寫自定義規(guī)則的經(jīng)驗。
千萬不要在學習 AST 上浪費時間,不同的庫對 AST 的實現(xiàn)是不同的,下次寫 babel 插件又要學其它的 AST 規(guī)則,再次安利一下 AST 神器astexplorer,只要把需要驗證的代碼放到 astexplorer
里跑一遍,然后總結(jié)出規(guī)律,邏輯其實十分簡單,對 AST 結(jié)果進行判斷就行了。
從團隊層面講,希望所有的團隊都有自己的 eslint 規(guī)則庫,可以大大降低 code review 的成本,又能保證代碼的一致性,一勞永逸的事情。
以上就是本文的全部內(nèi)容,希望對大家的學習有所幫助,也希望大家多多支持億速云。
免責聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進行舉報,并提供相關(guān)證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權(quán)內(nèi)容。