優雅整合 Linter、Husky、Lint-Staged:寫扣看扣皆大歡喜的密技

優雅整合 Linter、Husky 與 Lint-Staged:寫扣看扣皆大歡喜的密技

大部份的工程師都是謹慎 龜毛 的,除了桌上的物品要放整齊外,寫 code 更是井井有條,例如:開頭要幾個空白還是 tab、要在哪裡斷行、一行限制多少字等,說是細節卻也是基本禮貌

範例如下,同一隻檔案裡有 sayHello 與 sayHi 兩個 function,但它們有不同的 text indent,sayHello 是 2 個空白,sayHi 是 4 個空白,放在一起看是不是有點煩躁呢?

const sayHello = () => {
  console.log('Hello!');
};

const sayHi = () => {
    console.log('Hi!');
};

寫 code 除了讓自己開心之外,也要讓看 code 的人開心,像是 PR 的審閱者 (reviewer)、接手維護或對功能和技術有興趣的同事。

規範這些細節的目的是維護 code base 的品質、工作流程順暢,讓開發者能專注在功能開發與技術突破上。而這些規範會寫在 coding style 的 guidline,並在新人訓練時拿出來再三宣導,或是在 code review 時來討論。

寫 code 要注意的細節這麼多,應該記不住吧?況且純肉眼純手工打造真是太辛苦了,有沒有辦法能讓開發者與審閱者皆大歡喜?

「用工具自動處理」。


開發者:小事情用工具解決,省時省力

當我在寫 code 時,有時還是會因個人習慣或疏漏而沒有遵守團隊規範,像是斷行或分號這樣的小細節,或是功力還沒到爐火純青的地步,需要一點指點什麼是 best practice 以避免潛在風險和錯誤。

這些小細節可以靠工具幫忙微調,不用在寫程式時純手工做到完美,良好的建議也能有工具提示。

審閱者:好的程式碼要能看得開心

當 review 程式碼時,我的步驟會是

順道一提,這裡有兩篇文章,是我在閱讀 Google 的「The CL author’s guide to getting through code review」後分別寫下開發者篇審閱者篇的筆記還有自己的小感想,歡迎討論。

除了第一和第二和最後這個必須人工處理外,第三「排版」和第四「找錯、建議」幾乎都能用工具幫忙,不需要 reviewer 純肉眼看完

怎麼做呢?接下來來看幾個工具。

Linter

linter 是主要能幫我們解決以上問題的工具,處理寫程式時容易忽略的細節並自動修正以符合規範,減低 code base 混亂程度而能維持一致性,找出潛在錯誤,減少 code review 的負擔,甚至是給予 best practice 的建議。順道一提,當團隊有重構或調整規範時,linter 能幫我們完成這樣的巨大工程。

linter 有 JSLintJSHintESLint 多種選擇,當提到 linter/linting tool 這些關鍵字時,大多會是指 ESLint,原因是 ESLint 更貼近目前前端開發環境與習慣而被廣泛使用,像是客製化程度更高、報錯提示更加友善、文件說明與範例足夠詳細。

ESLint 的安裝、基本設定和說明,可參考教學文

yarn add eslint --dev # 安裝
yarn eslint --init # 初始化
yarn add eslint-config-airbnb --dev # 安裝稍後用到的 coding style

由於期望它能做到檢查並自動修正錯誤,因此稍後的選項會選擇 To check syntax, find problems, and enforce code style,之後再依序根據專案選擇適合的項目,像是 JavaScript modules (import/export)ReactBrowserUse a popular style guideAirbnbJSON

關於 ESLint 要選哪一種 style guide?這篇文章針對 Google、Airbnb 跟 Standard 三種常用的 coding style 做了比較,選擇專案或團隊適合的方案即可。如果團隊有自身考量,可在 config 中設定或覆寫。

執行 ESLint 來檢查特定檔案,或特定資料夾底下的特定檔案類型。

./node_modules/.bin/eslint ./src/App.js # 檢查特定檔案
./node_modules/.bin/eslint --ext .js ./src # 檢查 src 資料夾底下的 js 檔案

這個指令可以放在 package.json 的 scripts 裡面,方便之後重複執行和整合 CI/CD 流程。

"scripts": {
  "lint": "./node_modules/.bin/eslint --ext .js ./src",
  "lint:fix": "./node_modules/.bin/eslint --fix --ext .js ./src"
}

檢視設定檔 .eslintrc.json,想要做一些客製化的設定。

例如,設定以下規則:

rules 可以這樣設定。

"rules": {
  "indent": [2, 2], // text indent 為空白 2 
  "max-len": [2, { "code": 150 }], // 一行不可超過 150 
  "react/jsx-max-props-per-line": [2, { "maximum": { "single": 3, "multi": 1 } }], // React 元件的 props 超過 3 個時強制斷行
  "react/jsx-wrap-multilines": 2, // 元件 return 若多行要有小括號刮起來
  "semi": 2 // 結尾要有分號
}

當有程式碼這樣寫的時候…讓人煩躁啊,排版完全不行…

const renderPhotos = (list) => {
  const PREFIX = 'Hello!'

  return <div>
    {
        list.map(({
          comment,
          imageId,
          photoUrl,
        }) => (
          <img alt={`${PREFIX}_${comment}`} key={imageId} src={photoUrl} width={300} />
        ))
      }
    </div>
  ;
};

執行 yarn lint 就會報錯。

加上 --fix 自動 fix,例如:yarn lint --fixyarn lint:fix

修正完畢,身心舒暢。

const renderPhotos = (list) => {
  const PREFIX = 'Hello!';

  return (
    <div>
      {
        !!list.length && list.map(({
          comment,
          imageId,
          photoUrl,
        }) => (
          <img
            alt={`${PREFIX}_${comment}`}
            key={imageId}
            src={photoUrl}
            width={300}
          />
        ))
      }
    </div>
  );
};

設定好了 linter,接下來可能會遇到幾個問題…

[Q1] 第一個問題是,即使有人告訴你這樣寫程式不對,終究還是有人會推上去,對吧! 那就鎖住不要讓他 commit & push 就好了?可以吧?

[Q2] 第二個問題是,若專案並非一開始就設定好規範 (可能原本毫無規範?),或是規範有所更動 (像是換了 leader 而決定空白從 2 個變成 4 個),我們要一次改完,還是修改某個功能某個檔案時,再做調整呢?

在探討這兩個問題前,先來看兩個工具 husky 和 lint-staged。

Husky + Lint-Staged

接下來要用 husky 來處理觸發 linter 的時機,以及用 lint-staged 來處理被修正的範圍

husky 是能在 git 指令執行前,做些事情的鉤子 (git hooks),像是整理程式碼、跑測試等,目的是當檢查項目不通過時,就不要 commit 程式碼、搞壞 code base。

lint-staged 用於指定檢查範圍,只針對有變動的檔案而非整個專案做修改,或是根據檔案類型分別設定指令。

yarn add husky lint-staged --dev # 安裝
npx husky-init # 初始化

此時會在 .husky/pre-commit 看到簡單的 pre-commit hook,修改最後一行如下。

#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"

npx lint-staged

並在 package.json.lintstagedrc.json 做設定,告訴鉤子要在 commit 時幫忙做 lint 並自動修正。

"lint-staged": {
  "*.{js}": [
    "eslint --fix"
  ]
},

當修改檔案後,想要 commit 時就會幫我們自動修正然後完成 commit,不能修正的像是語法嚴重錯誤或要刪除程式碼,無法通過 lint 時,就會停止 commit 讓程式碼留在 staged 階段。

針對前面提到的 Q1…面對有人不分青紅皂白就把扣丟上來的問題…利用 husky + lint-staged 即可搞定不良的程式碼不要推進 code base 的問題!

但如果有人用 --no-verify 這樣暴力破解就要拖出去打了。

git commit -m "test" --no-verify

如上範例,設定在 commit 時觸發 linter 機制並稍後 push code,但若加上 --no-verify 則會讓 git commit 繞過 pre-commit 機制。文件可參考這裡那裡

或是在個別檔案的開頭對 eslint 用註解設定 /* eslint-disable */,企圖跳過某個檔案,不做檢查。

這都是非常不好的作法,千萬不要用!建議先檢視報錯訊息,看不懂可以查查文件、和同事討論。

更新時機:一次改完 vs 分批修改

前面 Q2 提到若專案並非一開始就設定好規範,或是規範有所更動,我們要一次改完,還是修改某個功能某個檔案時,再做調整呢?分別討論如下,注意,這裡的好壞並無絕對,考量 當下感受 aka DX 開發者快樂程度 或耗工與否等多方因素,選擇專案或團隊適合的方案即可。

一次改完

「一次改完」的作法是全部檔案都跑一次 linter、測試,然後 commit 即可。

分批修改

「分批修改」是指當修改某個功能某個檔案時,再做調整。可能的進行方式是先推一個關於 linter 的設定的 PR 進 master,然後再讓其他 branch 把這些設定拉下來,接著,修改 code 上通常會有兩種方式:(1) 使用 linter + husky + lint-staged 指令修改;或 (2) 整合在 VSCode 裡面,在改檔案的時候,順便一起做調整。幾乎都是兩者混用。

關於 (2) 整合在 VSCode 裡面,若是想在改專案的特定檔案的時候,順便一起做調整,可以在 VSCode 安裝 ESlint 套件,並在專案加入 VSCode 的設定檔 .vscode > settings.json,這樣就可以在存檔時自動修正。

設定檔範例。

{
  "editor.codeActionsOnSave": { "source.fixAll.eslint": true },
  "editor.formatOnSave": true
}

說明:

個人心得…關於新手不要使用 ESLint 這樣的工具做自動修正,有些建議不用的原因是新手可能會因此被慣壞而無法深入了解問題所在,失去學習的機會。我個人認為,的確 linter 給的提示資訊,很值得一看,能讓新手老手都能更精進技術,若直接被修復而略過就太可惜了,況且有做好 pre-commit 機制的確可以擋住不良的程式碼進入 code base,不必擔心存檔沒修正就搞壞。只是,先不考慮工作上的專案鬆緊度,若新手想藉此學東西,是很棒的值得鼓勵沒錯,但想要強調的是,功力的增進我會更偏好系統式的學習,像是看書和教學影片,而不是花時間在零碎的提示上,提示只是提醒,不能當成真正的養分來源。

總結

基本工作流程如下圖,實踐 commit code 時幫忙做些檢查然後預備推扣會是這樣進行的。

優雅整合 Linter、Husky 與 Lint-Staged:寫扣看扣皆大歡喜的密技

說明流程:

關於要在 commit 還是 push 階段做這個流程?有兩派討論… 有些團隊會在 push 階段才做這個流程,考量是開發者在做 commit 前必須自行先跑過 linter 和各種測試,確認無誤才能 commit,這絕對是個人修為、必須要做到的。但人非聖賢,或團隊總有小白?那麼就會產生這樣的混亂…由於程式碼已經 commit 準備 push,若有錯誤就要修改程式碼,然後再次跑整個流程以重新 commit,此時無法確保這 PR 的每個 commit 都是正確無誤的 (在 PR 上就會看到有些 commit 是紅叉叉有些是綠勾勾),謹慎者會 rebase commit 再重新 commit 和 force push code。若是在 PR 階段,rebase 後 force push code 是新的 commit hash,這會失去原先的 PR review 紀錄,對 reviewer 來說是比較麻煩的,在某個程度上失去協助開發者與審閱者最大化省時省力的效益。因此個人是比較偏好在 commit 時跑這樣的流程的,不過還是強調個人偏好不見得是最佳解,選擇專案或團隊適合的方案即可。


提到團隊合作…

關於讓團隊合作更順暢的秘訣,推薦閱讀-那些理所當然,卻像空氣般重要的小事:談開發流程、程式架構與職涯發展 - PJ (陳柏融)

那些理所當然,卻像空氣般重要的小事:談開發流程、程式架構與職涯發展 - PJ (陳柏融)

截圖自 PJCHENder網頁開發咩腳

支持他就是為他買杯咖啡

**支持他就是為他買杯咖啡**


祝大家都能開開心心寫扣看扣噢!

優雅整合 Linter、Husky 與 Lint-Staged:寫扣看扣皆大歡喜的密技


linter husky lint-staged ESLint 團隊合作 team work code review best practice WebConf Taiwan git GitHub 職涯