前言
雖然現在已經有很多實用的 ESLint 插件了,但隨着項目不斷迭代發展,你可能會遇到已有 ESLint 插件不能滿足現在團隊開發的情況。這時候,你需要自己來創建一個 ESLint 插件。
本文我將帶你了解各種Lint工具的大致歷史,然后一步一步地創建一個屬於你自己的 ESLint 插件,以及教你如何利用AST抽象語法樹來制定這個插件的規則。
以此來帶你了解 ESLint 的實現原理。
課外知識:Lint 簡史
Lint 是為了解決代碼不嚴謹而導致各種問題的一種工具。比如 == 和 === 的混合使用會導致一些奇怪的問題。
JSLint 和 JSHint
2002年,Douglas Crockford 開發了可能是第一款針對 JavaScript 的語法檢測工具 —— JSLint,並於 2010 年開源。
JSLint 面市后,確實幫助許多 JavaScript 開發者節省了不少排查代碼錯誤的時間。但是 JSLint 的問題也很明顯—— 幾乎不可配置,所有的代碼風格和規則都是內置好的;再加上 Douglas Crockford 推崇道系「愛用不用」的優良傳統,不會向開發者妥協開放配置或者修改他覺得是對的規則。於是 Anton Kovalyov 吐槽:「JSLint 是讓你的代碼風格更像 Douglas Crockford 的而已」,並且在 2011 年 Fork 原項目開發了 JSHint。《Why I forked JSLint to JSHint》
JSHint 的特點就是可配置,同時文檔也相對完善,而且對開發者友好。很快大家就從 JSLint 轉向了 JSHint。
ESLint 的誕生
后來幾年大家都將 JSHint 作為代碼檢測工具的首選,但轉折點在2013年,Zakas 發現 JSHint 無法滿足自己制定規則需求,並且和 Anton 討論后發現這根本不可能在JShint上實現,同時 Zakas 還設想發明一個基於 AST 的 lint。於是 2013年6月份,Zakas 發布了全新 lint 工具——ESLint。《Introducing ESLint》
var ast = esprima.parse(text, { loc: true, range: true }), walk = astw(ast); walk(function(node) { api.emit(node.type, node); }); return messages; 復制代碼
ESLint 的逆襲
ESLint 的出現並沒有撼動 JSHint 的霸主地位。由於前者是利用 AST 處理規則,用 Esprima 解析代碼,執行速度要比只需要一步搞定的 JSHint 慢很多;其次當時已經有許多編輯器對 JSHint 支持完善,生態足夠強大。真正讓 ESLint 逆襲的是 ECMAScript 6 的出現。
2015 年 6 月,ES2015 規范正式發布。但是發布后,市面上瀏覽器對最新標准的支持情況極其有限。如果想要提前體驗最新標准的語法,就得靠 Babel 之類的工具將代碼編譯成 ES5 甚至更低的版本,同時一些實驗性的特性也能靠 Babel 轉換。 但這時候的 JSHint 短期內無法提供支持,而 ESLint 卻只需要有合適的解析器就能繼續去 lint 檢查。Babel 團隊就為 ESLint 開發了一款替代默認解析器的工具,也就是現在我們所見到的 babel-eslint,它讓 ESLint 成為率先支持 ES6 語法的 lint 工具。
也是在 2015 年,React 的應用越來越廣泛,誕生不久的 JSX 也愈加流行。ESLint 本身也不支持 JSX 語法。但是因為可擴展性,eslint-plugin-react 的出現讓 ESLint 也能支持當時 React 特有的規則。
2016 年,JSCS 開發團隊認為 ESLint 和 JSCS 實現原理太過相似,而且需要解決的問題也都一致,最終選擇合並到 ESLint,並停止 JSCS 的維護。
當前市場上主流的 lint 工具以及趨勢圖:
從此 ESLint 一統江湖,成為替代 JSHint 的前端主流工具。
目標&涉及知識點
本文 ESLint 插件目標是在項目開發中禁用:console.time()。
- AST 抽象語法樹
- ESLint
- Npm 發布
- 單元測試
插件腳手架構建
這里我們利用 yeoman 和 generator-eslint 來構建插件的腳手架代碼。安裝:
npm install -g yo generator-eslint
復制代碼
本地新建文件夾eslint-plugin-demofortutorial:
mkdir eslint-plugin-demofortutorial
cd eslint-plugin-demofortutorial 復制代碼
初始化 ESLint 插件的項目結構:
yo eslint:plugin // 搭建一個初始化的目錄結構
復制代碼
此時文件的目錄結構為:
.
├── README.md
├── lib
│ ├── index.js
│ └── rules
├── package.json
└── tests
└── lib
└── rules
復制代碼
安裝依賴:
npm install
復制代碼
至此,環境搭建完畢。
創建規則
終端執行:
yo eslint:rule // 生成默認 eslint rule 模版文件
復制代碼
此時項目結構為:
.
├── README.md
├── docs // 使用文檔
│ └── rules
│ └── no-console-time.md
├── lib // eslint 規則開發
│ ├── index.js
│ └── rules // 此目錄下可以構建多個規則,本文只拿一個規則來講解
│ └── no-console-time.js
├── package.json
└── tests // 單元測試
└── lib
└── rules
└── no-console-time.js
復制代碼
上面結構中,我們需要在 ./lib/ 目錄下去開發 Eslint 插件,這里是定義它的規則的位置。
AST 在 ESLint 中的運用
在正式寫 ESLint 插件前,你需要了解下 ESLint 的工作原理。其中 ESLint 使用方法大家應該都比較熟悉,這里不做講解,不了解的可以點擊官方文檔如何在項目中配置 ESLint。
在公司團隊項目開發中,不同開發者書寫的源碼是各不相同的,那么在 ESLint 中,如何去分析每個人寫的源碼呢?
作為開發者,面對這類問題,我們必須懂得要使用 抽象的手段 !那么 Javascript 的抽象性如何體現呢?
沒錯,就是 AST (Abstract Syntax Tree(抽象語法樹)),再祭上那張看了幾百遍的圖。
在 ESLint 中,默認使用 esprima 來解析我們書寫的 Javascript 語句,讓其生成抽象語法樹,然后去 攔截 檢測是否符合我們規定的書寫方式,最后讓其展示報錯、警告或正常通過。 ESLint 的核心就是規則(rules),而定義規則的核心就是利用 AST 來做校驗。每條規則相互獨立,可以設置禁用off、警告warn⚠️和報錯error❌,當然還有正常通過不用給任何提示。
規則創建
上面講完了 ESLint 和 AST 的關系之后,我們可以正式進入開發具體規則。先來看之前生成的 lib/rules/no-console-time.js:
/**
* @fileoverview no console.time()
* @author Allan91
*/
"use strict"; //------------------------------------------------------------------------------ // Rule Definition //------------------------------------------------------------------------------ module.exports = { meta: { docs: { description: "no console.time()", category: "Fill me in", recommended: false }, fixable: null, // or "code" or "whitespace" schema: [ // fill in your schema ] }, create: function(context) { // variables should be defined here //---------------------------------------------------------------------- // Helpers //---------------------------------------------------------------------- // any helper functions should go here or else delete this section //---------------------------------------------------------------------- // Public //---------------------------------------------------------------------- return { // give me methods }; } }; 復制代碼
這個文件給出了書寫規則的模版,一個規則對應一個可導出的 node 模塊,它由 meta 和 create 兩部分組成。
- meta 代表了這條規則的元數據,如其類別,文檔,可接收的參數的 schema 等等。
- create:如果說 meta 表達了我們想做什么,那么 create 則用表達了這條 rule 具體會怎么分析代碼;
Create 返回一個對象,其中最常見的鍵名是AST抽象語法樹中的選擇器,在該選擇器中,我們可以獲取對應選中的內容,隨后我們可以針對選中的內容作一定的判斷,看是否滿足我們的規則。如果不滿足,可用 context.report 拋出問題,ESLint 會利用我們的配置對拋出的內容做不同的展示。
具體參數配置詳情見官方文檔
本文創建的 ESLint 插件是為了不讓開發者在項目中使用 console.time(),先看看這段代碼在抽象語法樹中的展現:
其中,我們將會利用以下內容作為判斷代碼中是否含有 console.time:
那么我們根據上面的AST(抽象語法書)在 lib/rules/no-console-time.js 中這樣書寫規則:
/** * @fileoverview no console.time() * @author Allan91 */ "use strict"; //------------------------------------------------------------------------------ // Rule Definition //------------------------------------------------------------------------------ module.exports = { meta: { docs: { description: "no console.time()", category: "Fill me in", recommended: false }, fixable: null, // or "code" or "whitespace" schema: [ // fill in your schema ], // 報錯信息描述 messages: { avoidMethod: "console method '{{name}}' is forbidden.", }, }, create: function(context) { return { // 鍵名為ast中選擇器名 'CallExpression MemberExpression': (node) => { // 如果在ast中滿足以下條件,就用 context.report() 進行對外警告⚠️ if (node.property.name === 'time' && node.object.name === 'console') { context.report({ node, messageId: 'avoidMethod', data: { name: 'time', }, }); } }, }; } }; 復制代碼
再修改 lib/index.js:
"use strict"; module.exports = { rules: { 'no-console-time': require('./rules/no-console-time'), }, configs: { recommended: { rules: { 'demofortutorial/no-console-time': 2, // 可以省略 eslint-plugin 前綴 }, }, }, }; 復制代碼
至此,Eslint 插件創建完成。接下去你需要做的就是將此項目發布到 npm平台。 根目錄執行:
npm publish
復制代碼
打開npm平台,可以搜索到上面發布的 eslint-plugin-demofortutorial 這個 Node 包。
如何使用
發布完之后在你需要的項目中安裝這個包:
npm install eslint-plugin-demofortutorial -D
復制代碼
然后在 .eslintrc.js 中配置:
"extends": [ "eslint:recommended", "plugin:eslint-plugin-demofortutorial/recommended", ], "plugins": [ 'demofortutorial' ], 復制代碼
如果之前沒有.eslintrc.js 文件,可以執行下面命令生成:
npm install -g eslint
eslint --init
復制代碼
此時,如果在當前項目的 JS 文件中書寫 console.time,會出現如下效果:
單元測試(完善)
對於完整的 npm 包來說,上面還只算是個“半成品”,我們需要寫單元測試來保證它的完整性和安全性。
下面來完成單元測試,在 ./tests/lib/rules/no-console-time.js 中編寫如下代碼:
'use strict'; // ------------------------------------------------------------------------------ // Requirements // ------------------------------------------------------------------------------ let rule = require('../../../lib/rules/no-console-time'); let RuleTester = require('eslint').RuleTester; // ------------------------------------------------------------------------------ // Tests // ------------------------------------------------------------------------------ let ruleTester = new RuleTester({ parserOptions: { ecmaVersion: 10, }, }); ruleTester.run('no-console-time', rule, { valid: [ // 合法示例 '_.time({a:1});', "_.time('abc');", "_.time(['a', 'b', 'c']);", "lodash.time('abc');", 'lodash.time({a:1});', 'abc.time', "lodash.time(['a', 'b', 'c']);", ], invalid: [ // 不合法示例 { code: 'console.time()', errors: [ { messageId: 'avoidMethod', }, ], }, { code: "console.time.call({}, 'hello')", errors: [ { messageId: 'avoidMethod', }, ], }, { code: "console.time.apply({}, ['hello'])", errors: [ { messageId: 'avoidMethod', }, ], }, { code: 'console.time.call(new Int32Array([1, 2, 3, 4, 5]));', errors: 1, }, ], }); 復制代碼
上面測試代碼詳細介紹見官方文檔。
根目錄執行:
npm run test 復制代碼
至此,這個包的開發完成。其它規則開發也是類似,比如您可以繼續制定其它規范,比如 ️console.log() 、debugger警告等等。
其它
由於自動生成ESLint的項目中依賴的 eslint 版本還在 3.x 階段,會對單元測試語法解析造成如下報錯:
'Parsing error: Invalid ecmaVersion.' 復制代碼
建議將該包升級到 "eslint": "^5.16.0" 。
以上。
查看Npm上發布的包
參考資料:
zhuanlan.zhihu.com/p/32297243 en.wikipedia.org/wiki/Lint_(… octoverse.github.com/ jslint.com medium.com/@anton/why-… www.nczonline.net/blog/2013/0… eslint.org jscs.info github.com/babel/babel… github.com/yannickcr/e… www.nczonline.net/blog/2016/0… medium.com/@markelog/j…
作者:Allan91
鏈接:https://juejin.im/post/5d91be23f265da5ba532a07e
來源:掘金
著作權歸作者所有。商業轉載請聯系作者獲得授權,非商業轉載請注明出處。
