news 2026/9/7 15:49:00

Git Hooks 实战:用 husky + lint-staged 实现提交前 ESLint 与 Prettier 自动校验

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Git Hooks 实战:用 husky + lint-staged 实现提交前 ESLint 与 Prettier 自动校验

1. 为什么偏要在 commit 之前加一道拦截门

先说个特别常见的尴尬场景:本地写完代码,git commit的时候也没做检查,推到远端后 CI 开始跑 lint 和类型检查,结果红灯亮了。你看着那一长串报错,心里其实很清楚——这个问题在本地两三秒就能发现,完全不该由 CI 来背这个锅。

我做 React 项目这些年,最怕的不是需求复杂,而是代码质量在“提交之后”才暴露问题。尤其是团队协作的时候,每个人的编辑器设置不一样,有人配了保存自动格式化,有人没有;有人习惯写完了手动跑一遍 ESLint,有人到了提测前才发现一堆 warning。这些差异最后都会集中在 commit 历史里,变成 review 时毫无意义的大量 diff。

这篇文章要解决的,就是用 Git Hooks 在提交前设置一道自动校验与格式化的闸门。具体到技术栈,是 Vite + React 19 项目,工具链采用 husky + lint-staged + ESLint + Prettier。核心效果是:你只要执行git commit,暂存区里的代码会先被自动格式化,再被 ESLint 检查,只要有任何 lint 错误或格式问题,提交就会直接失败,错误信息直接打在终端里。

有人可能会说,npm run lint也能做这件事,何必多装一堆工具?区别在于:手动检查靠自觉,自动拦截靠机制。后者不会被任何人遗忘,也不会因为某个同事今天状态不好而漏掉。

这套方案适合谁?适合正在从“一个人写代码”转向“多人协作维护”的团队,也适合任何一个想把自己的 commit 历史收拾得干净利索的个人开发者。它不挑项目大小,React 19、React 18,甚至 Vue 项目都能套用,只是今天所有示例都放在 Vite + React 19 的场景里展开。

1.1 从“CI 亮红灯”到“本地拦下来”:成本差在哪

很多团队把 lint 和格式化放在 CI 里做,理由是“反正机器会自动跑”。但这里有一个非常容易被忽略的成本问题:反馈链路太长。

在 CI 上发现一个格式问题,意味着你已经完成了编码、提交、推送、等待流水线执行这一整套流程。如果这个项目构建速度还慢一点,一次提交可能要等十分钟以上才知道代码过不过。而如果检查发生在本地,在你敲下git commit的那一瞬间,问题就被拦住了。改完重新 add、重新 commit,整个过程不会超过半分钟。

另一个容易忽略的问题是“污染历史”。如果 CI 过了但代码格式很乱,这些乱格式就已经进了 commit 历史。后续做git loggit blame的时候,看到的会是大量格式变更混在功能变更里,排查问题的时候非常痛苦。提交前自动格式化,能把这个污染源直接掐掉。

1.2 Git hooks 的生效时机,以及 pre-commit 为什么最常用

Git Hooks 不是什么新概念,它就是 Git 在执行特定动作时触发的脚本。常见的钩子有pre-commit(提交前)、prepare-commit-msg(编辑提交信息前)、commit-msg(提交信息生成后)、pre-push(推送前)等。

对我们这个场景而言,pre-commit是最合适的一道闸门。因为代码从“本地工作区”进入“版本历史”的最后一关就是 commit,在这之前拦截,能保证所有进入 Git 历史的内容都是被检查过的。

当然,如果你想做到更严格,也可以在pre-push里再跑一次全量检查,防止某些绕过 commit 检查的情况出现。但作为日常开发,pre-commit是最直接、反馈最快的选择。

1.3 这套方案在 Vite + React 19 项目里的特殊注意点

Vite 官方脚手架生成的 React 模板,默认已经帮你配好了 ESLint。但这里有个容易踩坑的地方:Vite 官方模板用的是 ESLint 9 的 flat config 体系,配置文件名不再是.eslintrc.cjs.eslintrc.json,而是eslint.config.js。很多从旧项目迁移过来的同学,第一反应是去找.eslintrc,结果发现根本不存在,一脸蒙。

React 19 也有一个特殊点需要留意:新版本默认使用新的 JSX transform,组件文件里不需要再import React from 'react'。这意味着如果你从旧项目里拷贝了react/react-in-jsx-scope这条规则过来,会直接报错。好在 Vite 官方模板没这个问题,但自己手动搭过配置的人就要警惕。

2. 工具链分工:husky、lint-staged、ESLint、Prettier 各管哪一段

很多人第一次接触这套组合时,最困惑的是工具之间的边界。到底谁负责拦截提交?谁负责格式化?谁负责检查规范?其实分清楚之后,一切都很好理解。

2.1 四件套的职责清单

我直接用一张表把这几个工具的分工理清楚:

工具职责一句话解释
husky管理 Git Hooks负责把pre-commit这类钩子脚本挂到 Git 上,并让团队共享同一套配置
lint-staged筛选暂存区文件只对git add过的文件执行命令,避免每次提交把整个项目 lint 一遍
ESLint静态代码检查检查 React/TypeScript 代码里是否存在错误、未使用变量、Hooks 依赖等问题
Prettier代码格式化统一引号、分号、缩进、换行等风格,让所有人的代码长成一个样

这套组合的逻辑是:husky 负责触发,lint-staged 负责挑选文件,Prettier 负责把格式改对,ESLint 负责揪出真正的代码问题。四者不是竞争关系,而是流水线关系。

2.2 为什么不建议直接手工维护 .git/hooks/pre-commit

有人会问:Git 本身就有.git/hooks/pre-commit这个东西,直接在目录里放一个脚本不就行了?理论上是可行的,但有几个致命问题。

第一,.git目录不会跟着仓库提交,所以这个钩子脚本只在你自己机器上生效,换一台电脑或换一个协作者就没了。第二,初始化脚本非常繁琐。当你git clone一个新的仓库下来,还得手动去.git/hooks里修改配置,这对团队协作来说是灾难。

husky 的价值就在这里:它提供了prepare脚本机制,在每次npm install的时候自动把钩子目录挂好。也就是说,同事拉取代码后只要安装一次依赖,提交前校验就会自动生效,不需要任何额外的手动配置。

2.3 版本边界:ESLint 9、Husky v9、lint-staged v15

写这篇内容的时候,我建议你用这些版本,它们彼此配合更稳:

  • husky: v9.x
  • lint-staged: v15.x
  • eslint: v9.x
  • prettier: v3.x
  • typescript-eslint: v8.x
  • eslint-plugin-react-hooks: v5.x
  • eslint-config-prettier: v10.x

这里要特别注意 husky。v9 版本的初始化方式和 v4/v8 完全不同,网上很多教程还在教npx husky install这套老流程,实际上在 v9 里已经被淘汰了。如果你照抄老教程,大概率会遇到“钩子文件生成了但就是不生效”的诡异问题。

2.4 pnpm 用户先看这里

如果你不是 npm 而是 pnpm 用户,需要在第 4 节的操作里额外留意一个点:pnpm 默认会拦截依赖包的 postinstall 脚本,而 husky 恰恰是用prepare脚本来完成初始化的。

如果你在安装 husky 后发现钩子根本没装上,第一件事就是去项目根目录跑一下pnpm rebuild husky,或者直接手动执行一下prepare脚本。这个问题非常隐蔽,报错也很少,但它真实存在于日常使用中。

3. 先把“地基”打好:Vite + React 19 里的 ESLint 与 Prettier 配置

在接 Git Hooks 之前,我习惯先把 ESLint 和 Prettier 单独跑通。原因很简单:如果命令本身就没配好,放进 hook 里也只会得到一堆莫名其妙的报错,排查起来更难。

3.1 从空项目到 ESLint 9 flat config 正常跑起来

创建一个 Vite + React 19 + TypeScript 项目:

npm create vite@latest react19-hooks-demo -- --template react-ts cd react19-hooks-demo npm install npm run lint

刚初始化完的项目,Vite 模板已经生成了eslint.config.js,并且默认配好了eslint-plugin-react-hookseslint-plugin-react-refresh。这里的npm run lint实际执行的是eslint .,全项目扫描。

如果你用的是官方模板,这一步通常能直接通过。但如果你是从老项目迁移过来的,需要手动建立 flat config,我给你一个可以直接用的版本:

import js from '@eslint/js' import globals from 'globals' import reactHooks from 'eslint-plugin-react-hooks' import reactRefresh from 'eslint-plugin-react-refresh' import tseslint from 'typescript-eslint' import prettier from 'eslint-config-prettier' export default tseslint.config( { ignores: ['dist'] }, { extends: [js.configs.recommended, ...tseslint.configs.recommended], files: ['**/*.{ts,tsx}'], languageOptions: { ecmaVersion: 2020, globals: globals.browser, }, plugins: { 'react-hooks': reactHooks, 'react-refresh': reactRefresh, }, rules: { ...reactHooks.configs.recommended.rules, 'react-refresh/only-export-components': [ 'warn', { allowConstantExport: true }, ], }, }, prettier )

注意最后一行那个prettier。这不是 Prettier 插件,而是eslint-config-prettier,它的作用是关掉 ESLint 中与 Prettier 格式规则冲突的那些规则。很多教程忽略这一步,结果 ESLint 和 Prettier 在同一段代码上打架,一个要分号一个不要分号,勾子永远过不去。

3.2 给 Prettier 一个明确且可执行的格式化基线

Prettier 的默认配置已经足够合理,但仍建议在项目根目录放一个.prettierrc文件,把团队约定固化下来:

{ "semi": false, "singleQuote": true, "printWidth": 100, "trailingComma": "es5", "endOfLine": "auto" }

我个人的偏好是无分号、单引号、每行 100 字符。这不是标准答案,重要的是团队统一。配置里最后那个endOfLine: "auto"建议保留,它可以减少 Windows 和 macOS 之间因为换行符导致的文件被大量改动的问题。

然后安装并验证:

npm install -D prettier eslint-config-prettier npx prettier --check src/

如果输出一堆文件名,说明文件没有格式化;执行npx prettier --write src/就能自动格式化。后面接入 lint-staged 时,我们会在提交时自动执行这个动作。

3.3 React 19 和 ESLint 配置之间的两个隐藏关系

第一个隐藏关系是 JSX 转换。React 19 使用新的 JSX transform,组件文件不用再引入 React。如果是从老项目迁移,记得不要在 ESLint 中开启react/react-in-jsx-scope,否则每个组件文件都会报'React' is not defined或者需要你引入一个根本用不到的依赖。

第二个隐藏关系是 Hooks 插件版本。React 19 的eslint-plugin-react-hooks版本已经到 v5,如果你还是 v4,某些新规则可能无法识别,尤其是针对函数组件和自定义 Hook 的依赖检查。建议直接安装最新版:

npm install -D eslint-plugin-react-hooks@latest

4. 接入 husky 与 lint-staged:真正实现提交前自动校验和格式化

这一步是整个方案落地的核心。前面所有的配置,到这一步都会被串起来。

4.1 安装 husky:一条 init 命令背后的机制

安装 husky 分两步:

npm install -D husky npx husky init

执行npx husky init之后,husky 会在项目根目录生成一个.husky文件夹,里面默认包含一个pre-commit文件。同时在package.json里加上这样一段脚本:

"scripts": { "prepare": "husky" }

这里的prepare是 npm 的生命周期脚本。每次项目执行npm install的时候,npm 都会自动运行它,husky 会用这个时机把 Git hooks 指向.husky目录。这也是为什么前面说“同事拉完代码装一次依赖,钩子就自动生效了”。

husky 默认生成的.husky/pre-commit内容一般是这样的:

npx lint-staged

如果只想先测试 hooks 机制,可以先把这一行改成echo "hook working",提交一次,看到终端打印出这句话,说明钩子已经生效,再改回来。

另外,可以用这条命令确认 hooks 路径确实被改过:

git config --get core.hooksPath

正常会输出.husky/_,这是 husky 在内部管理脚本用的目录。

4.2 写准 lint-staged 配置:让 lint 和 format 有序执行

在项目根目录新建lint-staged.config.js

export default { '*.{ts,tsx,js,jsx}': ['eslint --fix', 'prettier --write'], '*.{json,css,md,html}': ['prettier --write'], }

这个配置的含义是:当暂存区里有 TypeScript/JavaScript 文件时,先执行eslint --fix,把能自动修复的问题修掉;然后执行prettier --write,把格式统一。其他类型的文件,只做格式化。

注意这里eslint --fixprettier --write的顺序是有讲究的。我习惯先跑 ESLint 再跑 Prettier,因为 ESLint 修复过的代码可能还有格式问题,最后交给 Prettier 统一收尾,能保证输出的一致性。

如果项目里用了 Tailwind CSS 之类的工具,也可以在这个配置里补上 class 排序插件,逻辑一模一样。

4.3 跑一次真实提交,看它如何把“坏文件”拦在门口

模拟一次真实场景。我故意在src/App.tsx里写了一段没有分号、引号格式混乱还带未使用变量的代码,然后执行:

git add src/App.tsx git commit -m "test bad commit"

此时终端会出现类似这样的输出:

✔ Preparing lint-staged... ✔ Running tasks for staged files... ✖ eslint --fix found some errors. Please fix them and try again.

提交失败。这是因为 ESLint 发现了一些需要手动修复的问题,比如未使用的变量。像这种问题--fix修不掉,只能由开发者手工处理。

把那个未使用的变量删掉以后再试:

git add src/App.tsx git commit -m "test good commit"

这次 lint-staged 会把prettier --write改完的内容重新添加进暂存区,然后正常生成 commit。

这里有个容易忽略的细节:lint-staged 执行完后,Git 会重新 add 被修改过的文件,所以你的 commit 里包含的是格式化后的版本,而不是格式化前的版本。这一点非常重要,如果你没有用 lint-staged,而是自己在 hook 里写npm run lintnpm run format,格式化后的文件不会自动回到暂存区,提交进去的仍然是旧版本。

5. 接入后最容易翻车的几个场景与排查方法

工具链搭好只是开始,真正让人头疼的是接入之后遇到的各种“不生效”。下面这几个场景,几乎每个项目组都会碰到。

5.1 提交时 hooks 完全没出现

如果你执行git commit时,终端没有任何额外输出,说明 hooks 根本没有挂上。按这个顺序排查:

git config --get core.hooksPath

如果输出是.husky/_,说明 husky 管理了 hooks,问题可能在脚本本身。手动执行一次:

bash .husky/pre-commit

看看会不会报错。如果是 husky 新装的但 never 生效,检查项目根目录的.husky/pre-commit文件是否存在,内容是否为npx lint-staged,以及是否有执行权限。

如果core.hooksPath返回空,说明 husky 没有初始化成功。大概率是npm installprepare脚本没执行,可以手动跑一下:

npm run prepare

5.2 lint-staged 只改了暂存文件,但格式化结果没进提交

这种情况通常出现在直接使用prettier --write而不是 lint-staged 时。前面我强调过,lint-staged 会在处理完文件后自动执行git add,但如果你自己在 hook 里写的是:

npx prettier --write . npx eslint --fix .

那么这些命令虽然修改了工作区的文件,却不会自动把你的修改加进暂存区,最后提交进去的还是格式修改前的版本。更严重的是,因为你跑的是全量检查,可能把项目里原本就存在的旧错误也一并暴露出来,导致提交一直失败,但其实跟你这次改动无关。

解决方案就是回到 lint-staged 的正确配置上,不要自己写裸命令。

5.3 ESLint 9 下的配置冲突让提交一直被卡

ESLint 9 全面采用 flat config 之后,很多旧插件在新体系下会出现兼容问题,典型表现是eslint.config.jsplugin名称无法解析。如果你遇到了这类报错,先确认所有相关包的版本是支持 ESLint 9 的。

另一个常见冲突是 ESLint 规则和 Prettier 格式规则“互相打架”。表现是:eslint --fix改完,prettier --write又改回来,来回震荡,每次提交都有大量 diff。这时候检查一下有没有引入eslint-config-prettier,它会把 ESLint 里所有和格式化相关的规则关掉,让两者各管各的。

如果你用的是tseslint.config()这种辅助函数,记得把prettier配置对象放在整个数组的最后,这样它才能覆盖掉前面可能开启的冲突规则。

5.4 被人用 --no-verify 绕过以后

最后聊一个偏“纪律”的问题。git commit --no-verify可以跳过所有 Git Hooks,这是 Git 本身的机制,也是紧急情况下的逃生通道。

但必须有心理准备:一旦团队成员习惯了--no-verify,再好的自动化拦截也会变成摆设。我的建议是,把提交前校验当作“默认约定”,紧急场景下确实可以临时跳过,但跳过后要在 commit message 里留一句标记,方便后面回查。

另外,不要在 hook 里把所有检查都做完。比如完整的单元测试和构建,我认为更适合放在 CI 或 pre-push 阶段,而不是 pre-commit。因为 pre-commit 追求的是快,如果一次提交要等三分钟跑测试,开发者会想尽一切办法绕过它。把 lint、格式化这类“秒级”检查留在 pre-commit,把重活交给 CI,反而更容易落地。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/7 15:48:52

FileZilla Server全栈实操:从安装到端口映射与权限管理

只要碰过服务器文件备份、公司资料交接、网站目录维护这类活儿,FileZilla这个名字一定绕不开。但很多人对它的印象只停留在“一个FTP客户端”,需要下载文件时打开连一下,完事就关掉。这其实浪费了FileZilla最大的一层价值——它根本不是单一软…

作者头像 李华
网站建设 2026/9/7 15:48:47

服务器安全基线核查脚本实战:Linux与Windows检查项设计与排错

简介:面向运维安全人员的Windows与Linux基线核查脚本资源包,用于帮助企业IT、安全运维及等保合规建设人员快速完成系统安全配置检查。包内整合了Windows与Linux两套核查脚本、配套基线配置文档、配置文件、结果导出报表以及常见报错处理指南,…

作者头像 李华
网站建设 2026/9/7 15:46:25

论文降AI率实战指南:从检测原理到10分钟流水线

前几天凌晨一点多,一个读研的朋友给我发消息:“论文查重过了,AI率又爆了,降到明天中午交,怎么办?”我看了眼他那句话——“综上所述,本文将通过对……进行深入分析,以期为实现……提…

作者头像 李华
网站建设 2026/9/7 15:46:15

C++命名空间从入门到实战:语法、原理与避坑指南

1. 先从“重名爆炸”说起:为什么我们需要命名空间如果只用一句话回答“命名空间解决什么问题”,那就是:它让同名不同命的东西能和平共处。接触过C语言的朋友应该都有这种经历:在一个稍大一点的项目里,全局变量、函数名…

作者头像 李华
网站建设 2026/9/7 15:46:08

C++多态底层原理:虚函数表与内存布局全解析

1. 从一次“诡异”的线上崩溃聊起:多态到底是什么如果你写了几年C,一定在某个深夜里被多态搞到怀疑人生。我印象最深的一次,是在一个IM服务器的消息处理模块里,客户端消息类型有十几种,我写了一个MessageHandler的基类…

作者头像 李华