ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

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

2026/9/7 15:49:01 拓冰建站 浏览量
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 log、git 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.xlint-staged: v15.xeslint: v9.xprettier: v3.xtypescript-eslint: v8.xeslint-plugin-react-hooks: v5.xeslint-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 vitelatest react19-hooks-demo -- --template react-ts cd react19-hooks-demo npm install npm run lint刚初始化完的项目Vite 模板已经生成了eslint.config.js并且默认配好了eslint-plugin-react-hooks和eslint-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-hookslatest4. 接入 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.jsexport default { *.{ts,tsx,js,jsx}: [eslint --fix, prettier --write], *.{json,css,md,html}: [prettier --write], }这个配置的含义是当暂存区里有 TypeScript/JavaScript 文件时先执行eslint --fix把能自动修复的问题修掉然后执行prettier --write把格式统一。其他类型的文件只做格式化。注意这里eslint --fix和prettier --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 lint和npm 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 install时prepare脚本没执行可以手动跑一下npm run prepare5.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.js里plugin名称无法解析。如果你遇到了这类报错先确认所有相关包的版本是支持 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反而更容易落地。