前言
git
我们应该不陌生了,几乎每天工作都会用到。
今天要为大家介绍的是 git
里面的 hooks
,也就是 git
钩子。
git
钩子是什么呢?它可以用来干什么呢?为什么说它是强大的武器呢?带着这些问题,我们一起来看看, git
钩子它到底是个啥。
Git Hooks 简介
Git hooks
是 git
版本控制系统的一个功能,它允许我们在特定的 git
事件发生时执行自定义脚本。
这些事件可以是提交 (commit)
、推送 (push)
、合并 (merge)
等等。通过使用 Git hooks
,我们可以在这些事件发生前或发生后执行特定的操作,比如自动化测试、代码风格检查、部署到服务器等等。
Git hooks
是存储在 .git/hooks
目录下的可执行文件,这些文件的名称对应着不同的 git
事件。当 git
执行相关事件时,相应名称的钩子脚本就会被执行。
.git
是一个隐藏文件夹,在 mac
或者 linux
系统下,可以使用 ls -a
来查看,进入到.git/hooks
目录下,可以看到有如下文件:
下面详细介绍一下 git
系统中常用钩子的执行时机以及用途:
pre-commit
:在执行提交(commit)
之前运行。通常用于执行代码风格检查、静态代码分析、单元测试等操作,以确保提交的代码质量。prepare-commit-msg
:在提交(commit)
消息被编辑之前运行。通常用于修改或扩展提交消息,例如添加自动化生成的信息、验证提交消息格式等。commit-msg
:在提交(commit)
消息被创建后,但提交动作尚未完成时运行。通常用于验证提交消息的格式、内容等是否符合规范。post-commit
:在提交(commit)
完成后运行。通常用于发送通知、更新文档、执行某些特定的后续处理等操作。pre-rebase
:在执行变基(rebase)
操作之前运行。通常用于执行一些预检查,例如确保变基操作不会产生冲突或导致代码质量下降。post-checkout
:在检出(checkout)
完成后运行。通常用于执行一些与工作目录切换相关的操作,例如更新依赖、清理临时文件等。post-merge
:在合并(merge)
完成后运行。通常用于执行一些与合并操作相关的操作,例如重新构建项目、更新子模块等。pre-push
:在执行推送(push)
之前运行。通常用于执行一些预检查,例如运行测试、检查代码质量等。pre-receive(服务端钩子)
:在远程仓库接收到推送(push)
之前运行。通常用于执行一些服务端的预检查,例如验证提交的代码是否符合特定规范。update(服务端钩子)
:在推送(push)
到远程仓库但尚未完成更新时运行。通常用于执行一些与分支更新相关的操作,例如验证提交的代码是否符合特定规范、是否有权限更新等。post-receive(服务端钩子)
:在远程仓库接收到推送(push)
并完成更新后运行。通常用于执行一些服务端的后续处理,例如自动化部署、更新文档等。
Husky 工作流原理揭秘
在大致了解了这些钩子的执行时机以及作用之后,我们来学习一个在前端工程化中常用到代码规范技术选型: Husky+lint-staged+eslint
。
首先介绍一下这套技术是用来干嘛的——主要用来在提交代码的时候自动触发 eslint
代码扫描,用来校验并统一整个项目的代码规范。
下面分别介绍这三个工具在这套流程中起到的作用:
eslint
:这个大家都比较熟悉,主要是用来做代码扫描的lint-staged
:是一个用于在提交前运行Linter
的工具,它只会检查即将提交的文件,而不是整个项目。Husky
:允许我们自定义一些git
钩子
所以整个流程就是,使用 Husky
来自定义一些 git
钩子,然后配置 lint-staged
调用 eslint
去扫描代码,此时仅仅扫描待提交的文件而不是整个项目。
husky是怎么注入钩子的
首先我们先来安装一下 husky
, npm i husky --save-dev
,这里安装的是 ^9.0.11
这个版本,然后根据官方文档的提示,我们执行一下 npx husky init
。
此时会发现项目下多了一个 .husky
文件夹,以及 package.json
中多了一条命令。
当我们执行npx husky init
的时候,实际上执行了下面的这段代码。
它调用了 index.mjs
,以及在 .husky
目录下新建了一个 pre-commit
文件。我们先来看 index.mjs
。
下面重点来看这段代码
let { status: s, stderr: e } = c.spawnSync('git', ['config', 'core.hooksPath', `${d}/_`])
它主要执行了git config core.hooksPath ${d}/_
这个系统调用,这个系统调用的意思是,在执行 git
钩子时,不去调用.git/hooks目录下,而去调用 ${d}/_
这个目录,而这个目录就是 .husky
下的 _
目录
数组 l
主要是一些 git
钩子的名称, pre-commit
、 commit-msg
等,然后在 .husky
目录下新建了一个 _
文件夹,以及在这个文件夹下新建了几个钩子文件,这几个钩子文件的内容都填入了下面的内容,下面的内容意思是当调用这个脚本的时候,调用同目录下的 h
脚本。
#!/usr/bin/env sh
. "${0%/*}/h"
也就是说,当我们 commit
的时候, git
会帮我们调用 pre-commit
钩子,然后 husky
设置了这些钩子的执行目录—— .husky/_
。
所以当我们 commit
的时候,实际上调用了.husky/_
目录下的 pre-commit
脚本,然后 pre-commit
脚本又调用了 h
脚本。
我们再来看 h
脚本:
可以看到 h
脚本执行了这个命令
sh -e "$s" "$@"
然后我们主要关注这两句:
h="${0##*/}"
s="${0%/*/*}/$h"
这两句的意思是调用上一级目录的脚本名称,也就是当我们 commit
的时候,会调用 .husky
目录下的 pre-commit
脚本。
可以看到最终调用结果与我们的分析无误,因为我们没有配置 test
这个命令 所以执行报错。
至此,已经把 husky
注入 git
钩子的主要流程阐述完毕。
自定义pre-commit进行eslint扫描
下面我们来试试自定义一个 pre-commit
钩子,主要是在 pre-commit
钩子文件中调用需要执行的命令。
我们希望在 commit
的时候调用 eslint
来做代码检查,所以可以先安装一下:npm i eslint --save-dev
。
然后 eslint
的配置文件 .eslintrc.cjs
内容如下:
这个时候只需要在 .husky/pre-commit
中填入以下内容:
npx eslint .
就可以在提交代码的时候自动调用 eslint
进行代码检查:
lint-staged是怎么工作的
lint-staged
是一个用于在 git
暂存区中运行代码检查工具的工具,通常与 ESLint
、Prettier
、Stylelint
等代码检查工具一起使用,以确保只对暂存区中的文件进行检查,避免不必要的全局检查。
首先来安装一下 lint-staged
:
npm install lint-staged --save-dev
然后在 package.json
中配置一下 lint-staged
需要调用的脚本
"lint-staged": {
"*.(js|jsx|tsx)": "eslint"
},
最后在 pre-commit
钩子中配置一下调用 lint-staged
:
npx lint-staged
可以看到此时提交的时候已经使用 lint-staged
去配合 eslint
做代码检查。
lint-staged
也是通过 git
命令去获取暂存区的文件,比如可以通过 git diff --cached --name-only
这样的命令去获取。
获取到暂存区的文件之后,再调用相应的 linter
去扫描对应的文件。比如 eslint
就扫描 js/ts
文件, stylelint
扫描 css/less
等文件。
Git Hook 实现自动化部署
下面再来介绍一个使用 Git Hook
做自动化部署的示例,在我们推送代码的时候,自动帮我们构建并且把产物推送到服务器上。
这种方式用来部署测试环境没有问题的,但是一定不能用在部署生产环境上。
相比于 Jenkins+Git服务端钩子
而言,这种方式更加的简单方便,无需过多的环境配置。
但是缺点也比较明显:打包是在本地,会占用本地的资源;团队协作时打包环境很容易不一致,可能会导致一些意想不到的问题。
我们主要用到的是 pre-push
这个钩子,它在你执行git push
命令之前被触发。这个钩子允许你在数据推送到远程仓库之前执行一些自定义的操作。
我们可以在这个钩子中编写一些脚本来执行想要的操作,如果这些操作成功完成,Git就会继续推送数据到远程仓库;如果这些操作失败, Git
会中止推送过程,并显示相应的错误信息。
以下的自动化部署适用于你的部署流程是走 ftp
部署,即把打包后的 dist
目录传输到 nginx
或者其他 web
服务器目录下。
我们在 .huksy
目录下新建一个 pre-push
文件,然后填入以下内容
#!/bin/bash
path='你的项目路径'
cd "$path"
rm package-lock.json
rm -rf node_modules
npm i
npx vite build && node "$path/scripts/publish.cjs"
- 进入到项目目录中
- 删除并重装依赖
- 构建并执行发布脚本
然后来看发布脚本:
const fs = require("fs");
const path = require("path");
const archiver = require("archiver");
const { sshConfig } = require("./config.cjs");
/* ------------------ 请配置服务器信息 --- start --------------------- */
const zipName = "test.zip";
const remotePath = `/www/wwwroot/test-${+new Date()}`; // 要上传到服务器的目标路径
const originRemotePath = "/www/wwwroot/test";
const distPath = "../dist"; // 要压缩的文件夹路径
/* ------------------ 配置服务器信息 --- end --------------------- */
const output = fs.createWriteStream(zipName); // 压缩后的文件
const archive = archiver("zip");
output.on("close", function () {
console.log(`${archive.pointer()} total bytes`);
console.log(
"archiver has been finalized and the output file descriptor has closed."
);
});
archive.on("error", function (err) {
throw err;
});
archive.pipe(output);
const directoryPath = path.join(__dirname, distPath);
archive.directory(directoryPath, false);
archive.finalize();
const Client = require("ssh2").Client;
const conn = new Client();
conn
.on("ready", function () {
console.log("服务器连接成功");
conn.exec(`mkdir ${remotePath}`, (err) => {
if (err) throw err;
conn.sftp(function (err, sftp) {
if (err) throw err;
const readStream = fs.createReadStream(zipName);
const writeStream = sftp.createWriteStream(remotePath + "/" + zipName);
readStream.pipe(writeStream);
writeStream.on("close", function () {
console.log(`File ${remotePath} 上传 完成`);
// 解压
conn.exec(
`rm -rf ${originRemotePath} && cd ${remotePath} && unzip -o ${zipName} && mv ${remotePath} ${originRemotePath} `,
function (err, stream) {
if (err) throw err;
stream
.on("close", function (code, signal) {
console.log("部署 完成");
// 删除本地压缩包
fs.unlinkSync(zipName);
conn.end();
})
.on("data", function (data) {
console.log("解压中: " + data);
});
}
);
});
});
});
})
.connect(sshConfig);
- 根据你的服务器
ssh
配置去连接服务器 - 把打包产物
dist
压缩成一个压缩包 - 把这个压缩包传输到你服务器的资源目录下,比如
nginx
的静态资源目录 - 解压这个压缩包并删除旧的打包产物,这就完成了整个发布
其中 ssh
配置大致长成这个样子:
module.exports.sshConfig = {
host: "hostname",
port: 22,
username: "username",
password: "password",
};
至此,我们已经完成在提交代码的时候自动构建以及推送产物到测试环境中,完成了部署。
最后
以上就是本文的全部内容,介绍了 git
钩子以及它的一些实际用途。如果你觉得有意思的话,点点关注点点赞吧~