
1. 项目概述Husky在团队协作中的定位1.1 从一次线上事故说起先讲个真实经历。有一次我负责的团队在发版前发现某个开发者的代码没有通过ESLint检查就直接合并到了主干分支结果线上出现了白屏。排查下来发现不是代码能力问题而是流程漏洞——大家在本地提交代码时代码风格检查、单元测试这些门禁形同虚设全靠个人自觉。有人会说我“忘了跑”有人说“本地跑过了”但实际上根本没执行。这种情况在多人协作的项目里太常见了。Human的自觉性在重复性劳动面前是不可靠的而机器规则才是真正的保障。Husky就是解决这个问题的核心工具。它的本质是一个Git Hooks管理工具让我们能够在git commit、git push等操作之前自动执行指定脚本从工具层面强制执行代码质量门禁。谁没通过检查谁就提交不上去没有例外没有借口。1.2 Husky到底是什么Husky这个名字听上去挺可爱取自一只哈士奇的卡通形象。它在npm上的全称是husky是JavaScript生态中最流行的Git Hooks解决方案之一。它要做的事情很简单让你在package.json里就能配置Git Hooks脚本而不用直接去改.git/hooks/目录下的原生脚本文件。原生Git Hooks的痛点很多.git目录是本地仓库的一部分不会被提交到远程仓库团队其他成员拉代码时无法共享这些钩子配置钩子脚本要用shell或者某种可执行语言来写对前端开发者不友好。Husky通过prepare生命周期钩子和core.hooksPath配置把钩子配置直接存放在项目根目录的.husky/文件夹中这个目录可以被提交到Git仓库从而实现了全团队的自动同步。1.3 解决什么问题、适合谁Husky适合的场景非常明确多人协作的JavaScript/TypeScript项目尤其是前端工程化体系已经建立、但流程执行总靠“自觉”的团队。它解决的问题就是“让流程自动化、强制化”。如果项目里只有你一个人开发那Husky的价值有限。但一旦团队人数超过三人代码风格开始出现分歧代码评审开始占用大量时间Husky的价值就会立刻显现——把机器能查的问题全部拦截在提交之前把评审时间留给真正需要人判断的逻辑设计。这篇文章适合前端工程师、Node.js全栈开发者、前端Leader和DevOps工程师阅读。我会从原理、配置、实战到踩坑把Husky这套体系讲透。2. 核心原理深度拆解2.1 Git Hooks机制的本质要理解Husky先要理解Git Hooks。Git Hooks是Git内建的一种机制允许在特定的重要动作发生时触发自定义脚本。比如pre-commit在输入提交信息之前触发prepare-commit-msg在编辑器打开之前触发commit-msg在提交信息编辑完成后触发pre-push在推送之前触发。这些钩子都存放在.git/hooks/目录下Git会在对应动作发生时查找该目录下是否有对应名字的可执行文件如果有就执行。原生的钩子脚本可以用任意语言编写只要能被系统执行即可最常见的是Shell脚本。听起来挺好对吧问题出在共享机制上。.git/目录是仓库元数据所在它只存在于本地不会跟随代码提交到远程。这意味着就算你在本地配置了完美的钩子团队里其他人拉取代码时这些钩子配置并不会同步过去。想让整个团队都有统一的行为就得做大量额外工作。2.2 Husky的两个创新点Husky解决这个问题的思路经历了两个阶段每个阶段的方案都可以说是踩了无数坑之后的血泪教训。**第一阶段Husky 4及更早版本**采用“自动修改Git配置”的方式。在初始化时Husky 4会把.git/hooks/pre-commit做成一个软链接指向husky内部维护的脚本。同时它会在.git/config中加入core.hooksPath的配置或者直接向.git/hooks目录写入链接文件。因为.git/hooks下的文件都是本地文件Husky通过安装时自动修改的方式实现了钩子的自动化配置。但问题在于软链接在Windows环境下经常出问题而且如果团队成员没有主动运行npm install钩子就根本不会装上去。**第二阶段Husky 9及之后**采用了完全不同的技术方案。它利用prepare钩子npm install后自动执行来配置Git。具体来说Husky 9在安装时通过npm install的prepare生命周期在项目根目录运行husky命令这个命令会执行git config core.hooksPath .husky。这样一句话就把Git的钩子目录指向了项目文件夹下的.husky/目录。因为.husky/目录下的文件是会被提交到远程仓库的前提是不要把它加到.gitignore里整个团队的钩子配置就实现了完全同步。2.3 从软链接到hooksPath的演进逻辑我特意要展开讲这一点因为理解这个演进过程你才能真正理解为什么Husky 9的配置方式和Husky 4完全不同也才能明白网上那些教程为什么互相“打架”。Husky 4的核心机制是安装时在.git/hooks/下建立软链接。例如当你运行npx husky install时它会帮你在.git/hooks目录下创建名为pre-commit的符号链接这个链接指向Husky在node_modules里的真实脚本。这种方案能做到自动化但有几个致命问题第一如果你用了npm ci干净安装prepare阶段的自定义脚本可能会被跳过或者因为网络原因失败导致钩子没有装上第二在Window系统上默认情况下创建符号链接需要管理员权限这让很多前端开发者在Windows环境里直接装失败第三也是最重要的.git/hooks是纯本地文件你的钩子逻辑如果分散在node_modules内部某个深层目录里一旦某个版本升级路径变动有可能导致钩子静默失效。Husky 9的思路完全不同不再去动.git/hooks目录而是通过一条git config core.hooksPath .husky命令把整个Git的钩子搜索目录整体指向项目根目录的.husky/。这个机制是Git自身就支持的跟软链接相比它更稳定、更透明而且.husky/目录可以直接被Git跟踪天然解决了团队同步问题。注意core.hooksPath这个配置是对当前仓库生效的它不是全局配置。如果你在别的克隆副本里没有执行husky install那钩子不会生效。这个问题我后面的“常见问题”部分会详细讲。3. 环境准备与初始化配置3.1 版本选择为什么要用9.x如果你去npm上搜husky最新的主版本已经迭代了多个大版本。Husky 4到Husky 9之间的配置方式差异巨大很多老教程还在用husky: { hooks: {...} }这种package.json内嵌配置那是Husky 4的玩法在Husky 9中已经完全无效了。我强烈建议新项目直接用最新版本9.x理由非常简单对比项Husky 4Husky 9配置位置package.json内的husky.hooks字段独立.husky/目录下的Shell脚本安装机制通过npx husky install写入.git/hooks软链接通过prepare脚本执行git config core.hooksPath .husky团队同步需要每个人手动安装初始化提交.husky/目录后自动同步Windows兼容性软链接需要权限容易不稳定纯路径配置兼容性好版本迭代已停止维护持续维护更新为什么Husky 4被淘汰吃过的亏太多。曾经有同事在Windows上开发他拉取代码后npm install一切正常但就是没有钩子拦截最后查了半天发现是Windows软链接权限问题导致的。这种事情只要遇到一次你就会毫不犹豫地切换到新版本。3.2 初始化配置的完整步骤新建一个项目或者想在现有项目里加入Husky操作流程如下。首先确认Node.js环境建议Node.js 16以上版本npm 7以上版本。老版本Node可能跟新版本Husky有兼容性问题。初始化package.json如果还没有npm init -y安装Husky并初始化npm install husky --save-dev npx husky initnpx husky init会帮我们完成几件事在项目根目录创建.husky/文件夹并生成一个示例钩子脚本pre-commit在package.json的scripts中写入prepare: husky脚本把.husky/目录标记为可提交状态。此时的项目结构大概是这样的你的项目/ ├── .husky/ │ └── pre-commit ├── node_modules/ ├── package.json └── ...打开.husky/pre-commit文件里面默认内容是这样的npm test这个文件的含义是每次执行git commit时先运行npm test如果测试不通过提交直接被打断。3.3 手动添加其他钩子文件初始化只帮你创建了pre-commit这个示例其他钩子如commit-msg、pre-push需要手动创建。有两种方式。第一种在.husky/目录下手动新建文件文件名就是钩子名不需要后缀名。比如新建.husky/commit-msg然后写入npx --no -- commitlint --edit $1然后给它加执行权限Linux/Macchmod x .husky/commit-msg第二种方式用命令创建npx husky add .husky/commit-msg npx --no -- commitlint --edit $1husky add命令的底层逻辑很简单创建文件、写入内容、加上执行权限。如果文件已存在它会提示是否覆盖。这个命令最方便的地方是它能自动处理不同平台的可执行权限问题。值得注意的是在Windows上通过husky add添加的文件不需要额外处理权限而在手动创建时一定要注意chmod x否则Git会直接忽略没有执行权限的钩子脚本。4. 核心场景实战配置4.1 场景一配合lint-staged实现增量代码检查这是Husky最经典的使用场景。在你的项目里pre-commit钩子不要做全量检查而要做增量检查。什么意思如果项目已经有几万行代码每次提交都全量跑一遍ESLint效率极其低下——提交一次可能等上几分钟。更合理的方案是只检查本次修改的那几个文件。此时就需要lint-staged登场。lint-staged的功能是在Git暂存区中找出你这次要提交的文件然后只对这些文件运行你指定的检查或格式化命令。它和Husky是黄金搭档。安装方式npm install lint-staged --save-dev在package.json中配置lint-staged{ lint-staged: { *.{js,ts,vue,jsx,tsx}: [ eslint --fix, prettier --write ], *.{css,scss,less}: [ prettier --write ], *.{json,md,yaml,yml}: [ prettier --write ] } }然后将.husky/pre-commit的内容修改为npx lint-staged为什么在pre-commit里只需要写npx lint-staged就够了因为lint-staged内部会自动从package.json里读取配置然后对暂存区文件执行命令。如果ESLint检查失败它会以非零状态退出Husky检测到脚本执行失败就会中断提交。这个过程是自动联动的。这里有个细节想多说一句eslint --fix这个命令会自动修复一些简单的问题比如引号、分号、缩进修不完的问题才会报错。这就意味着大多数风格问题实际上根本不需要开发者手动改工具直接改完再提交极其舒服。4.2 场景二commit-msg钩子约束提交信息格式代码检查只是门槛之一提交信息的规范化同样重要。为什么要规范提交信息因为提交信息是后续生成CHANGELOG、做版本发布、进行代码回溯的基础。如果每个人的提交信息随意写历史记录的可用性会大打折扣。这个时候需要commitlint。它专门用来校验提交信息是否符合规范通常是约定式提交即Conventional Commits格式比如feat: 某某功能、fix: 某某bug。安装npm install commitlint/cli commitlint/config-conventional --save-dev创建.commitlintrc.js或.commitlintrc.jsonmodule.exports { extends: [commitlint/config-conventional] };然后在.husky/commit-msg中写入npx --no -- commitlint --edit $1这里的--edit参数的含义是检查某个提交信息文件内容是否符合规范。$1是Git传给commit-msg钩子的参数内容是提交信息暂存文件的路径。npx --no是先不执行某个包再把命令委托给后续的commitlint执行。配置完成后如果你提交时写了fix bug这种不规范的信息提交会被直接拒绝并提示你格式错误。很多有经验的团队还会把commitlint的规则进一步自定义比如限制提交信息的type类型必须从指定枚举中选择或者要求正文不能为空。这部分配置可以根据团队情况自己调整。4.3 场景三pre-push钩子做高质量的强制门禁pre-commit适合做快速检查pre-push适合做重一点的质量门禁。我之前团队的做法是pre-commit跑lint-stagedpre-push跑单元测试和构建。新建.husky/pre-pushnpx husky add .husky/pre-push npm run test:unit npm run build为什么要在pre-push跑测试因为pre-commit如果也跑全量测试每次提交都要等很久开发者体验非常差。把重活放到推送之前执行是个更合理的策略推送的次数远少于提交的次数等一会儿也就认了。有一个细节值得注意如果你在pre-push里运行了构建这其实也对CI/CD有一定程度的替代作用。因为代码在推送到远程之前就已经被验证过能构建成功远程CI的构建压力会小很多。当然CI的构建依然有其存在价值——它是最终防线能捕获一些本地环境无法发现的问题。4.4 场景四prepare钩子与团队自动化提到prepare这可能是Husky机制里最容易被忽视的。prepare是npm的生命周期钩子在npm install完成之后自动触发。我们在package.json中配置的prepare: husky就是让每次安装依赖时自动执行husky命令而这个命令会自动设置好Git hooks路径。这意味着什么团队成员拉取项目代码后只需要执行常规的npm installHusky就会自动配置钩子不需要额外手动运行任何初始化命令。这种机制配合.husky/目录入库就实现了“零配置”的团队统一。需要留意的是如果使用了--ignore-scripts参数安装依赖比如npm install --ignore-scriptsprepare钩子不会触发安装完成后Husky就没有配置钩子路径。这类情况常见于部分安全审查比较严格的环境。解决办法是在安装完成后手动执行一次npx husky init。5. 常见问题与排查技巧实录5.1 钩子不生效最让人抓狂的问题我见过最多的问题是Husky明明装好了pre-commit文件也写好了但提交代码时钩子完全没反应。遇到这种情况第一件事是看Git的钩子路径配置是否正确。执行以下命令检查git config core.hooksPath如果输出是undefined或者空值说明core.hooksPath没设置。Husky就没生效。正常情况下应该输出一个路径比如.husky或完整的绝对路径。如果输出是.husky但hook还是不触发那么就检查一下.husky/目录下文件的可执行权限。在Linux/Mac上ls -la .husky/看文件权限是否包含x。如果没有执行chmod x .husky/pre-commit .husky/commit-msg这里有一个用户容易踩的坑用Windows的git bash创建的文件提交到Linux的CI机器时权限位往往会重置。所以跨平台团队一定记得在提交前检查好文件权限。5.2 钩子之间的顺序与$1参数有些开发者会好奇commit-msg里的$1到底是什么Git在执行不同的钩子时传入的参数是不同的。搞明白这些参数的传递规则你才能写出正确的钩子脚本。钩子名称被调用的时机可用参数pre-commit在输入提交信息前无参数可以从环境变量GIT_EDITOR等获取上下文prepare-commit-msg在编辑器启动前默认提交信息创建后$1提交信息文件路径$2提交来源message、template等commit-msg在提交信息编辑完成后$1包含提交信息的文件路径pre-push在git push期间远程引用更新前$1远程名$2远程URLpre-receive服务端接收推送时无参数从标准输入读取refs信息例如pre-commit不需要参数它只要退出码是0就放行非0就拦截。而commit-msg钩子里Git会将包含用户提交信息的临时文件路径传给脚本commitlint --edit $1才能去读取这个文件内容。5.3 如何绕过Husky执行紧急提交有时候情况紧急你要提交一份代码绕过检查该怎么办Husky和Git本身提供了跳过机制最常用的是在commit命令后加--no-verifygit commit --no-verify这个参数会跳过pre-commit和commit-msg钩子。同样地push时可以用git push --no-verify但我要给你一个直白的建议不要养成用--no-verify的习惯。它的作用是紧急救援而不是日常逃课。很多团队纪律严肃甚至会在服务器端的CI上再做一次检查来堵住那些绕过本地规则的提交。如果你要用--no-verify确保你清楚自己在做什么并且事后要能补上修复提交。还有一种情况是某次提交想跳过某一条检查但不跳过所有钩子。很遗憾Husky和Git原生的机制不支持这种“精细跳过”。能做到的方式是在脚本里加判断比如判断环境变量或在某个分支上才启用某个钩子。这块要看项目自身需求来实现了。5.4 多分支场景下的钩子管理很多项目会面临一个问题不同分支的钩子需求不同。举个例子main分支是生产分支要求提交信息必须带issue编号feature/xxx功能分支是内部开发分支要求可以宽松一些。Husky本身不区分分支但你可以利用git branch --show-current命令获取当前分支名然后在钩子脚本里做条件判断。示例如下在.husky/commit-msg中branch$(git branch --show-current) if [[ $branch main ]]; then npx --no -- commitlint --edit $1 else exit 0 fi这种写法我在团队里用过效果不错。不过要说明一点团队成员对“分支不同、规则不同”这样的复杂度接受度不一。如果没有强需求我还是建议全项目统一一套简单规则复杂化只在确实需要时引入。5.5 性能问题如何避免钩子拖慢团队节奏Husky本身不会觉得慢真正慢的是你在钩子里执行的命令。我在一个中型项目上见过他们在pre-commit里跑了npm run lint全量检查结果每次提交要等3分钟以上团队怨声载道。解决思路很简单能用增量方式就用增量方式。lint-staged是增量检查的标配tsc --noEmit这种全量类型检查也可以想办法做缓存比如加上--incremental参数让TypeScript的托管缓存文件加速后续检查。还可以考虑用concurrently并行执行多个检查缩短总耗时。另外一个好用的实践是把耗时极短的检查放pre-commit把耗时较长的检查放pre-push把耗时很长的完整构建和集成测试放CI。这种分级设计既保证了质量门禁的存在又不让开发者感觉到明显的节奏被打断。5.6 CI环境中的注意事项CI持续集成机器上往往也会安装依赖、跑构建。如果你的CI脚本里有npm ci它会触发prepare钩子除非显式禁用所以Husky会自动初始化。但CI通常不做代码提交所以Husky在CI里基本是闲置状态危害不大。真正需要注意的是有些项目的CI流程里包含自动化提交比如自动更新版本号后提交此时如果CI机器的Git配置有问题或者全局Git版本过低Husky会拦截CI的自动化提交导致流水线失败。解决办法是在CI的自动化提交命令中加上--no-verify或者确保CI机器上的Git版本和Husky兼容。还有一点npm ci和npm install都会触发prepare但npm ci在CI里是推荐用法。如果在CI上配置了--ignore-scriptsHusky就不会配置。如果你的CI里确实需要跑测试和构建建议不要禁用npm scripts或者显式在CI的构建步骤执行一次npx husky init。6. 项目改造实战从零到完整门禁体系6.1 在现有项目里渐进式引入Husky如果要在老项目里引入Husky我建议不要一步到位。直接加太多门禁会给团队带来非常大的抵触情绪最后演变成人人用--no-verify绕过检查。渐进式落地的节奏很重要。具体做法分三步走。第一步先加pre-commit的lint-staged增量检查只做格式化修复不强制拦截报错。这样可以先让大家适应工具链感受到代码风格统一的舒适感。第二步等团队适应后再把严重级别高于某个阈值的ESLint错误设置为阻塞让一些真正会影响代码质量的问题在本地暴露。第三步加上commit-msg的commitlint约束修正整个团队的提交信息规范。这一步放在后面是因为改动提交习惯的摩擦成本比改代码风格还要高。6.2 团队同步知识库让所有人都理解Husky技术落地的瓶颈往往不在技术本身而在人的接受度。我会在团队Wiki或README里写一份简单明了的Husky使用说明重点只有几条Husky会在什么时候帮你检查、如果钩子拦截了你应该看什么信息、怎么解决常见拦截问题。最重要的是写清楚不要用--no-verify绕过钩子如果你觉得某个钩子不合理请主动反馈给前端基建负责人而不是自己默默跳过。6.3 一步到位的完整工程示例下面给出一份可直接参考的工程配置示例。package.json中关键部分{ scripts: { prepare: husky, lint: eslint . --ext .js,.ts,.vue, lint:fix: eslint . --fix --ext .js,.ts,.vue, test: vitest run, build: vue-tsc --noEmit vite build }, devDependencies: { commitlint/cli: ^19.0.0, commitlint/config-conventional: ^19.0.0, husky: ^9.0.0, lint-staged: ^15.0.0 }, lint-staged: { *.{js,ts,vue,jsx,tsx}: [ eslint --fix, prettier --write ], *.{json,yaml,yml,md}: [ prettier --write ] } }.husky/pre-commitnpx lint-staged.husky/commit-msgnpx --no -- commitlint --edit $1.husky/pre-pushnpm run test这样配置下来整个团队就拥有了一套完整的前置质量门禁系统。提交前做增量代码风格检查提交时校验提交信息规范推送前跑单元测试。三个门禁把守卫工作分级完成节奏感非常舒服。7. 探索更多可能的扩展Husky能做的事情比基础配置多得多。结合项目本身的技术栈你完全可以让它承担更多自动化的任务。一套我非常推荐的扩展是pre-commit阶段用lint-staged做代码风格统一commit-msg阶段用commitlint规范提交信息pre-push阶段跑单测同时利用prepare做依赖安装后的钩子自动初始化。有了这套基础你还可以加入secretlint扫描密钥泄露、加入depcheck检测无用依赖、加入tsc类型检查、加入danger.js做代码评审辅助。实际中我还碰到过一个有意思的场景团队成员常有忘记生成类型定义文件的问题。我们把“生成类型定义”写进pre-commit钩子里让工具自动补齐杜绝了这类问题。你能把多少重复劳动交给Husky取决于你对项目痛点的理解和设计能力。8. 我的实操心得与建议总结如果只让我说一条最重要的经验我会说**Husky是工程规范落地的第一环但它必须配合“轻量、快速、清晰报错”这个原则。**当时我在团队推动Husky落地的时候最怕的就是钩子里堆了太多任务导致提交一次要等五分钟。一旦开发者觉得提交代码变成一件痛苦的事所有门禁都会变成摆设。所以在设计钩子脚本时优先级永远是速度、准确性和清晰的错误提示。还要记住一点Husky的钩子脚本本身也是代码它需要被维护和演进。我见过一些团队的.husky/pre-commit脚本随着人员更迭已经变得混乱不堪。建议把钩子脚本的维护也纳入代码评审的范围保持简洁、注释清晰、单一职责。最后一个小技巧在调试Husky钩子时你可以直接手动执行.husky/pre-commit来观察脚本输出。如果脚本返回非零退出码提交就会失败。理解“退出码”就是理解Husky的钥匙。所有钩子逻辑本质上都是在说检查通过返回0不通过返回非0。搞明白这一点你就能自由地驾驭Husky为项目设计出更贴合实际流程的质量门禁了。