最近在研究代碼 Lint
相關的內容,業界比較經常使用的方案是Husky
配合lint-staged
在代碼提交前進行Lint,防止將不規範的代碼提交到遠端。前端
對Husky
的工做原理很感興趣,花了點時間研究,藉此文作一下總結,但願對正在學習這塊內容的朋友有一些幫助。node
說「最佳實踐」可能有點不恰當,但我見過的大多數前端項目都是採用這種組合git
以Javascript爲例,要進行代碼Lint,主要有如下步驟:github
eslint
、husky
和lint-staged
,如何安裝不是本文重點,請自行學習.eslintrc.js
文件,配置eslint
,具體配置根據項目和團隊需求自行配置,可參見eslint文檔package.json
或者.huskyrc
文件中增長 husky 配置項"husky": {
"hooks": {
"pre-commit": "lint-staged"
}
},
複製代碼
"lint-staged": {
"**/*.js": "eslint"
},
複製代碼
到此,git commit時就會進行代碼校驗,而且只會校驗staged的文件。shell
ESlint
和lint-staged
不是本文的重點,請自行學習,本文重點關注Husky
的原理npm
Husky是如何在代碼提交時觸發代碼校驗的?在研究它的原理以前,須要介紹另一個概念:git hooks
,官方文檔的描述是:json
和其它版本控制系統同樣,Git 能在特定的重要動做發生時觸發自定義腳本。 有兩組這樣的鉤子:客戶端的和服務器端的。 客戶端鉤子由諸如提交和合並這樣的操做所調用,而服務器端鉤子做用於諸如接收被推送的提交這樣的聯網操做。 你能夠爲所欲爲地運用這些鉤子。bash
目前git支持17個hooks,都以單獨的腳本形式存儲在.git/hooks文件夾下:服務器
以一次commit爲例,會前後觸發pre-commit
、prepare-commit-msg
、commit-msg
和post-commit
等hooks。咱們能夠利用這些hooks作一些有趣的事。好比:咱們能夠利用pre-commit
進行代碼校驗,利用commit-msg
進行commit message的校驗,只要你懂得shell語法,固然你也可使用Perl、Ruby或者Python。markdown
另外,並不須要咱們建立全部hook腳本,只須要按需建立便可。
Husky
官方的描述是:
Git hooks made easy(讓git hooks變得簡單)
想象一個場景,好比在一個多人協做的團隊,你在.git/hooks
中建立了一些hooks,你但願共享給隊友,但.git/hooks
文件夾並不會提交到遠端,無奈只能拷貝。
Husky
就是爲了解決這個問題而生的,只須要簡單的配置,就能夠完成hook的工做,具體配置方法參見Husky
使用文檔,以package.json
爲例:
// package.json
"husky": {
"hooks": {
"pre-commit": "eslint"
}
},
複製代碼
在npm安裝Husky
時,Husky
會在項目的.git/hooks
文件夾下建立全部支持的hooks,另外還會建立 husky.local.sh
和husky.sh
兩個文件。其實每一個hook腳本的內容都同樣:
# pre-commit
#!/bin/sh
# husky
# Created by Husky v4.2.5 (https://github.com/typicode/husky#readme)
# At: 2020/8/3 上午11:25:21
# From: ...(https://github.com/typicode/husky#readme)
. "$(dirname "$0")/husky.sh"
複製代碼
咱們能夠看到僅僅是執行husky.sh
腳本,重點在husky.sh腳本中。
Husky是如何作到在安裝後建立hooks中的文件的? 實際上是用了npm scripts的install指令,當npm包在安裝完畢後會自動執行該指令下的腳本,具體可參加文檔。 經過查看Husky的package.json可知,指令爲:
node husky install
最終執行的是 ./lib/installer/bin
中的腳本,而 hooks 的建立邏輯在 ./lib/installer/hooks.js
中,有興趣的同窗能夠去看下源碼。
Husky的核心代碼都在husky.sh文件中:
# Created by Husky v4.2.5 (https://github.com/typicode/husky#readme)
# At: 2020/8/3 上午11:25:21
# From: ... (https://github.com/typicode/husky#readme)
debug () {
if [ "$HUSKY_DEBUG" = "true" ] || [ "$HUSKY_DEBUG" = "1" ]; then
echo "husky:debug $1"
fi
}
command_exists () {
command -v "$1" >/dev/null 2>&1
}
run_command () {
if command_exists "$1"; then
"$@" husky-run $hookName "$gitParams"
exitCode="$?"
debug "$* husky-run exited with $exitCode exit code"
if [ $exitCode -eq 127 ]; then
echo "Can't find Husky, skipping $hookName hook"
echo "You can reinstall it using 'npm install husky --save-dev' or delete this hook"
else
exit $exitCode
fi
else
echo "Can't find $1 in PATH: $PATH"
echo "Skipping $hookName hook"
exit 0
fi
}
hookIsDefined () {
grep -qs $hookName \
package.json \
.huskyrc \
.huskyrc.json \
.huskyrc.yaml \
.huskyrc.yml
}
huskyVersion="4.2.5"
gitParams="$*"
hookName="$(basename "$0")"
debug "husky v$huskyVersion - $hookName"
# Skip if HUSKY_SKIP_HOOKS is set
if [ "$HUSKY_SKIP_HOOKS" = "true" ] || [ "$HUSKY_SKIP_HOOKS" = "1" ]; then
debug "HUSKY_SKIP_HOOKS is set to $HUSKY_SKIP_HOOKS, skipping hook"
exit 0
fi
# Source user var and change directory
. "$(dirname "$0")/husky.local.sh"
debug "Current working directory is $(pwd)"
# Skip fast if hookName is not defined
# Don't skip if .huskyrc.js or .huskyrc.config.js are used as the heuristic could
# fail due to the dynamic aspect of JS. For example:
# `"pre-" + "commit"` or `require('./config/hooks')`)
if [ ! -f .huskyrc.js ] && [ ! -f husky.config.js ] && ! hookIsDefined; then
debug "$hookName config not found, skipping hook"
exit 0
fi
# Source user ~/.huskyrc
if [ -f ~/.huskyrc ]; then
debug "source ~/.huskyrc"
. ~/.huskyrc
fi
# Set HUSKY_GIT_STDIN from stdin
case $hookName in
"pre-push"|"post-rewrite")
export HUSKY_GIT_STDIN="$(cat)";;
esac
# Windows 10, Git Bash and Yarn 1 installer
if command_exists winpty && test -t 1; then
exec < /dev/tty
fi
# Run husky-run with the package manager used to install Husky
case $packageManager in
"npm") run_command npx --no-install;;
"npminstall") run_command npx --no-install;;
"pnpm") run_command pnpx --no-install;;
"yarn") run_command yarn run --silent;;
*) echo "Unknown package manager: $packageManager"; exit 0;;
esac
複製代碼
咱們提取其中幾個關鍵點來進行分析:
1、第一個關鍵點是經過 basename "$0" 獲取當前腳本的名稱,好比: pre-commit
,這一點很重要,後面的指令匹配都是圍繞這個名稱,後面內容中的 hookName 都以 pre-commit 爲例
hookName="$(basename "$0")"
複製代碼
2、帶二個關鍵點是 hookIsDefined 函數,它的原理就是經過grep
指令判斷各個配置文件中是否存在 pre-commit,
hookIsDefined () {
grep -qs $hookName \
package.json \
.huskyrc \
.huskyrc.json \
.huskyrc.yaml \
.huskyrc.yml
}
複製代碼
第三個關鍵點是 run_command 函數,做用就是用本地的husky-run指令執行hook,按照文章開頭的配置,就是執行eslint
。
npx --no-install husky-run pre-commit "$gitParams"
複製代碼
husky-run對應的執行腳本是.node_modules/husky/bin/run.js
,腳本內容也很簡單,就是調用了.node_modules/husky/lib/runner/bin.js
,而最終是調用了 .node_modules/husky/lib/runner/index.js
中的runCommand接口,在接口中起了子進程執行pre-commit中對應的腳本。
到此Husky
的原理介紹完畢,咱們進行一下總結:
package.json、.huskyrc、.huskyrc.json...
)讀取相應的 hook 配置