如何為團隊潛規則明碼標價

doodlewind發表於2018-05-05

靠譜的前端團隊一般都會引入自己的程式碼風格規範,但真實專案中的問題常常不是統一了空格數量和是否加分號就能解決的,而是有很多看不見的暗坑。我們是否有程式碼風格之上,對程式碼質量的更高階把控呢?ESLint 外掛或許能夠為你開啟新世界的大門。

在前端發展日新月異的這個時代,我們實際上正在面臨日益嚴峻的前端程式碼腐化問題。譬如,但凡接手維護過前端專案的同學,應該多少都遇到過這樣的場景:

  • 約定好的模組路徑、命名方式等規範沒有被遵守,專案展開像是一盤散沙。
  • 要做同樣的事情,有的地方用了第三方庫、有的地方用了內部庫、有的地方自己重新裸寫。
  • 老語法和新語法混編共存。比如光匯入一個庫的方式,就有 ESM / CommonJS / UMD 等不一而足。
  • ……

按照破窗理論,如果環境中的不良現象如果被放任存在,會誘使人們仿效,甚至變本加厲。但程式碼中的熵增現象並不意味著程式設計師們都是犯罪分子,很多時候上面這些問題,實際上源自於專案中對新人來說陌生的隱式潛規則

  • 如果新同學不知道我們已經習慣了匯入 lodash/xxx 來減少包體積,那麼他可能會全量匯入 lodash。
  • 如果新同學不知道我們已經封裝了一個處理請求、序列化之類常見業務需求的基礎庫,那麼他可能安裝一個第三方庫,甚至重新發明輪子。
  • 如果新同學不知道我們的業務元件約定了需從某個公共基類繼承,那麼他就有可能另起爐灶搞一個他最容易理解的新版本。
  • ……

這個層面上的一致性問題已經超出了是用空格還是 tab,或者行尾加不加分號的範疇,市面上已有的程式碼風格檢查工具是「管不到這麼寬」的。當然了,這個層面的問題也確實可以用 Code Review 來解決,但人工評審未必在每個標榜敏捷和 Moving Fast 的團隊中都推得開。那麼,我們是否有更高效率的手段,來將這些隱式的「潛規則」沉澱下來呢?這裡我們嘗試給出一種答案:編寫自己的業務 Lint 外掛

在 2018 年,ESLint 基本已經是靠譜前端腳手架中的必備依賴了。但多數情況下我們都是使用一個現成的程式碼風格規範。但 ESLint 實際上並不僅僅可以用於檢測空格、換行等風格問題,在與業務開發規範相結合後,就會發現它還具有非常大的潛力。而自己從頭編寫一個 ESLint 外掛的過程其實也並不複雜,讓我們來看看如何實踐吧:

環境配置

和普通的前端專案一樣,ESLint 外掛也提供了一套開箱即用的腳手架。只需要安裝全域性依賴:

npm install -g yo generator-eslint
複製程式碼

就可以建立我們自己的外掛了:

mkdir eslint-plugin-demo
cd eslint-plugin-demo

yo eslint:plugin

? What is your name? ...
? What is the plugin ID? demo
? Type a short description of this plugin: ...
? Does this plugin contain custom ESLint rules? Yes
? Does this plugin contain one or more processors? No

npm install
複製程式碼

初始化一個外掛,就和 create-react-app 一樣簡單對吧?

建立規則

現在是時候來建立我們的第一條 Lint 規則了!作為例子,空格排版強迫症患者的筆者不喜歡在程式碼裡看到這樣的註釋:

// 獲取abc資料2次
複製程式碼

Web 上中文排版的慣例其實是這樣的:

// 獲取 abc 資料 2 次
複製程式碼

但是並不是誰都和筆者一樣甚至在微信聊天裡都堅持手動插入空格,而在 commit 記錄裡強行改動別人的註釋,也有種替人挖鼻孔的不適感。那麼我們把這個約定升級為 ESLint 的規則呢?我們需要理解一點 ESLint 的工作原理。

ESLint 使用 Espree 這個 JavaScript parser 來解析你的專案原始碼。Parser 會將原始碼字串解析為一棵抽象語法樹(AST),對於樹中的每一個節點,ESLint 都會尋找是否存在與之匹配的規則,若匹配則計算出該規則是否滿足。而 AST 是什麼樣的呢?譬如一行 // hello world 的 JS 程式碼檔案,AST 格式形如:

{
  "type": "Program",
  "start": 14,
  "end": 14,
  "body": [],
  "innerComments": [
    {
      "type": "Line",
      "value": " hello world",
      "start": 0,
      "end": 14
      "range": [
        0,
        12
      ]
    }
  ],
  "//": "......"
}
複製程式碼

基於這個資料結構,如果希望對所有的變數宣告語句新增規則,那麼我們的外掛規則就形如:

module.exports = function (context) {
  return {
    'VariableDeclaration' (node) {
      // 在這裡搞事情
      // ...
    }
  }
}
複製程式碼

這個 VariableDeclaration 是哪來的呢?這就是時候展示你作為資深前端,對於 ES Spec 的熟悉了!實際上,JavaScript 中的每一種語句,在規範中都定義了相應的型別,我們按照型別名稱即可編寫對其進行校驗的規則了。如果我們希望對註釋做校驗,那麼將上面示例中的名稱換成 Comment 即可。是不是很符合直覺呢?

上面的這種方式可以理解為非常經典的 Visitor 模式,它在方便宣告式地編寫規則的同時,也有相鄰節點之間完全透明,不方便一些複雜操作的問題。因此你也可以使用一些更過程式的 API 來輔助規則的編寫:

module.exports = {
  create: function (context) {
    const sourceCode = context.getSourceCode()

    return {
      // Program 相當於 AST 根節點
      Program () {
        const comments = sourceCode.getAllComments()
        comments.forEach(node => {
          if (/* 滿足校驗規則 */) {
            context.report(node, 'Something WRONG!')
          }
        })
      }
    }
  }
}
複製程式碼

對於我們現在檢測空格的需求,一個現成的依賴是 pangu.js。我們在上面的註釋處呼叫 pangu 的格式化 API 就能夠實現校驗了。但在實際編寫自己的外掛時,具體的業務規則往往不是難點,難點實際上在於對 JS 語法樹結構的熟悉。這裡特別推薦 astexplorer 這個工具,它能夠直觀地讓你瞭解原始碼對應的 AST 結構,方便校驗規則的編寫。

到這裡,我們應該已經對編寫規則有了一些直觀的認識了。回到開頭提出的問題,我們就可以用 ESLint 對症下藥了:

  • 對於特定模組檔案,我們能夠編寫 ESLint 規則,要求其變數命名滿足特殊的約定。
  • 對於經過團隊基礎庫封裝後的原生 API,在 ESLint 規則中禁止它的出現,從而避免重新發明 fetch 一類的問題。
  • 對於不符合最佳實踐的語法使用,我們可以及時告警。比如,發現 require 語句正在為 _lodash 賦值時,這多半會帶來包體積的劇增,可以編寫規則來避免。
  • ……

測試驅動

我們已經知道了怎麼編寫靈活的校驗規則,但這些程式碼多半在日常的業務開發中不會遇到,該怎麼保證它靠譜呢?這就需要我們引入測試驅動的開發模式了。

在外掛的 package.json 裡,會有這樣的指令碼:

"scripts": {
  "test": "mocha ./tests/**/*.js"
}
複製程式碼

作為示例,我們在 /tests 目錄下新增 spacing-test.js 測試用例,填入這樣的內容:

const rule = require('../lib/rules/spacing')
const RuleTester = require('eslint').RuleTester

const ruleTester = new RuleTester()
ruleTester.run('comment', rule, {
  valid: [
    '// 白色相簿 2'
  ],
  invalid: [
    {
      code: '// 白色相簿2',
      errors: [{
        message: 'Something WRONG!',
        type: 'Line'
      }]
    }
  ]
})
複製程式碼

這就是通過測試驅動 ESLint 外掛開發的基礎方式了。對於規則所希望覆蓋到的程式碼片段,可以通過測試用例的形式提供,這會在很大程度上便利後來者的理解和維護。編寫完測試用例後,執行用例的方式也非常簡單:

npm test
複製程式碼

測試用例全部通過,就代表著外掛大功告成了!剩下的就是將它釋出到 NPM 上,按照 ESLint 外掛的配置方式,在你的專案中引入就行啦。在第一步腳手架為你生成的 README 中,這個過程已經有了很詳盡的文件,在此就不贅述了。

總結

很多前端同學為了鑽研技術深度,會去閱讀 ES Spec 的規範文件。但可惜的是這個層面的內容很多時候對於一般的業務開發用處不是很大。但在你具備了開發(而不是使用)ESLint 外掛的能力後,配合上你對 JS 本身的熟悉,就會有種解鎖了「控制程式碼的程式碼」技能的船新感覺:用程式碼去約束和優化程式碼本身,這就是 Meta Programming 的威力了吧。

上文中編寫的註釋外掛也已經發布到 GitHub,歡迎參考或供強迫症同學試用哦。最後突然想沒來由地提一句…

願意為你在微信里加空格的妹子,一定是真愛了。

相關文章