持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第3天,点击查看活动详情
大家好我是乔柯力,今天为大家介绍如何从工程化的角度用 husky 来配置项目,让团队所有人都按照既定规范来写代码和提交代码。
什么是 git hooks?
所谓的 git hooks 就是在 Git 的命令之前或之后执行一些脚本而已,最常用的两个是:
precommit
:定义在 commit 之前的脚本prepush
:定义 push 之前的脚本
这些脚本都被放在了 .git/hooks
隐藏目录内,以可执行文件的形式存在:
$ ls .git/hooks
applypatch-msg.sample pre-applypatch.sample pre-rebase.sample update.sample
commit-msg.sample pre-commit.sample pre-receive.sample
fsmonitor-watchman.sample pre-merge-commit.sample prepare-commit-msg.sample
post-update.sample pre-push.sample push-to-checkout.sample
这些以 .sample 结尾的是示例文件,不会真正执行的。我们可以创建一个真实的 pre-commit 脚本,里面写上一行代码 date
,然后在本地随便创建一个 README.md 文件,然后 commit,你会发现输出了当前的日期和时间,说明 date
命令生效了。
$ git commit -am 'feat: 测试pre-commit hook'
2022年10月13日 星期三 23时44分57秒 CST
[master (root-commit) 1c47f08] feat: 测试pre-commit hook
1 file changed, 1 insertion(+)
create mode 100644 README.md
我们把刚才的 date 去掉,换成 abcd 不存在的命令,然后再随便修改一点 README.md 代码,重新提交一次:
$ git commit -am 'feat: 测试会执行失败的 pre-commit hook'
.git/hooks/pre-commit: line 1: abcd: command not found
你会发现,commit 行为被拦截了,因为前置的 pre-commit 脚本执行报错。所以我们可以利用 Git 的这个特性来做一些很有价值的事情。
什么是 husky?
husky 是基于 git hooks 实现的一套共享 hooks 脚本的解决方案,便于团队协作。我们首先在项目中安装 husky 依赖:
$ yarn add husky --dev
yarn add v1.22.17
info No lockfile found.
[1/4] 🔍 Resolving packages...
[2/4] 🚚 Fetching packages...
[3/4] 🔗 Linking dependencies...
[4/4] 🔨 Building fresh packages...
success Saved lockfile.
success Saved 1 new dependency.
info Direct dependencies
└─ husky@8.0.1
info All dependencies
└─ husky@8.0.1
然后创建 husky 配置:
$ npx husky install
husky - Git hooks installed
这个命令执行完毕之后,项目中会多出一个 .husky 的目录:
.husky
└── _
├── gitignore
└── husky.sh
这个目录是干什么用的呢?上面说过,.git/hooks
是默认的保存 hooks 脚本的目录,但是用户可以自己修改,Git 提供了 core.hooksPath
选项让用户自定义脚本目录,husky 就是利用了这个原理,当你查看配置的时候,会发现配置文件中多了一行:
core.hookspath=.husky
所以 husky 把 hooks 脚本目录改成了 .husky 目录,好处就是可以跟随代码一起提交到远程,大家一起遵循 hooks 约束。
到这里还有一个问题没解决,就是虽然你在本地创建了 husky 配置,修改了 hooks 目录,那如何确保大家都做这个事情呢?难道一个个通知吗?这肯定不现实。这时,就用到了 npm 提供的 prepare hook,我们输入下面的命令在 package.json 中添加 prepare 脚本:
npm set-script prepare "husky install"
这里用到了 npm 提供的 set-script 命令,意思是设置脚本命令,这个时候再看 package.json 文件会发现自动添加了一行 prepare 脚本用于执行 husky install
命令:
"scripts": {
"prepare": "husky install"
}
当你的同事 pull 下来仓库代码 install 完依赖之后,就会自动执行 prepare 脚本,于是 husky 就会被自动启用了。
接下来我们增加一个 pre-commit 的 hook:
$ npx husky add .husky/pre-commit "date"
husky - created .husky/pre-commit
这个时候 .husky 目录里面就多了一个 pre-commit 的脚本:
.husky
├── _
│ └── husky.sh
└── pre-commit
里面的代码是:
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
date
意思是在 commit 之前,先执行这里面的脚本,如果脚本返回 0 则允许提交,脚本返回 1 则不允许提交,我们可以手动执行一下这个脚本看看:
$ bash .husky/pre-commit
2022年10月13日 星期三 23时30分00秒 CST
可以看到顺利执行了,也没有出错,像这种代码就不会阻止 commit 行为。我们可以继续在这个脚本里面添加一些其他代码,例如:
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
date
node xxx.js
npm run xxx
说了这么多,究竟能干些什么具体的事情呢?接下来通过案例向大家展示:
案例分析
限制提交时间
假如有这么一个需求:为了防止内卷,只允许在工作日的 9:00~18:00 之间提交代码。 我们就可以设计这么一个方案了,首先在 .husky/pre-commit 脚本里面添加一行代码:
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
node scripts/check-commit-time.js
然后创建 scripts/check-commit-time.js 文件,写下时间检查逻辑:
const now = new Date()
const week = now.getDay()
const hour = now.getHours()
const validWeek = week >= 1 && week <= 5
const validHour = hour >= 9 && hour < 18
if (validHour && validWeek) return
throw new Error('不在可提交时间段之内,拒绝内卷,从我做起!')
然后 commit 的时候,会先执行这段 js 代码,如果不在可提交时间段内,就会报错:
$ git commit -m "feat: 测试检查提交时间"
/Users/keliq/awesome-app/scripts/check-commit-time.js:7
throw new Error('不在可提交时间段之内,拒绝内卷,从我做起!')
^
Error: 不在可提交时间段之内,拒绝内卷,从我做起!
at Object.<anonymous> (/Users/keliq/awesome-app/scripts/check-commit-time.js:7:7)
at Module._compile (node:internal/modules/cjs/loader:1101:14)
at Object.Module._extensions..js (node:internal/modules/cjs/loader:1153:10)
at Module.load (node:internal/modules/cjs/loader:981:32)
at Function.Module._load (node:internal/modules/cjs/loader:822:12)
at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:79:12)
at node:internal/main/run_main_module:17:47
husky - pre-commit hook exited with code 1 (error)
如果在指定时间段内,则可以顺利提交:
$ git commit -m "feat: 测试检查提交时间"
[master (root-commit) 4f8a4df] feat: 测试检查提交时间
1 file changed, 13 insertions(+)
create mode 100644 package.json
拒绝不规范的 commit message
为了避免大家在提交时随意写 commit message,可以使用 validate-commit-msg 来进行约束,强制要求符合以下提交规范:
<type>(<scope>): <subject>
首先我们用 husky 添加一个新的 commit-msg hook:
$ npx husky add .husky/commit-msg 'npm run validate-commit-msg'
然后在 package.json 中添加一行 script:
$ npm set-script "validate-commit-msg" "validate-commit-msg"
如果用不规范的提交消息进行测试,会发现报错了:
$ git commit -m "检查commit msg"
> awesome-app@1.0.0 validate-commit-msg
> validate-commit-msg
INVALID COMMIT MSG: does not match "<type>(<scope>): <subject>" !
检查commit msg
husky - commit-msg hook exited with code 1 (error)
当消息格式正确时可以顺利提交:
$ git commit -m "feat: 检查commit msg"
> awesome-app@1.0.0 validate-commit-msg
> validate-commit-msg
[master 09ee0aa] feat: 检查commit msg
1 file changed, 7 insertions(+), 5 deletions(-)
提交之前自动格式化代码
这里需要引入两个工具:
- prettier:用于格式化代码
- lint-staged:检查缓存区的文件是否符合规范
因此,首先安装依赖:
$ yarn add prettier lint-staged --dev
我们在项目中新建 .prettierrc 文件,设置格式化的规则,例如:
{
"singleQuote": true,
"semi": false,
"printWidth": 400,
"arrowParens": "avoid",
"trailingComma": "es5",
"proseWrap": "never",
"quoteProps": "consistent"
}
具体字段的含义这里不做详细解释,感兴趣的同学可以参阅官方文档。然后随便写点代码到 src/index.js 文件中,例如第一行代码故意多加一个分号:
const nickname = 'keliq';
console.log(nickname)
然后用下面的命令测试 prettier 的效果:
$ npx prettier --check src
Checking formatting...
[warn] src/index.js
[warn] Code style issues found in the above file. Forgot to run Prettier?
发现出现了 warn 提示,说明代码格式不正确,我们把第一行末尾的分号去掉之后再运行就 OK 了:
$ npx prettier --check src
Checking formatting...
All matched files use Prettier code style!
上面的代码只是检测,那能不能当检查到代码格式不正确时,自动进行格式化呢?当然是可以的,只需要运行下面的命令即可:
$ npx prettier --write src
你可能会问,既然 perttier 这么强大,那还要 lint-staged 干啥?其实 lint-staged 最重要的功能是:只处理暂存区的文件!这是 prettier 做不到的。假如一个大项目有成百上千个文件,每次提交之前全量检查一下格式,是很耗费时间的,聪明的做法是只检查暂存区的代码即可,我们在 package.json 中添加下面的代码:
{
"lint-staged": {
"*": "prettier --write src"
}
}
然后运行:
$ npx lint-staged
✔ Preparing lint-staged...
✔ Hiding unstaged changes to partially staged files...
✔ Running tasks for staged files...
✔ Applying modifications from tasks...
✔ Restoring unstaged changes to partially staged files...
✔ Cleaning up temporary files...
你会发现暂存区的代码已经被格式化了,所以只需要 pre-commit 脚本中运行 lint-staged 即可:
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
node scripts/check-commit-time.js
npx lint-staged
其他
如果用 sourcetree 或者 fork 之类的可视化工具,提交的时候可能会报错:
.husky/pre-commit: Line 4 npm: command not found
在命令行中是可以正确触发钩子的,这是因为 GUI 工具找不到 husky 钩子中需要使用的命令,解决办法就是找到命令所在的路径,然后添加到 ~/.huskyrc
文件中,例如获取 npm 所在的目录:
$ where npm
/usr/local/bin/npm
我们可以将该目录添加到环境变量中:
$ echo 'export PATH="/usr/local/bin/:$PATH"' >> ~/.huskyrc
另外,git hooks 的约束是防君子不妨小人的,因为可以通过命令绕过:
$ git commit -m "yolo!" --no-verify
如果哪天觉得约束不爽了想卸载掉 husky 也很简单:
$ yarn remove husky # 移除依赖
$ rm -rf .husky && git config --unset core.hooksPath # 删除目录重置core.hooksPath