编辑器的保存时格式化依赖每个开发者主动配置。但现实中总有人忘了装扩展、忘了开启设置、或者使用不支持的编辑器。这些情况下,未格式化的代码会通过 git commit 流入代码库。
Git Hooks 就是针对这个问题的兜底方案。它在代码提交之前自动运行格式化,无论开发者的编辑器配置如何,格式化都会发生。
Husky 是一个让 Git Hooks 配置变得简单的工具。Git 原生的 hooks 是放在 .git/hooks/ 目录下的 shell 脚本,但这个目录不会被 git 跟踪,团队成员无法共享。Husky 解决了这个问题——它让 Git Hooks 配置文件可以被提交到代码库中,团队成员拉取代码后自动生效。
安装和配置 Husky:
# 安装 npm install --save-dev husky # 初始化(创建 .husky/ 目录,配置 Git hooks 路径) npx husky init # 创建 pre-commit hook echo "npx lint-staged" > .husky/pre-commit
配置完成后,每次 git commit 时,.husky/pre-commit 中的命令会自动执行。如果命令失败(退出码非 0),commit 会被中断——代码不会被提交。
如果你在 pre-commit 中运行 prettier --write .,它会格式化整个项目。但 pre-commit 应该只处理本次提交涉及的文件——处理所有文件既浪费时间,又可能产生不相关的格式变更。
lint-staged 解决了这个问题。它只对 git add(暂存区)中的文件运行指定的命令。
安装和配置:
npm install --save-dev lint-staged
在 package.json 中添加配置:
{ "lint-staged": { "*.{js,jsx,ts,tsx,css,json,md}": "prettier --write", "*.{js,jsx,ts,tsx}": "eslint --fix" } }
这个配置的含义:对于所有暂存区中的 .js/.jsx/.ts/.tsx/.css/.json/.md 文件,运行 prettier --write;对于 .js/.jsx/.ts/.tsx 文件,额外运行 eslint --fix。
当开发者执行 git commit 时,工作流是这样的:
注意 lint-staged 会在格式化完成后自动重新 git add——把格式化后的文件加回暂存区,让 commit 包含格式化后的版本。这是关键的一步,否则 commit 会包含格式化之前的旧版本。
一个典型的项目配置:
// package.json { "devDependencies": { "prettier": "^3.0.0", "eslint": "^8.0.0", "eslint-config-prettier": "^9.0.0", "husky": "^9.0.0", "lint-staged": "^15.0.0" }, "lint-staged": { "*.{js,jsx,ts,tsx,css,json,md,yml,yaml}": [ "prettier --write" ], "*.{js,jsx,ts,tsx}": [ "eslint --fix" ] } }
# .husky/pre-commit npx lint-staged
// .prettierrc { "printWidth": 100, "singleQuote": true }
这套配置的运行效果:每次 git commit 时,只对暂存区的文件运行 Prettier 和 ESLint,格式化后的文件自动重新暂存。如果 ESLint 发现无法自动修复的质量问题(退出码 1),commit 中断,开发者必须先修复。
如果你的项目很简单,不想引入 lint-staged 的额外依赖,可以在 pre-commit 中直接用 git 命令过滤文件:
# .husky/pre-commit(不用 lint-staged 的版本) git diff --cached --name-only --diff-filter=ACM \ | grep -E '\.(js|jsx|ts|tsx|css|json|md)$' \ | xargs -r npx prettier --write git update-index --again
这个脚本做了三件事:获取暂存区中新增或修改的文件列表、过滤出需要格式化的类型、对它们运行 Prettier。git update-index --again 等同于 lint-staged 的重新暂存操作。
但手动脚本不如 lint-staged 健壮——它不会像 lint-staged 那样处理文件路径中的空格、不会自动恢复因格式化失败导致的暂存区状态。在正式项目中,lint-staged 仍然是推荐的选择。
lint-staged 的配置除了字符串命令,还支持数组形式,按顺序执行多个命令:
{ "lint-staged": { "*.{js,jsx,ts,tsx}": [ "eslint --fix", "prettier --write" ], "*.md": [ "prettier --write", "markdownlint --fix" ] } }
数组里的命令全部成功才算通过。如果某个文件同时匹配多个 glob(比如 a.ts 同时被两条规则命中),lint-staged 会执行所有匹配的命令,但不会重复传入同一个文件。
一个实用的顺序是"先 Prettier 后 ESLint":Prettier 先把格式统一,ESLint 再修质量规则。顺序反过来可能导致 ESLint 修复后又被 Prettier 重新排版,陷入来回修改。
配置文件除了 package.json,也可以放在独立的 lint-staged.config.js:
module.exports = { "*.{js,jsx,ts,tsx}": "eslint --fix && prettier --write", };
对于多包仓库,还可以在子包各自的 package.json 里配置 lint-staged,让它按包范围运行。
hook 没有执行。先检查 .husky/ 目录是否存在、pre-commit 文件是否有执行权限;用 git config core.hooksPath 查看 hooks 路径是否为 .husky;如果从旧版 Husky 升级,确认初始化命令已经跑过。
pre-commit 报错导致无法提交。lint-staged 会中断整个 commit。修复方式是先修文件再重新提交。如果格式化成功但后续命令失败,检查命令顺序和退出码。
钩子被 --no-verify 绕过。Husky 无法阻止开发者用 git commit --no-verify 跳过钩子,这是 Git 的设计,不是配置错误。应对方式是在团队规范里约定禁止,并靠 CI 检查兜底——这正是下一节的内容。
Windows 换行符问题。如果仓库开启了 autocrlf,Prettier 格式化后可能出现"文件没改却显示有 diff"的情况。建议在 .editorconfig 里统一 end_of_line = lf,并用仓库级 .gitattributes 固定换行符处理策略。
Husky v9 简化了配置流程,不再需要在 package.json 中配置 prepare 脚本。直接运行 npx husky init 即可设置好 Git hooks 路径。如果你在升级旧项目,注意 v9 的 pre-commit 文件需要添加执行权限:
chmod +x .husky/pre-commit
Git Hooks 是 Prettier "无感自动化"的第二层。它比编辑器集成更可靠——不依赖开发者的个人配置,所有经过 commit 的代码都会被格式化。但 Git Hooks 只保护了 commit 这一个入口。如果有人通过其他方式修改文件(比如直接在 GitLab/GitHub 的网页编辑器中修改),格式化就不会发生。下一节的 CI/CD 检查是第三层保障。
