ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

Husky 与 Git Hooks 实践:从原理到前端项目配置与排错

Husky 与 Git Hooks 实践:从原理到前端项目配置与排错 打开终端执行一次git commit代码没提交成功先被拦下来的是eslint和prettier的检查结果。这个场景在近几年的前端项目里太常见了背后负责“拦人”的就是 Husky 和它管理的 Git hooks 脚本。如果你对 Husky 的理解还停留在“装完就能用”或者正在被“hook 不生效”“同事的机器上跑不起来”这类问题折磨这篇文章就是写给你看的。我会把 Husky 在脚本管理上做的事情拆开讲清楚包括它和 Git hooks 的关系、底层实现逻辑、常见钩子的配置实战以及我在真实项目里踩过的那些坑。1. 为什么前端工程越来越离不开 Husky1.1 一次提交背后的“质量关卡”先还原一个日常开发画面。你改完一个功能git add了五个文件正准备提交结果屏幕上弹出一行报错✖ eslint: src/utils/format.ts - error提交被中断了。你很不爽但也只能回去改代码。这个“提交被中断”的动作就是 Git hooks 在起作用而 Husky 则是把这些 hooks 变成前端工程标准配置的推手。很多人把 Husky 简单理解成“一个拦截提交的工具”这没错但不完整。Husky 真正的价值在于它让 Git hooks 的创建、同步、维护变成项目的一部分而不是靠每个开发者手动往.git/hooks里塞脚本。没有 Husky 的时候代码规范检查、提交信息校验、push 前跑测试这些事情一个人写得挺好换台机器或者换个同事就全没了。1.2 Husky 在整个 Git hooks 体系中的位置要明白 Husky 在干什么得先知道 Git hooks 是个什么东西。Git 允许你在特定的事件节点挂载自定义脚本这些脚本存放在仓库的.git/hooks/目录下文件名决定触发时机。常见的有Hook 名称触发时机典型用途pre-commit执行git commit时代码格式检查、lint 检查commit-msg提交信息写入后校验 commit message 格式pre-push执行git push时跑单元测试、构建检查pre-receive服务端接收推送时服务端策略校验Git 执行这些脚本的规则很简单脚本退出码为 0就继续退出码非 0就中止当前操作。Husky 做的就是帮你把这些脚本放到正确的位置并且让它们在安装依赖时自动生效。1.3 从“本地生效”到“全员生效”的跨越手动写.git/hooks/pre-commit有个致命问题.git目录不会被提交到远端也不会被 clone 拉下来。这意味着你精心设计的检查脚本到了同事电脑上就是不存在。Husky 把自己变成依赖安装在package.json里通过 npm 的安装生命周期自动为你创建 hooks。只要npm install跑过每个人的本地仓库就都有一份相同的检查逻辑。这也是为什么 Husky 在前端工程化里几乎成了标配——它是少数几个能做到“配置一处全员同步”的工具之一。理解了这层逻辑再去看它的安装脚本和配置方式思路就清晰了。2. 从 Git hooks 到 Husky安装与生效的底层逻辑2.1 老版本的做法侵入.git目录Husky 经历过一次比较重要的版本迭代。在 v4、v5 那个时代Husky 的安装脚本做的事情非常“暴力”直接在.git/hooks/目录下写入文件或者把 Git 的 hooks 路径重新指向到 Husky 管理的位置。这种方式有个显而易见的副作用一旦你删掉.git目录重新初始化仓库或者某个工具重写了.git/hooks下的文件Husky 的钩子就失效了。而且不同版本的 Husky 安装逻辑不一致团队里两个人用不同 npm 版本装出来的效果都可能不同。所以后面 Husky 才做了重构改成现在这套更干净也更符合 Git 规范的方案。2.2 现代 Husky 的做法core.hooksPath指向项目目录Git 提供了一个全局配置项core.hooksPath允许你把 hooks 目录从默认的.git/hooks改到其他任意位置。现代 Husky 做的事情本质上是git config core.hooksPath .husky运行之后Git 在执行提交、推送这些操作时会去.husky/目录下找对应的 hook 文件。这个目录是项目的一部分可以被提交到远端仓库。所以当同事 clone 项目后执行npm installHusky 的prepare脚本就会帮他把core.hooksPath指向本地的.husky目录。这里有一个值得注意的细节core.hooksPath是 Git 的本地配置存在当前仓库的.git/config里不会跟着项目代码走。所以“npm install 后自动生效”这一步必须依赖 Husky 的安装脚本去执行git config。一旦跳过安装脚本hook 就静默失效了这是后面排查问题的关键线索之一。2.3prepare脚本与团队的安装关系打开一个接入了 Husky 的现代前端项目的package.json你会看到这样一段{ scripts: { prepare: husky } }npm 的prepare脚本会在npm install之后自动执行本地安装时Husky 的 CLI 就是靠这个入口完成初始化。顺带一提prepare脚本在npm publish前也会执行但正常开发场景下影响不大。团队协作时你只需要保证两件事package.json里有prepare脚本.husky/目录被正常提交到代码仓库。只要这两点满足新成员 clone 项目、执行npm installhusky 钩子就会自动就位。比老版本手动让每个人去跑npm install husky要可靠得多。2.4 从 package.json 配置到文件式配置的变化老版本 Husky 支持在package.json里写husky.hooks节点来声明钩子内容{ husky: { hooks: { pre-commit: npm run lint } } }现在的版本已经彻底转向了文件式配置也就是每个 hook 对应.husky/下的一个文件。比如.husky/pre-commit#!/usr/bin/env sh . $(dirname -- $0)/_/husky.sh npx lint-staged这个变化不仅仅是形式上的。文件式配置有几个明显优势hook 脚本可以直接用 shell 语法逻辑复杂度不受 JSON 格式限制文件名就是 hook 名结构一眼就能看明白每个 hook 的修改记录会在 Git 历史里体现得清清楚楚review 代码的时候能看到这个钩子是谁加的、为什么加。从工程治理的角度来说这比一行行挤在package.json里健康得多。3. 高频 hook 实战pre-commit、commit-msg、pre-push 的配置知道了 Husky 的原理我们来点实际的。以我目前维护的前端项目为例一个比较合理的 hooks 组合长这样3.1 pre-commit lint-staged 的代码检查流水线pre-commit里有三个选择检查全部代码、检查暂存区代码、什么都别查。“检查全部代码”听起来很严谨但项目一大全量 lint 一次动辄几十秒极其影响开发体验。实际工程里更合理的是用lint-staged只检查git add过的那部分文件。lint-staged配合 Husky 的典型写法是先安装依赖npm install --save-dev husky lint-staged npx husky init.husky/pre-commit文件内容npx lint-stagedlint-staged的规则可以放在package.json里也可以单独建.lintstagedrc文件。我习惯放在 package.json 中{ lint-staged: { *.{js,jsx,ts,tsx,vue}: [eslint --fix, prettier --write], *.{css,scss,less}: [prettier --write], *.{json,md}: [prettier --write] } }这里有个细节值得多说一句eslint --fix会直接修改文件修改后的内容又被 lint-staged 自动重新暂存所以提交进去的是修好的代码。但如果你的编辑器不是自动保存风格或者团队里有人没用 format-on-save这个流程会在 commit 时帮你兜底避免“本地代码格式一塌糊涂提交前才发现”的尴尬。3.2 commit-msg commitlint 的提交信息规范commit-msg钩子的触发时机是在提交信息已经写入之后、提交完成之前。它可以帮你挡住那些fix bugupdate之类毫无意义的提交信息。我用的是约定式提交Conventional Commits规范配合 commitlint 来做校验。安装 commitlintnpm install --save-dev commitlint/cli commitlint/config-conventional.husky/commit-msg文件内容npx --no -- commitlint --edit $1配置一个commitlint.config.jsmodule.exports { extends: [commitlint/config-conventional] };这个组合的效果是提交信息必须以feat:、fix:、docs:、chore:等类型开头比如feat: 用户模块新增导出功能。如果格式不对commit 直接被拒绝。这个钩子刚上线的头两天团队肯定有人抱怨“提交个代码怎么这么麻烦”但坚持两周后你去看 Git 历史那种整齐划一的提交记录会让你觉得当时顶住压力是对的。3.3 pre-push 的回归测试与性能控制pre-push是我的最后一道安全网。和pre-commit不同push 的触发频率远低于 commit所以可以放入更重的操作比如跑一遍完整的单元测试。.husky/pre-pushnpm run test但这里有个现实的考量如果项目测试用例特别多每次 push 都跑全量测试团队成员会非常暴躁。我见过一些团队把pre-push里的测试精简成“只跑受影响模块的测试”或者“只跑类型检查”。比如npm run typecheckTS 项目尤其推荐这个。typecheck虽然也要几十秒但比全量测试轻得多又能拦住大量低级类型错误。如果你维护的是规模较大的项目可以根据实际情况调整核心原则是这个 hook 应当抓住真正的硬错误而不应该变成一个消耗耐心的瓶颈。3.4 一个值得直接复制的完整组合把上面几个串起来一个中等规模的 TypeScript 前端项目最终 hooks 配置大致是.husky/ ├── pre-commit - npx lint-staged ├── commit-msg - npx --no -- commitlint --edit $1 └── pre-push - npm run typecheck依赖清单npm install --save-dev husky lint-staged commitlint/cli commitlint/config-conventional这套组合跑了大半年最直观的感受是code review 的时候几乎看不到“顺手改了个缩进”“漏了分号”这类琐碎问题注意力能全部放在逻辑本身。这就是 hooks 带来的隐形收益——它把审查者的时间还给了真正需要人判断的地方。4. 钩子不触发的排查链路从现象到根因Husky 这类工具装好之后最大的噩梦就是“明明配置了但提交的时候没有任何反应”。我处理过的 hook 失效问题多了基本可以归纳成下面这条完整的排查链路。4.1 先复现手动执行 hook 脚本遇到 hook 不生效别急着改配置。先把问题缩小到“是脚本本身不执行还是日志没显示”。直接手动执行一次 hook 文件sh .husky/pre-commit如果手动执行时脚本报错那说明问题在 shell 命令本身如果手动执行一切正常问题大概率出在 Git 没有调用这个 hook。这一步能帮你快速划分排查方向避免在错误的方向上折腾半天。4.2 检查core.hooksPath与目录结构Git 没有调用 hook最常见的根因就是core.hooksPath没有正确指向.husky。执行git config core.hooksPath正常情况下会输出.husky。如果输出为空或者指向了其他目录说明 husky 的初始化没有成功执行或者被其他配置覆盖了。修复方式很简单手动执行一次git config core.hooksPath .husky但这里不要停留在“改好就行”的层面还要问一句为什么会被改掉。我遇到过的真实情况包括同事手动执行了某个工具的命令该工具顺手改了core.hooksPath还有人用了老版本的 husky 初始化命令。排查时要顺藤摸瓜找到是谁改的免得下次又被重置。还需要确认.husky/目录下的文件确实存在并且文件名拼写正确。Git 对 hook 文件名是大小写敏感的pre-commmit少一个 m或者写成Pre-commit都静默不执行。4.3 检查安装流程与 npm 生命周期如果core.hooksPath是对的hook 文件也存在但 Git 操作时还是没反应那就要看安装环节了。重点检查两件事package.json里的prepare脚本还在不在上一次npm install是否完整执行。有一个很常见的坑同事的 npm 配置了ignore-scriptstruenpm install 时一律跳过生命周期脚本husky 的 prepare 根本没跑。这时候你在他那台机器上git config core.hooksPath多半是空的或者指向了旧路径。让他把ignore-scripts关掉重新执行 install 即可。还有一个隐蔽的坑是 npm 的缓存。某些情况下老版本的 husky 被缓存了新加的 hook 文件没有正常创建出来。处理方式是清理 npm 缓存后重新 installnpm cache verify npm install4.4 非法跳过与绕过--no-verify和HUSKY0有些“hook 不生效”其实不是失效而是被人为绕过了。git commit --no-verify会跳过所有客户端 hookgit push --no-verify同理。Husky 本身也支持通过环境变量HUSKY0临时禁用所有钩子。举个例子HUSKY0 git commit -m 紧急修复这条命令会直接跳过 pre-commit 和 commit-msg让提交成功。我不反对紧急情况下用这个开关但强烈建议在团队规范里明确--no-verify只能用于刻不容缓的救火场景并且事后必须补上对应的检查。否则“反正可以绕过”会成为习惯hook 的存在意义就瓦解了。排查这类情况时可以看下 shell 历史记录或者 CI 日志确认是不是有人用了跳过参数。问题现象可能根因解决动作所有 hook 均未执行core.hooksPath配置丢失或指向错误git config core.hooksPath .husky单个 hook 未执行文件名拼写错误、大小写不对核对.husky/下的文件名新成员机器上未生效npm 配置了ignore-scripts关闭后重新npm install提交/推送时无拦截使用了--no-verify或HUSKY0检查历史命令和项目约定5. 团队规范与 CI 协作让 Husky 成为项目资产5.1 如何让新同事 clone 后自动具备 hooksHusky 最容易被低估的一点就是它的“传染性”。只要package.json的prepare脚本和.husky/目录都进了仓库那么新成员 clone 项目后执行一遍npm install一切就自动就位。这个体验在新老成员之间是完全一致的——这正是它取代手动配置.git/hooks的根本原因。但要注意一个例外情况如果你的项目用的是 pnpm并且启用了ignoredBuiltDependencies之类的过滤机制husky 的依赖安装可能被跳过。具体表现同样是 hook 不生效。解决办法是在项目初始化时把 husky 加入允许列表或者让成员手动执行pnpm rebuild husky。这类问题在 macOS 和 Windows 上还可能因为 shell 环境不同出现差异排查时要多留个心眼。5.2 CI 中处理 hooks 的正确姿势很多团队上来就把 Husky 装好结果 CI 里跑npm install时又执行了一遍 hooks导致流水线偶尔因为环境差异被莫名卡住。实际上CI 环境里根本不需要跑这些客户端 hook因为提交代码的人已经在本地被检查过了。推荐的做法是在 CI 脚本中显式关闭# .github/workflows/ci.yml 或对应 CI 配置 env: HUSKY: 0这行配置让 husky 在 CI 环境内完全静默既不会干扰安装流程也不会在流水线的构建步骤里多出不必要的检查。记住一个原则本地做质量拦截CI 做最终验证。两端分工明确才不会互相打架。5.3 本地环境差异与规避方案前端团队的开发环境远比想象中复杂有人用 macOS有人用 Windows有人用 WSL。Husky 生成的 hook 文件本质是 shell 脚本Windows 环境尤其容易出现执行权限或路径解析问题。规避方案主要有三种保持 hook 文件内部的 shell 脚本尽量简洁不要写复杂的管道、grep、awk 逻辑把复杂检查逻辑收敛到 npm scripts 或 node 脚本里hook 文件只保留一行调用在团队共享的开发文档里写清楚 Windows/WSL 下的安装注意事项。我自己遇到过一次比较经典的坑某成员在 Windows 上用系统的 Git Bashnpx husky init生成的文件没有可执行权限导致 hook 静默失效。最后通过给.husky/下的文件手动追加执行权限解决chmod x .husky/pre-commit .husky/commit-msg .husky/pre-push这个操作在 macOS 和 Linux 上通常不会遇到但 Windows 环境真的要格外注意。6. 我踩过的坑和几个提升体验的小细节6.1 lint-staged 与 stash未暂存改动丢失的惊吓lint-staged 在处理暂存区时会利用 Git 的 stash 机制临时保存未暂存的改动等 lint 完成后再恢复。有一个版本出现过一种情况如果你的未暂存改动包含了会引发 lint 错误的代码而暂存区是干净的lint-staged 可能把这些改动混进来导致现场看起来很乱。我的经验是提交前尽量保持工作区是干净或基本可控的状态。不要养成“先随便 add 一部分留着一大堆改动继续写”的习惯。这不仅是 lint-staged 的策略问题更是 Git 使用的健康习惯。6.2 给 hook 加上可读性输出Husky 拦截住提交时默认输出往往是 lint 的错误信息但那是给机器看的不是给人看的。我习惯在 hook 脚本开头加一段友好提示告诉当前开发者发生了什么、该怎么处理。比如.husky/pre-commit可以写成npx lint-staged || { echo 代码格式有问题先执行 npm run lint:fix 修复再重新 add 和 commit。 exit 1 }团队里有新人时这类提示能大幅降低不知所措的概率。而且加了这段逻辑还避免了 lint-staged 失败后 Git 抛出一堆令人困惑的底层报错直接面对问题核心。6.3 不要把所有检查都塞进 hook最后想说一个关于“度”的建议。Husky 是很好的质量闸门但不要把它变成全体开发者的枷锁。我见过有的项目在 pre-commit 里同时跑 eslint、stylelint、prettier、tsc、单测提交一次要等五分钟。这样做的结果是大家为了省时间要么频繁使用--no-verify要么手动把 hook 关掉。当一个工具开始被高频规避时它就已经失去了约束力。合理的做法是把检查按“提交时”和“推送时”拆分提交时跑轻量级的暂存区检查和提交信息校验推送时跑成本更高但能兜底的类型检查或关键模块测试。这个分工既能覆盖大部分问题又不会让开发者觉得每一步都在过安检。6.4 一个小技巧手动维护 hook 文件的版本演进Husky 的 hook 文件本身就是项目代码的一部分所以它也应该走 code review 流程。团队里在调整.husky/目录下的脚本时我会在 PR 描述中附带一段验证说明比如“在 macOS 和 WSL 下各跑了一次提交验证通过”。这样能提前暴露跨平台兼容层面的大多数隐性问题。毕竟hook 文件失效的代价不是报错而是毫无提示地失去一道保护网。这一路用下来Husky 给我的整体感受是它本身不复杂复杂的是你如何设计这套脚本组合、如何把团队协作的规则固化下来、如何在出问题时快速定位是环境还是配置的原因。把这个工具吃透了你收获的其实不是某一条命令的用法而是一套关于“如何在代码入库前设卡”的完整实践思路。
返回列表