跳转到内容

第 11 章 · 格式化、lint 与提交钩子

一句话:格式化器统一代码长相,lint 找出可能的错误,提交钩子在 git commit 时自动把两者跑一遍,不合格的代码进不了仓库。 用它还是用别的:Python 用 Ruff 一个工具管格式化和 lint;JS、TS 用 Prettier 管格式化、ESLint 管 lint;钩子用 pre-commit 统一管理,两种语言写在同一个配置文件里。 核对版本:Prettier 3.9、ESLint 10.12、Ruff 0.16、pre-commit 4.6,2026-10

工具 安装 查版本
EditorConfig 不用装,多数编辑器内置支持;VS Code 装扩展,见第 2 章 无
Prettier npm i -D prettier npx prettier --version
ESLint npm init @eslint/config@latest npx eslint --version
Ruff pip install ruff 或 uv tool install ruff ruff --version
pre-commit pip install pre-commit pre-commit --version

Prettier 和 ESLint 装在项目的开发依赖里,用 npx 运行,团队和 CI 用的是 package-lock.json 锁定的同一个版本。npm init @eslint/config@latest 会问要检查哪种文件、用哪种模块写法、有没有框架和 TypeScript、代码跑在浏览器还是 Node,然后装好依赖并写出 eslint.config.js(或 .mjs)。Ruff 和 pre-commit 装进项目的虚拟环境,或用 uv tool install 装成全局命令。

  • 格式化(Formatter):只管长相,缩进、空格、引号、换行、行宽,改完代码意思不变。Prettier 和 ruff format 是格式化器,几乎不留可调的选项,团队不用再争风格。
  • lint(Linter,代码检查):找可能的错误和坏写法,比如用了没定义的变量、导入了没用的模块。ESLint 和 ruff check 是 lint 工具,一部分问题加 --fix 能自动修,剩下的要人改。两者和编辑器的关系见第 3 章。
  • 提交钩子(Git hook):Git 在某些时刻自动运行的脚本,放在 .git/hooks/ 里。pre-commit 钩子在每次 git commit 生成提交之前运行,任何一项失败,这次提交就不生成。
  • pre-commit 框架:用一个 .pre-commit-config.yaml 列出要跑的钩子,自动下载各钩子的运行环境,Python、JS 项目都能用。配置文件提交进仓库,每个人 pre-commit install 一次,钩子就装进自己的 .git/hooks/。
  • 约定式提交(Conventional Commits):提交说明用固定前缀开头,feat: 新功能,fix: 修错,docs: 文档,chore: 杂项。工具能据此自动生成更新日志、决定版本号。

下图是装好钩子后一次提交的路径:钩子改了文件或报了错,提交就中止,改好重新 git add 再提交;通过后推送,CI 再用同样的检查跑一遍,兜住没装钩子的人。

提交钩子和 CI 的两道检查

任务 命令 说明
JS、TS
格式化全部文件 npx prettier --write . 直接改文件,.gitignore 里的不动
只检查格式 npx prettier --check . 不改文件,列出不合格的;CI 里用
lint npx eslint . 有 error 时退出码非 0
lint 并自动修 npx eslint . --fix 修不了的照样报出来
Python
格式化 ruff format . 直接改文件
只检查格式 ruff format --check . 不改文件;CI 里用
lint ruff check . 带 [*] 的是能自动修的
lint 并自动修 ruff check --fix . 如删掉没用的 import;修不了的照样报出来
pre-commit
把钩子装进当前仓库 pre-commit install 每次新克隆后都要运行一次
对全部文件跑一遍 pre-commit run --all-files 只查已被 Git 跟踪的文件
把钩子升到最新版本 pre-commit autoupdate 改写配置里每个 rev,改完要提交
跳过钩子提交 git commit --no-verify 只在紧急时用,CI 仍会拦下

给已有的 Python 项目加提交前检查

Section titled “给已有的 Python 项目加提交前检查”
  1. 在项目的虚拟环境里装 pre-commit:pip install pre-commit。
  2. 在项目根目录写 .pre-commit-config.yaml,内容见 11.5;先把 rev 升到当前版本:pre-commit autoupdate。
  3. 装钩子:pre-commit install。
  4. 对历史代码跑一遍,第一次要下载钩子环境,等几分钟:pre-commit run --all-files。
  5. 钩子改过的文件看一眼,剩下的报错手动改,再跑到全部 Passed:pre-commit run --all-files。
  6. 把配置和修过的文件一起提交:git add -A。
  7. 提交时钩子自动运行:git commit -m "chore: 加 pre-commit 钩子"。
  8. 告诉团队成员拉取后各装一次:pre-commit install。

Prettier 和 ESLint 用项目里装好的版本,钩子写成 repo: local,同样交给 pre-commit 管:

  1. 装 Prettier:npm i -D prettier。
  2. 生成 ESLint 配置,按提示回答:npm init @eslint/config@latest。
  3. 装关掉 ESLint 格式规则的配置,在 eslint.config.js 里 import eslintConfigPrettier from "eslint-config-prettier/flat",放在配置数组最后:npm i -D eslint-config-prettier。
  4. 先在命令行核对两样都能跑通:npx prettier --check . && npx eslint .。
  5. 按 11.1 装好 pre-commit,在 .pre-commit-config.yaml 里写下面的 local 钩子,再装钩子:pre-commit install。
  6. 对全部文件跑一遍并修好:pre-commit run --all-files。
  7. 提交:git commit -m "chore: 加 Prettier、ESLint 和 pre-commit"。
repos:
- repo: local # 用项目里装好的工具(新克隆先 npm ci)
hooks:
- id: prettier
name: prettier
entry: npx prettier --write --ignore-unknown # 不认识的文件类型跳过
language: system # 直接调用本机命令
types: [text] # 只交给它文本文件
- id: eslint
name: eslint
entry: npx eslint --fix
language: system
files: \.(js|mjs|cjs|jsx|ts|tsx)$ # 只检查脚本文件

钩子装在每个人自己的 .git/hooks/ 里,不随仓库走,没装或用了 --no-verify 的提交照样能推上来,CI 是最后一道:

  1. Python 项目用第 10 章 10.4 的模板,里面已有:ruff check . && ruff format --check .。
  2. 把模板里 Ruff 的版本写死成 .pre-commit-config.yaml 里 rev 去掉 v 的那个号:pip install ruff==<版本>。
  3. JS 项目用第 10 章的 Node 模板,里面已有:npx prettier --check . && npx eslint .。
  4. 推送后看检查结果:gh run watch。
配置项 在哪 管什么
.editorconfig 项目根目录 编辑器的缩进、换行符、文件末尾空行
.prettierrc 项目根目录 Prettier 的少数可调项,如行宽、分号、引号
eslint.config.js 项目根目录 ESLint 的规则;package.json 没写 "type": "module" 时用 eslint.config.mjs
[tool.ruff] pyproject.toml Ruff 的行宽、目标 Python 版本、启用哪些规则
.pre-commit-config.yaml 项目根目录 提交时跑哪些钩子、各用哪个版本

这五个文件都提交进仓库,团队成员和 CI 读到的是同一套规则。

最小 .editorconfig。注释必须单独占一行,写在行尾会被当成值的一部分:

# 到这一级为止,不再往上层目录找
root = true
# 以下对所有文件生效
[*]
# 文件编码
charset = utf-8
# 换行符统一用 LF
end_of_line = lf
# 用空格缩进,每级 2 格
indent_style = space
indent_size = 2
# 文件末尾留一个换行,删掉行尾空格
insert_final_newline = true
trim_trailing_whitespace = true
# Python 文件每级缩进 4 格
[*.py]
indent_size = 4

最小 .pre-commit-config.yaml(Python 项目):

repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.16.10 # Ruff 版本,CI 里的 Ruff 要和它一致
hooks:
- id: ruff-check # lint
args: [--fix] # 能修的自动修
- id: ruff-format # 格式化,要放在 ruff-check 之后
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v6.0.0 # 通用小钩子的版本
hooks:
- id: trailing-whitespace # 删行尾空格
- id: end-of-file-fixer # 文件末尾补一个换行
- id: check-yaml # YAML 语法检查

pyproject.toml 里 Ruff 的最小配置:

[tool.ruff]
line-length = 100 # 行宽
target-version = "py312" # 按 Python 3.12 的语法检查
[tool.ruff.lint]
select = ["E", "F", "I", "UP", "B"] # 基本错误、import 排序、新语法、常见坑
  • Prettier 排好的代码,ESLint 又报缩进、引号、分号的问题。 原因:ESLint 开着管格式的规则,和 Prettier 冲突。处理:npm i -D eslint-config-prettier,在 eslint.config.js 里导入 eslint-config-prettier/flat,放在配置数组最后。
  • ESLint couldn't find an eslint.config.* file. 原因:ESLint 9 起默认只读平面配置(Flat Config)eslint.config.*,旧的 .eslintrc.* 不再读取。处理:重新生成配置:npm init @eslint/config@latest,规则搬过去后删掉 .eslintrc.*。
  • 第一次提交卡住好几分钟,输出 Installing environment for ...。 原因:pre-commit 在下载并安装每个钩子的运行环境。处理:等它装完,之后有缓存,换了 rev 才重新装;pre-commit run --all-files 可以提前装好。
  • 提交失败,钩子显示 Failed 和 files were modified by this hook。 原因:预期行为,钩子把文件修好了,但修改还在工作区,没进这次提交。处理:看一眼改动,git add <文件> 后重新 git commit。
  • 别人推上来的代码没排版,lint 报错一堆。 原因:他没运行 pre-commit install,或用了 --no-verify,钩子只在装过的机器上运行。处理:CI 里再跑一遍同样的检查(见 11.4),设成 PR 的必需检查(见第 9 章)。
  • 只改了一行,格式化后 git diff 显示整个文件都变了。 原因:换行符(CRLF 和 LF)或缩进设置和原文件不一致。处理:先统一 .editorconfig 和 .gitattributes(见第 7 章 7.5),单独提交一次全量格式化,再做正常修改。
  • EditorConfig:.editorconfig 每个属性的含义,以及哪些编辑器内置支持、哪些要装插件。
  • Prettier 文档:可调选项、忽略文件的写法,以及和 ESLint 一起用的说明。
  • ESLint 文档:平面配置 eslint.config.js 的写法、规则列表和从 .eslintrc 迁移的指南。
  • Ruff 文档:格式化器和 lint 的全部规则、pyproject.toml 里的配置项。
  • pre-commit:.pre-commit-config.yaml 的全部键、local 钩子的写法和常用钩子列表。
  • 约定式提交(中文):feat、fix 等前缀的规范和破坏性变更的写法。