一、背景
之前创建的项目,发现代码 commit 提交的时候没有了任何校验,具体表现:
- 一是 feat fix 等主题格式校验没有了
- 二是代码 lint 不通过也能提交
尝试解决这个问题,并深入了解husky的实现原理,将相关的一些知识点分享下。
二、lint配套工具
完整的一套代码规范工具,可能包含:husky + lint-staged + eslint + stylelint + prettier 等
1. git 钩子:
git 本身提供多个生命周期钩子,常见的例如 pre-commit、commit-msg、pre-push 等。
pre-commit 钩子:
在 git commit 命令执行之前触发,配合指定的脚本命令可用来执行代码 lint 校验、代码格式化、跑测试用例等任务,以确保在提交代码之前达到一定的质量标准。
commit-msg 钩子:
是在 git 编辑提交信息之后、提交之前触发,可用于 commit 信息的格式校验。
2. husky:
husky 是一个用来简化 Git 钩子管理的工具。它可以帮助你在 Git 的生命周期事件(如 pre-commit、pre-push 等)中轻松地添加脚本,并且与常用的 js 工具和任务管理器(npm、yarn)集成良好。
3. lint-staged:
lint-staged 用于结合 husky 和 pre-commit,能做到只对 Git 暂存区(git add . 之后)的文件 运行校验任务,从而实现只处理改动的文件,提升commit效率,在大型项目上效果更加明显。
4. eslint:
eslint 是用于对 js 和 jsx 做代码校验的工具,通过静态分析代码来快速地发现和修复一些代码语法问题,帮助前端开发规范代码、提高效率。
5. stylelint:
stylelint 是用于对 css 样式 做代码校验的工具,同上述 eslint 类似,帮助前端开发在编写 css 及 less等预处理器代码时避免犯错,提升开发效率。
6. prettier:
prettier 是目前最常用的前端代码格式化工具,支持多项规则配置,帮助统一代码风格。适当的配置后可以与上述 eslint 和 stylelint 兼容。
三、husky原理
疑问:
- git 是全局命令,而 husky 只是项目里引入的一个插件,它是怎么和全局的 git 命令产生联系的?
- 我们通过 git commit -m “xxx” 提交代码时,husky 做了什么?
(以下以 mac os 下 husky v9 版本示例)
1. husky初始化:
1)配置方式:
- 一般会在项目 package.json 的 scripts 里配置下 prepare 命令(“prepare”: “husky”),用于执行 husky 的初始化。prepare 是 npm 的钩子,在项目 npm i 时会自动执行 npm run prepare。
2)解析husky命令:
- 执行 husky 命令 实际就是执行 node_modules/.bin/husky 文件,里面没啥东西,主要是三块,自动修改 husky init 命令、错误命令输出处理、执行 index.mjs,显然核心代码在 index.mjs 里。
- 找到 node_modules/husky/index.mjs 文件,解析下里面的核心代码动作:
- 执行命令 git config core.hooksPath .husky/_ ,创建目录 .husky/_
- .husky/_ 里写入 .gitignore 内容为 *
- 复制 node_modules/husky/husky 文件内容到 .husky/_ 的 h 文件
- 遍历 l 数组,即支持的所有 git 钩子,复制到 .husky/_ 下同名文件,内容统一为 . “${0%/*}/h”
- .husky/_ 里创建文件 husky.sh,内容为空。
3)解析说明:
-
一般来说项目都是用 git 来作为代码版本控制工具,每个项目在 git init 初始化后,根目录都会生成一个 .git 隐藏文件夹,里面是 git 的配置文件 及 代码记录信息。
-
husky 在执行初始化命令后,会自动修改 git 的 hooks 目录,具体表现为在项目 .git 文件夹下的 config 文件里新增了 core.hooksPath 配置,指向项目的 .husky/_ 目录,这样执行 git commit 命令时就会自动寻找项目 .husky/_ 下的钩子文件。
- 未指定时,默认的 hooks 目录是 .git/hooks
-
然后在 .husky/_ 目录下自动生成 git 钩子文件 和 一些执行脚本。
*
2. husky执行流程:
(以项目 pre-commit 钩子为例)
1)开始运行 commit
- 修改项目文件,运行 git add .
- 运行 git commit -m “xxx”,开始流程。
2)读取 hooks 目录配置
- git工具会去项目的 .git 目录 config 文件里读取 core.hooksPath 目录配置,指向了.husky/_ 目录
3)寻找 pre-commit 业务脚本
- 首先在 .husky/_ 目录下找到 pre-commit 钩子文件,即 .husky//pre-commit,并执行,实际执行是指向了同目录下的 h 文件,即 .husky//h
- ${0%/*}:0表示当前执行的脚本路径,%/* 表示从字符串末尾匹配 /*,保留前面部分
- 执行 h 文件,里面基本是统一读取 husky 配置 以及 做了错误校验,然后实际执行是指向了上一级目录下的同名文件,即 .husky/pre-commit,这个就是我们要寻找的业务侧钩子脚本。
- ${0##*/}:##/* 表示最大化匹配 */,保留后面部分
4)执行 pre-commit 脚本
- 执行 .husky/pre-commit 文件,这个就是真正的业务侧钩子脚本,里面内容是就是跑 lint-staged。
5)执行 lint-staged 脚本
- 运行 lint-staged 命令时,首先是要读取 lint-staged 配置,在 .lintstagedrc 文件里(部分项目可能在 package.json 里配置的),里面就是对不同类型的文件跑不同的命令
6)执行成功 or 失败
- 如果命令执行成功,pre-commit 钩子就顺利执行完成了,接着会往下走流程,走 commit-msg 钩子,做 提交信息的格式校验,大致流程类似。
- 如果执行异常,钩子会中断,不会往下走,commit 失败。如异常命令有抛出信息会在命令行输出。
四、问题解决:
回到最初提到的 bigfish 项目 commit 失效问题。
1. 问题原因:
在代码提交时有warning提示,翻译下来就是 .husky 文件夹下的两个文件是不可执行的,
其实就是 .husky 里的这两个钩子文件没有被赋予可执行权限。
- 如果这俩文件是在Windows下创建后上传的,在mac下把项目拉下来后就不可执行了。
2. 解决方式:
方式一:
- 在项目根目录下运行命令,给钩子文件手动赋予可执行权限,
- chmod +x .husky/commit-msg .husky/pre-commit
方式二:(推荐)
- 升级 husky 版本到 v9,修改 package.json 里的 preare 命令为 “husky”,然后运行 npm run prepare
- husky v9 版本更新日志里有一项变更,通过自动设置正确的文件权限,解决了 husky v8 中的这个权限问题。
- husky v9 版本更新日志里有一项变更,通过自动设置正确的文件权限,解决了 husky v8 中的这个权限问题。
3. 全局format:
解决完后,建议执行一次 npm run format 命令,即 prettier,对全局做一次代码格式化。
因为每次 commit 校验的时候项目虽然会自动执行 format,但只对修改的文件执行,建议提前对项目所有文件 format 处理下。
五、IDE自带git工具问题:
可能有些人会遇到使用 vscode 或 在线编辑器 等IDE时,自带的 git 管理工具无法 commit,执行时会立即报错。
1. 问题原因:
可能你使用了 nvm 等 node 管理工具,使用 git 工具可能会无法 commit,原因是执行 husky 命令时识别不出 node 环境,详见链接,需要补充环境配置。
2. 解决方式:
在 .husky/pre-commit 和 .husky/commit-msg 文件内容里的 npx执行命令 之前,添加环境配置代码:
待添加的环境配置代码内容:
- 方式一:(个人搜寻并组合出来的)
if [ -s "$HOME/.nvm/nvm.sh" ]; then# First load nvm and provide access to the nvm command.export NVM_DIR="$HOME/.nvm/nvm.sh". "$(dirname $NVM_DIR)/nvm.sh"# Use the nvm ls command to detect the version being used.export NVM_DIR="$HOME/.nvm"a=$(nvm ls | grep 'node')b=${a#*(-> }v=${b%%[)| ]*}# Export the current version in your path for husky to find.export PATH="$NVM_DIR/versions/node/$v/bin:$PATH"
fi
- 方式二:(官方文档里的,待验证)
export NVM_DIR="$HOME/.nvm"
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" # 加载 nvm