最近在研究代码 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 配置