JS Linter 進化史

JS Linter 進化史

來自專欄前端之美

據說在 C 語言剛剛開始起步的時候,有若干常見的代碼問題不能被編譯器捕獲,因此有人開發了一款名為 Lint 的附加程序,用於掃描源代碼以捕獲這些問題。因此後續類似的工具都叫 Lint 或 Linter。

JavaScript 最初被倉促設計出來解決一些網頁中的「小」問題,不過蒙幸運女神的眷顧,逐步發發展壯大,成為當前風頭最勁的編程語言之一。在這一過程中,Linter 工具應運而生,主要解決如下幾個問題:

  • 避免最初設計中存在的不少糟粕,提倡更多地使用 Good Parts
  • 發現一些 JS 代碼中存在的問題,例如忘記聲明而導致的全局變數
  • 保證代碼風格統一,便於大型項目維護

本文主要介紹下 JS Linter 進化史中的三個里程碑式的工具:JSLint、JSHint 和 ESLint。

開山鼻祖 JSLint

JSLint 是公認的第一個 JS Linter,由赫赫有名的老道-Douglas Crockford 在 2002 發布。彼時的前端還處於萌芽狀態,大部分人還是用 JS 來寫一些非常粗糙的頁面效果、表單驗證。JQuery 也要在四年之後才正式發布。

JSLint 的核心是 Top Down Operator Precedence(自頂向下的運算符優先順序)技術實現的 Pratt 解析器。由於涉及到比較底層的詞法解析、語法解析等編譯原理,本文中不再贅述,不過推薦感興趣的同學去看下原文,或者看「代碼之美」一書中的第九章中文版。簡單來說,就是用 JS 實現了一個處理 JS 源碼的解析器,可以識別出其中的各種表達式、語句、函數等組成要素。

Pratt 解析器的處理結果本質上類似於 AST(抽象語法樹),但並沒有做到那麼極致,畢竟那時候還是 2002 年啊,用 JS 實現一個 JS 解析器本身已經是一件非常了不得的成就。有了這樣強大的解析器支持,事實上基本可以做任何事,例如代碼壓縮、代碼檢查、代碼高亮等。

JSLint 即是藉助它的強大能力,在其內部的一些處理過程中恰當地進行改造,加入了一些規則的檢查,從而實現了 Lint 的功能。

舉個例子:

var varstatement = function varstatement(prefix) { var id, name, value; if (funct[(onevar)] && option.onevar) { warning("Too many var statements."); } else if (!funct[(global)]) { funct[(onevar)] = true; } ...};

如果設定了 onevar 規則,即同一作用域下只能用一個 var 聲明所有變數,那麼在每個包含 var 的語句解析時都會進行該規則的檢查,如果發現之前同一作用域已經有 var 語句,則報告一個 「Too many var statements.」 的 warnning。

毫無疑問,JSLint 的出現,以及接下來蝴蝶書的問世,讓廣大前端工程師在如果用好 JS 的 Good Parts 方面受益匪淺。

繼往開來 JSHint

金無足赤,人無完人。在隨後的前端跨越式發展中,JSLint 的一些不足暴露了出來。2011 年 12 月 20 日,Anton Kovalyov 發表了一篇標誌性的文章:Why I forked JSLint to JSHint,指出了 JSLint 存在的幾個主要問題:

  1. 令人不安地固執己見,沒有提供一些規則的配置

    Anton 措辭激烈:「It is quickly transforming from a tool that helps developers to prevent bugs to a tool that makes sure you write your code like Douglas Crockford.」
  2. 對社區反饋不關注

    這一點從 JSLint 的 Issues、Pull Requests 中可以看出,另外值得注意的是 Contributors 中只有兩名開發者,而且第二名開發者只有三行代碼的修改(註:可能跟 Douglas 的習慣有關,有些 PR 是通過的,但是並沒有合併,而是由他自己提交了新的 commit)

JSHint 就是在這樣的背景下誕生了:

And, as we were saying from the day zero, it will always be a community-driven tool. Simply because a community of programmers working together is better than a single person working alone. No matter who this person is.

它的核心定位是社區驅動的代碼質量工具,因為 Anton 堅信程序員組成的社區一起協作要好於單打獨鬥。

在核心實現仍然基於 Pratt 解析器的基礎上,藉助社區的共同努力,JSHint 進行了多方面的改進:

  1. 更多可配置的規則

    這是社區的核心訴求。除了開放規則的 Pull Request 外,JSHint 在規則文檔、預定義環境方面也做了諸多努力
  2. 代碼模塊化

    JSLint 源文件只有一個 5000 余行的大文件,對於一般開發者來說難以維護。JSHint 按照功能劃分為 lex、scope、core、reporter 等多個模塊,功能更聚焦
  3. CLI

    命令行工具的支持,為很多第三方工具提供了基礎,JSHint 可以很好地和各種 IDE 集成
  4. 測試

    JSLint 一行測試代碼也沒有,JSHint 的整體代碼測試覆蓋率達到了 99%

下圖是 Google Trends 中 JSHint 和 JSLint 關鍵字搜索次數的趨勢圖:

JSLint VS JSHint - Google Trends

可以看出,在 JSHint 快速增長的同時,JSLint 基本處於停滯或者下降狀態。

回過頭來看,JSHint 的出現幾乎是必然的,前端開發者日益增長的代碼質量訴求和落後偏執的工具之間出現了不可調和的矛盾。JSHint 選擇了一條正確的道路,眾志成城,其利斷金。現在 JSHint 已經有多達 236 個 Contributors,上千個 Pull Requests。

重新出發 ESLint

JSHint 在第三版的計劃中有這樣一條:

Build a foundation for plugins by exposing AST and adding additional hooks.

前端爆髮式增長帶來的巨大需求讓 JSHint 變得愈加難以應對,通過暴露 AST 信息來支持第三方插件無疑是一劑良方。然而理想總是美好的,現實卻非常殘酷。這一計劃直到現在仍然沒有實現。究其原因,在 JSLint 時代就存在的規則檢查和 Pratt 解析器深度耦合首當其衝,可謂是成也蕭何敗蕭何。

2013 年,前端界另外一名標杆性人物 Zakas 在業務開發中遇到一個 XMLHttpRequest 在 IE7 中的問題,他希望使用 JSHint 來避免類似問題。Zakas 找到 Anton,了解到插件的提案被擱置並且短期內看起來沒有希望去實施。Zakas 很失望,但牛人畢竟是牛人,他開始著手尋找其他解決方案。

Zakas 首先在公司使用的一個 PHP Linter 上得到了一些啟示,因為那個 Linter 使用了 AST 作為中間表示層,所有的規則都基於 AST 做進一步分析。他開始調研 JS 是否有類似工具,Ariya 開發的 Esprima 進入了他的視線。Zakas 盛情邀請 Ariya 到他的公司 Box 做一次不限主題的分享,而 Ariya 選擇的主題恰恰是 Esprima。分享過程中,Ariya 介紹到了 AST 表示層的意義以及目前基於它的很多工具,例如遍歷 AST 的工具 Estraverse 以及作用域分析工具 Escope。這一切讓 Zakas 受益匪淺,下一代 JS Linter 工具的方案在他腦海中漸漸清晰起來。

很快,在 2013 年六月份的一個周末,Zakas 完成了第一個可執行的版本。藉助於 Esprima 等工具,ESLint 的核心代碼只有 100 行:

api.verify = function(text, config) { ... // 添加規則 Object.keys(config.rules).forEach(function(key) { var rule = rules.get(key); if (rule) { rule(new RuleContext(key, api)); } else { throw new Error("Definition for rule " + key + " was not found."); } }); // 解析代碼,生成 AST var ast = esprima.parse(text, { loc: true, range: true }), walk = astw(ast); // 核心邏輯:遍歷 AST 各個節點 walk(function(node) { // 將當前節點通過事件進行分發,監聽這些類型節點的規則會執行自己的檢查邏輯 api.emit(node.type, node); }); // 返回檢查結果信息 return messages;};

規則的實現完全和 Linter 核心邏輯分離,以全等(eqeqeq)規則為例:

// eqeqeq rulemodule.exports = function(context) { // 監聽遍歷 AST 中觸發的 BinaryExpression 事件 context.on("BinaryExpression", function(node) { // 對這一類型節點的操作符進行檢查 var operator = node.operator; if (operator === "==") { context.report(node, "Unexpected use of ==, use === instead."); } else if (operator === "!=") { context.report(node, "Unexpected use of !=, use !== instead."); } });};

這種架構帶來了很多好處:

  1. 關注點分離

    解析器專註於源碼的詞法解析、語法解析,並生成符合 ESTree 規範的 AST。ESLint 核心部分專註於配置生成、規則管理、上下文維護、遍歷 AST、報告產出等主流程。ESLint 的規則、報告部分則通過約定介面的形式獨立出來,方便自定義擴展。這種關注點分離的架構使得 ESLint 具備了很好的可維護性和擴展性。舉了例子,為了支持一些 ES6 的新特性和 JSX,ESLint 團隊在 2014 年開發了 Espree 替代 Esprima,得益於這一良好架構,ESLint 只需要改一下依賴即可,開著飛機修飛機不再是啥難事兒
  2. 支持自定義規則

    這是 ESLint 的核心訴求。通過將代碼解析與代碼檢查邏輯分開,自定義規則可以註冊相關事件關注到自己需要檢查的代碼片段,並將檢查結果添加到報告中。一個典型的例子是 eslint-plugin-react 中的 no-find-dom-node 規則,通過自己編寫這個規則,可以禁止所有 React 代碼中的 findDomNode 調用,Code is law,而且 Code 可以 Contribute,這是多麼激動人心的一件事情啊!迄今為止,已經有超過 1000 個 ESLint 的規則插件,比較知名的有 eslint-plugin-react,eslint-plugin-jsx-a11y,eslint-plugin-vue 等
  3. 保持內核的簡單

    眾多功能通過插件的形式予以支持,內核不與特定的框架、庫、JS 方言相關。目前 ESLint 支持自定義的解析器、規則插件、預編譯插件、結果報告,它更像是一個平台,對核心的流程設定約束,並開放各方面的能力,從而適應複雜多變的實際場景

下圖是 NPM Trends 中 ESLint,JSHint 和 JSLint 關鍵字搜索次數的趨勢圖:

ESLint VS JSHint VS JSLint - NPM Trends

Zakas 後來專門寫了一篇文章來反思 ESLint 的成功,其中主要幾條有:

  • 人們編寫更多的 JS 代碼

    這是一個大環境,有需求才有供給,正是前端的蓬勃發展帶動了前端框架、庫、工具、規範的繁榮
  • ES6/Babel

    ES6 藉助於 Babel 等工具,「舊時王謝堂前燕,飛入尋常百姓家」,然而 JSHint 囿於架構問題,難以靈活調整並適應這種變化,ESLint 則站在 Esprima 等工具的肩膀上,對 ES6 甚至更新的一些規範都有良好的支持,發展迅速
  • React

    React 橫空出世時的 JSX 著實讓大家吃驚了一把,JS 還可以這樣寫。隨之而來的一個問題是,Linter 怎麼對 JSX 進行解析和檢查呢?得益於良好的架構,ESLint 基於 Esprima 開發了 Espree 支持了 JSX 的解析,再加上針對 JSX 開發的自定義規則,以一種優雅的方式實現了對 JSX 的支持。15 年 2 月份 ESLint 的下載量是 89,000,這個功能發布後,三月份的下載量暴漲了 80%
  • 可擴展性是關鍵

    做好內核,開放能力,這種深入問題本質而又極度開放包容的思路是 ESLint 成功的關鍵
  • 傾聽社區

    JSLint 的弊端 Zakas 怎能不知。在 ESLint 項目中,很多功能其實並不是他一個人拍腦袋想出來的,而是從社區的交流中慢慢摸索而來。例如可共享配置、JSX、自定義解析器等等。社區代表了用戶,他們是最熟悉實際需求的人
  • 專註於實際使用價值而不是競爭對手

    從 ESLint 項目創立之初起,Zakas 就從沒有說過 ESLint 比 JSHint 更好。他一直在說的是 ESLint 可以讓你寫自己的檢查規則,而不是貶低其它工具。他很尊重那些繼續使用 JSLint、JSHint 或者 JSCS 的開發者。當然,ESLint 的整個團隊仍然在繼續讓它變得更好用。這樣一種開放包容的心態,也讓 ESLint 和其它 Linter 工具保持了良好的互動,有時候也會「商業互吹」一把

小結

通過回顧這十幾年 JS Linter 工具進化史,一方面情不自禁地為這些核心貢獻者的付出和他們的聰明才智感到欽佩、敬仰,他們締造了這段傳奇。另一方面,於興衰之間,我們可以窺見一些成功的信條:順勢而為,關注本質,開放能力,發動群眾搞建設才是王道。

期望本文能讓你在了解這段還在繼續的歷史的同時,投身於前端開發的洪流中,做出自己的一份貢獻。

不是彩蛋

Zakas 病了,而且很嚴重。2016 年 4 月 12 號離開最後一家公司 Box 後,就一直在家休養。他目前還在跟萊姆病(Lyme disease)做鬥爭。希望他可以早日好起來!有能力翻牆的小夥伴可以到他的 Twitter 或者 Medium 加油鼓勵。

想要了解細節的話,可以讀下他的這篇文章 When your health costs you your job。

參考文獻

  1. JSLint - WikiPedia
  2. A Comparison of JavaScript Linting Tools
  3. The inception of ESLint
  4. Reflections on ESLints success
  5. JavaScript Code Analysis

推薦閱讀:

你所見過、畫過、想過最美的畫面?
ysl圓管哪個色號最值得買?
「美」來源於什麼?
字寫的好是一種怎麼樣的體驗?

TAG:前端開發 | | 代碼質量 |