前言 前段時間,部門的前端項目遷移到 monorepo 架構,筆者在其中負責跟 git 工作流相關的事情,其中就包括 git hooks 相關的工程化的實踐。用到了一些常用的相關工具如 husky、lint-staged、commitizen、commit-lint 等,以此文記錄一下整個的實踐過程 ...
前言
前段時間,部門的前端項目遷移到 monorepo 架構,筆者在其中負責跟 git 工作流相關的事情,其中就包括 git hooks 相關的工程化的實踐。用到了一些常用的相關工具如 husky、lint-staged、commitizen、commit-lint 等,以此文記錄一下整個的實踐過程和踩過的坑。
註意:下文中的例子以及命令都是基於 Mac OS,如果你是 windows 用戶,也不用擔心,文中也會闡述大致原理和運行邏輯,對應的 windows 命令可以推理得知。
Git Hooks
Git Hooks 是什麼
大多數同學應該都對 git hooks
相當瞭解,但是筆者還是想在這裡詳細解釋一下。
首先是 hook
,這其實是電腦領域中一個很常見的概念,hook
翻譯過來的意思是鉤子或者勾住,而在電腦領域中則要分為兩種解釋:
- 攔截消息,在消息到達目標前,提前對消息進行處理
- 對特定的事件進行監聽,當某個事件或動作被觸發時也會同時觸發對應的
hook
也就是說hook
本身也是一段程式,只是它會在特定的時機被觸發。
理解了 hook
這一概念,那麼 git hooks
也就不難理解了。git hooks 就是在運行某些 git 命令時,被觸發的對應的程式。
在前端領域,鉤子的概念也並不少見,比如 Vue 聲明周期鉤子、React Hooks、webpack 鉤子等,說到底它們都是在特定的時機觸發的方法或者函數
常見的 Git Hooks 有哪些
git hooks 分為兩類
客戶端 hook
pre-commit
hook, 在運行git commit
命令時且在 commit 完成前被觸發commit-msg
hook, 在編輯完 commit-msg 時被觸發,並且接受一個參數,這個參數是存放當前 commit-msg 的臨時文件的路徑pre-push
hook, 在運行git push
命令時且在 push 命令完成前被觸發
服務端 hook
pre-receive
在服務端接受到推送時且在推送過程完成前被觸發post-receive
在服務端接收到推送且推送完成後被觸發
這裡只列舉了一部分,更多的 git hooks 詳細信息見官方文檔
在本地 git 倉庫中的 .git/hooks
文件夾中也可以看到常用的 git hooks 示例
從圖中可以看到,預設的 git hooks 都是 shell 腳本,只需要將 git hooks 的示例文件的 .sample 擴展名去掉,那麼示例文件即可生效。
一般來說,在前端工程中應用 git hooks 都是運行 javaScript 腳本,就像這樣
#!/bin/sh
node your/path/to/script/xxx.js
或者是這樣
#!/usr/bin/env node
// javascript code ...
原生的 Git Hooks 的缺陷
原生的 git hooks 有一個比較大的問題是 .git
文件夾下的內容不會被 Git 追蹤。這就表示,無法保證讓一個倉庫中所有的成員都使用同樣的 git hooks,除非倉庫的所有成員都手動同步同一份 git hooks,但這顯然不是個好辦法。
Husky
Husky 的使用
- 安裝 husky
pnpm install husky --save-dev
- husky 初始化
npx husky install
- 設置 package.json 的 prepare。來保證 husky 可以正常運行
npm set-script prepare "husky install"
- 添加 git hooks
npx husky add .husky/${hook_name} ${command}
husky install 命令做了什麼
事實上,husky install
命令是解決 git hooks 問題的關鍵
- 第一步: husky install 會在項目根目錄下創建
.husky
以及.husky/_
文件夾(文件夾也可以自定義),然後在.husky/_
文件夾下創建husky.sh
腳本文件。 這個文件的作用就是保證通過 husky 創建的腳本能夠正常運行,它的實際應用的地方後面會講到。更多關於這個腳本的討論可以看這裡 github issue。 - 第二步: husky install 會運行
git config core.hooksPath ${path/to/hooks_dir}
,這個命令用來指定 git hooks 的路徑,此時觀察項目下.git/config
文件, [core] 下麵會多出一條配置:hooksPath = xxx
。當 git hooks 被某些命令觸發時,Git 會運行core.hooksPath
指定的文件夾下的 git hook。
更多關於 husky 的配置、命令相關文檔,看這這裡
值得註意的是 core.hooksPath
是 Git v2.9 推出的新特性,而 Husky 也是在 v6 版本開始使用 core.hooksPath
這個特性。在這之前的版本,Husky 會直接覆蓋 .git/hooks
文件夾下所有的 hook,來使通過 Husky 配置的 hooks 生效。另外,在配置了 core.hooksPath
後 Git 會忽略 .git/hooks
文件夾下的 git hooks
husky add 命令做了什麼
當運行如下命令
npx husky add .husky/pre-commit npx eslint
.husky 目錄下會新增一個 pre-commit 文件,文件內容為
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
npx eslint
此時已經成功添加了一個 pre-commit
git hook,這個腳本會在運行 git commit 命令時執行。
在腳本的第二行,引用了上面所說的 .husky.sh
文件,也就是說通過 husky 創建的 git hook 在被觸發時,都會執行這個腳本。
梳理一下,husky 是如何解決原生的 git hooks 的問題的,首先前面已經提到了原生 git hooks 主要的問題是 git 無法跟蹤 .git/hooks 下的文件,但是這個問題已經被 git core.hooksPath 解決了,那麼新的問題就是,開發者仍然需要手動設置 git core.hooksPath。 husky 在 install 命令中幫助我們設置了 git core.hooksPath,然後在 package.json 的 scripts 中添加 "prepare": "husky install"
,這樣每次安裝依賴的時候就會執行 husky install
,因此就可以保證設置的 git hooks 可以被觸發了。
常用的 git 相關工具庫
lint-staged
在 pre-commit hook 中,一般來說都是對當前要 commit 的文件進行校驗、格式化等,因此在腳本中我們需要知道當前在 Git 暫存區的文件有哪些,而 Git 本身也沒有向 pre-commit 腳本傳遞相關參數,lint-staged
這個包為我們解決了這個問題,lint-staged 的文檔中第一句這樣說道:
Run linters against staged git files and don't let