優雅整合 Linter、Husky、Lint-Staged:寫扣看扣皆大歡喜的密技
29 Aug 2023大部份的工程師都是謹慎 龜毛 的,除了桌上的物品要放整齊外,寫 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 程式碼時,我的步驟會是
- 第一,了解規格和需求,以及閱讀和檢查文件。審閱者可能需要開發者的文件、會議記錄或 demo 等方式說明這次 code change 的 PR 的目的、實作方向。 關於 PR 文件要怎麼寫,這裡有個簡單的範例和說明可供參考。
- 第二,跑一跑確認是不是能正常運作,如果功能不能正常運作、不符合設計和測試規格和需求,不論程式碼寫得多麽精美都要 request change
踢回去。 - 第三,看程式碼的第一步是觀察排版,像是空多少空白、怎麼斷行、命名這種 coding style 是否符合團隊規範的狀況。
- 第四,確認有沒有潛在的錯誤,像是語法誤用、刪除多餘的程式碼、是否易於維護,是否有足夠的測試、註解與補充文件。
- 最後,根據設計和測試規格再次細細推敲程式碼是否合乎需求,理解這樣的實作是否能讓功能正常運作,有沒有任何沒考慮到的細節會造成重大問題。通常資深同事對於系統會有比較完善的了解,多想一下能減少日後被 call 來救火的機率。
順道一提,這裡有兩篇文章,是我在閱讀 Google 的「The CL author’s guide to getting through code review」後分別寫下開發者篇與審閱者篇的筆記還有自己的小感想,歡迎討論。
除了第一和第二和最後這個必須人工處理外,第三「排版」和第四「找錯、建議」幾乎都能用工具幫忙,不需要 reviewer 純肉眼看完。
怎麼做呢?接下來來看幾個工具。
Linter
linter 是主要能幫我們解決以上問題的工具,處理寫程式時容易忽略的細節並自動修正以符合規範,減低 code base 混亂程度而能維持一致性,找出潛在錯誤,減少 code review 的負擔,甚至是給予 best practice 的建議。順道一提,當團隊有重構或調整規範時,linter 能幫我們完成這樣的巨大工程。
linter 有 JSLint、JSHint 和 ESLint 多種選擇,當提到 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) → React → Browser → Use a popular style guide → Airbnb → JSON。
關於 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
,想要做一些客製化的設定。
例如,設定以下規則:
- text indent 為空白 2 格。
- 一行不可超過 150 字。
- 結尾要有分號。
- React 元件的 props 超過 3 個時強制斷行。
- 元件 return 若多行要有小括號刮起來。
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 --fix
或 yarn 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 即可。
- 好處是
- 本次 PR / CL 的更動不牽涉任何功能上的調整,目的單純,測試有通過或簡單跟團隊過一下就好
痛一次而已 - 對審閱者來說易於理解、容易 review,因為比對的程式碼都會是已經調整過的樣子了,不用考慮格式問題,能專注在其他更重要的地方上。
- 所有檔案都會被修改到。
- 本次 PR / CL 的更動不牽涉任何功能上的調整,目的單純,測試有通過或簡單跟團隊過一下就好
- 壞處是
- 正在開發的 branch 或即將 merge 的 PR 都必須解決衝突問題,需要花點時間重新整理程式碼和測試。開發團隊的其餘成員必須謹慎考量手上的工作量、專案時程、package 是否有 dependency 的疑慮。
分批修改
「分批修改」是指當修改某個功能某個檔案時,再做調整。可能的進行方式是先推一個關於 linter 的設定的 PR 進 master,然後再讓其他 branch 把這些設定拉下來,接著,修改 code 上通常會有兩種方式:(1) 使用 linter + husky + lint-staged 指令修改;或 (2) 整合在 VSCode 裡面,在改檔案的時候,順便一起做調整。幾乎都是兩者混用。
- 好處是
- 正在開發的 branch 或即將 merge 的 PR 沒有要解衝突、重新整理程式碼和測試的問題,自己的 code 自己調整,對開發者來說能減少衝突、避免錯誤。
- 壞處是
- 多次修改
可能會痛很多次 - 對審閱者來說,code change 包含格式調整和需求變更,需要花較多時間 review。
- 有些檔案可能因為沒有任何更新,直到專案 archieve 終究都沒被改到。
- 多次修改
關於 (2) 整合在 VSCode 裡面,若是想在改專案的特定檔案的時候,順便一起做調整,可以在 VSCode 安裝 ESlint 套件,並在專案加入 VSCode 的設定檔 .vscode > settings.json
,這樣就可以在存檔時自動修正。
設定檔範例。
{
"editor.codeActionsOnSave": { "source.fixAll.eslint": true },
"editor.formatOnSave": true
}
說明:
"editor.codeActionsOnSave": { "source.fixAll.eslint": true }
在儲存檔案時讓 ESLint 自動修正。"editor.formatOnSave": true
在儲存檔案時利用 formatter 自動格式化程式碼,例如:Prettier。整合 VSCode + Prettier + ESLint 可參考這裡。由於格式化上並無特別需求,因此基本上我大多只用 ESLint。
個人心得…關於新手不要使用 ESLint 這樣的工具做自動修正,有些建議不用的原因是新手可能會因此被慣壞而無法深入了解問題所在,失去學習的機會。我個人認為,的確 linter 給的提示資訊,很值得一看,能讓新手老手都能更精進技術,若直接被修復而略過就太可惜了,況且有做好 pre-commit 機制的確可以擋住不良的程式碼進入 code base,不必擔心存檔沒修正就搞壞。只是,先不考慮工作上的專案鬆緊度,若新手想藉此學東西,是很棒的值得鼓勵沒錯,但想要強調的是,功力的增進我會更偏好系統式的學習,像是看書和教學影片,而不是花時間在零碎的提示上,提示只是提醒,不能當成真正的養分來源。
總結
基本工作流程如下圖,實踐 commit code 時幫忙做些檢查然後預備推扣會是這樣進行的。
說明流程:
- commit code 時 (注意不是 push code),會觸發 husky 幫忙執行預定要做的檢查項目,像是 linter 和跑測試。想知道自己專案要做什麼可檢查設定檔
.husky/pre-commit
或package.json
中的 husky 設定。 - 如果檢查項目都通過,就會做 commit,反之沒過就會讓程式碼留在 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 (陳柏融)
截圖自 PJCHENder網頁開發咩腳
祝大家都能開開心心寫扣看扣噢!