接手一个写了两年的老项目,最让人头疼的往往不是业务逻辑有多绕,而是代码风格乱得让人抓狂。有人喜欢双引号,有人坚持单引号;有人缩进两个空格,有人非要四个;更别提那些注释掉的代码、没用到的 import,以及一提交就让 CI 变红的格式错误。我在经历过几次“格式化提交”把整个 PR 的 diff 搞得没法看之后,终于沉下心整理了一套代码整理小工具,把格式化、静态检查、无用代码清理、提交前强制检查串成一条自动流水线。现在每天提交代码前只需要跑一条命令,其它事都交给工具。
这篇内容适合正在维护老项目的人、带小团队的开发组长,以及所有厌倦了在 code review 里争论“这里该不该换行”的普通写码人。我会把工具选型、配置思路、实际落地的步骤和踩过的坑一次讲清楚,你可以直接照着抄,也可以根据自己项目的语言和团队习惯做替换。
1. 内容整体设计与思路拆解
1.1 为什么“整理代码”不能靠人的自觉
代码整理这件事,听起来好像很简单:大家约定一个风格,写代码的时候注意一点不就行了?但实际操作过的人都知道,这几乎不可能靠自觉完成。原因是人的注意力是有限的,你在思考一个复杂的状态流转时,根本顾不上这个变量名下面是不是多了一个空行,也顾不上这次提交是不是把调试用的 console 语句带进去了。
更麻烦的是,每个人对“整洁”的理解都不一样。后端同事觉得行宽 120 没问题,前端同事觉得超过 100 就该换行;有人喜欢 import 按字母序排,有人喜欢按路径长短排。这些差异在没有工具约束时,最终都会变成 code review 里的无效讨论,既消耗耐心,又掩盖了真正重要的代码逻辑问题。
所以我在设计这套整理流水线的时候,首先定的原则就是:凡是机器能判断的,绝对不让人去判断。格式、排序、无用引用、明显的错误写法,全部交给工具处理。人的精力应该花在架构设计、业务边界和真正的逻辑正确性上。
1.2 整理工具的三个层次:格式化、静态检查、结构清理
很多刚开始接触代码整理的人,会把“格式化”和“代码检查”混为一谈。实际上它们解决的是不同层次的问题。
第一层是格式化,解决的是“看起来乱”的问题。比如缩进不统一、字符串引号不一致、行尾有没有多余空格、换行位置是否合理。这类工具的代表是 Prettier、Black、gofmt,它们的特点是没有太多可配置项,直接按既定规则重写整个文件。你不需要关心规则细节,只需要接受它的审美。
第二层是静态检查,解决的是“写得不规范”的问题。比如声明了变量但没使用、用了已废弃的 API、隐式类型转换可能引发的 bug、数组方法的回调缺少返回值。这类工具的代表是 ESLint、Ruff、PyLint,它们能发现不是错误但很容易演变成错误的问题。
第三层是结构清理,解决的是“代码存在但不该存在”的问题。比如未使用的 import、注释掉的死代码、永远不会被调用的函数。这类工作通常由 vulture、knip 这类工具完成,也可以在静态检查规则里配置一部分。
这三层缺一不可。只做格式化,代码确实整齐了,但浅层问题还会不断出现;只做静态检查,虽然能发现一些问题,但代码风格仍然五花八门。我把它们全部纳入流水线后,才真正感受到什么叫做“提交代码没有心理负担”。
1.3 设计原则:配置进仓库、本地先跑、CI 兜底
工具链搭好之后,我给自己定了三条使用原则,这里直接分享出来。第一,所有配置必须提交进仓库,包括编辑器配置、格式化配置、检查规则、钩子脚本,任何人克隆下来跑一遍安装命令,就能获得完全一致的环境。第二,所有整理动作必须能本地一键执行,我已经受够了打开文档照着一条条敲命令的日子,能写进脚本的命令绝不手动执行。第三,CI 里必须加一道只读检查,它不负责改代码,只负责告诉你这次提交有没有通过规范校验。
按这个思路做下来,整理工具就从“某个人的某台机器上跑的东西”变成“团队共同遵守的刚性标准”。新人来了不用问“我们项目用什么风格”,跑一条命令,所有文件都会被自动处理成符合规范的形态。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 工具选型解析:找到适合自己的整理组件
2.1 前端/JavaScript 技术栈常用组合
前端生态里,Prettier 加 ESLint 是过去几年用得最广的组合,我自己的项目也一直用这套。Prettier 负责格式化,ESLint 负责检查代码质量问题。两者各管一摊,基本不会打架,只要注意把 ESLint 里跟格式相关的规则关掉就好。
| 工具 | 作用 | 主要解决的问题 | 常见替代方案 |
|---|---|---|---|
| Prettier | 代码格式化 | 统一缩进、引号、换行、空格等纯风格问题 | Biome(Rust 实现,速度更快) |
| ESLint | 静态检查 | 未使用变量、不可达代码、危险写法等质量问题 | Biome、JSHint |
| lint-staged | 暂存区过滤 | 只检查本次提交涉及的文件,避免全量检查太慢 | filter-lint-annoying 等脚本方案 |
| Husky | Git 钩子管理 | 在 commit 前自动触发检查和格式化 | lefthook、pre-commit 框架 |
如果你是新项目,或者受够了 Prettier 加 ESLint 的双重配置,可以试试 Biome。它用 Rust 写的,启动速度和执行速度都比原组合快很多,而且内置了格式化器和 linter,一个工具搞定两件事。我个人的建议是:老项目别折腾迁移,新项目可以大胆尝试。
2.2 Python 技术栈常用组合
Python 项目我用的组合是 Black 加 isort 加 Ruff,再用 vulture 做深度死代码扫描。Black 是出名的“不可配置”格式化器,它的哲学是:别跟我争论缩进和换行,我用我的审美帮你统一。isort 专门负责把 import 语句整理成标准顺序,分组、排序都按 PEP8 的推荐方式。Ruff 是这两年很火的 linter,用 Rust 写的,速度比 Flake8 快很多,而且底层复用了很多 Flake8 插件的规则。
| 工具 | 作用 | 主要解决的问题 | 使用建议 |
|---|---|---|---|
| Black | 代码格式化 | 统一换行、缩进、空格 | line-length 建议设为 88 或 100,团队里先定好 |
| isort | import 排序 | import 语句分块、按字母排序 | profile 设为 black,避免和 Black 冲突 |
| Ruff | 静态检查 | 未使用变量、语法问题、常见反模式 | 规则集可以先开 E、F,逐步叠加 |
| vulture | 死代码检测 | 未使用的函数、类、变量和 import | 最小置信度建议从 60 起步,慢慢调高 |
这套组合最让 Python 开发者省心的地方在于,Black 和 isort 的配置可以直接在 pyproject.toml 里统一声明,配合 pre-commit 框架,提交前会自动跑完所有整理动作。
2.3 通用基建:EditorConfig、Git 钩子和工具链选择标准
除了各语言的格式化器和 linter,我还强烈建议在所有项目里加一个 .editorconfig 文件。它不负责格式化内容,只管最基础的缩进类型、缩进宽度、文件编码、行尾符这些元信息。只要你用的编辑器装了 EditorConfig 插件,打开文件的那一刻缩进就会自动切到项目想要的模式,几乎无感。
Git 钩子这块,前端项目我用 Husky 加 lint-staged,Python 项目用 pre-commit 框架,其他语言项目用 lefthook 比较多。它们的共同点是都能在 commit 之前拦截一次,把整理动作强制嵌入工作流。三者的选择标准很简单:你的项目主语言是什么,优先用那个生态里维护最活跃的钩子工具。
选择整理工具时,我还有一个额外判断标准:先看它能不能自动修复,再看它的可配置项是否克制。能自动修复的工具才有资格进入流水线,否则就只是多一个“提出意见但没人改”的报告。可配置项太多反而容易让团队陷入规则讨论,像 Prettier 和 Black 这种“只有几个选项”的工具,才是最适合作为基础摩擦力的存在。
3. 实操过程与核心环节实现:搭建完整整理流水线
3.1 从零配置一个 JavaScript 项目的整理工具链
我先用一个前端项目做例子,展示从安装依赖到配置完成的全过程。假设项目用的是 pnpm,Node 版本是 20 以上,ESLint 用当前主流的 flat config 方式。
先安装基础依赖:
bash复制pnpm add -D prettier eslint eslint-config-prettier typescript-eslint
然后创建 .prettierrc.json,我的常用配置是:
json复制{
"semi": true,
"singleQuote": true,
"tabWidth": 2,
"trailingComma": "all",
"printWidth": 100,
"endOfLine": "lf"
}
这里说明一下每个字段的意图。semi 控制语句末尾的分号,我选择始终保留,因为 JavaScript 的自动分号插入机制在某些情况下会引发难以排查的问题。singleQuote 选 true 纯粹是团队审美,代码里不需要写大量的字符串拼接,单双引号的影响很小。printWidth 设置为 100,是我测量团队绝大多数笔记本屏幕宽度后的折中选择,120 太长,80 又太容易换行。
接着创建 ESLint 的配置文件 eslint.config.js,最简形态长这样:
javascript复制import js from '@eslint/js';
import prettier from 'eslint-config-prettier';
import tseslint from 'typescript-eslint';
export default tseslint.config(
{
ignores: ['dist', 'node_modules', 'coverage'],
},
js.configs.recommended,
...tseslint.configs.recommended,
prettier,
);
这里最关键的一步是把 prettier 放在数组最后。eslint-config-prettier 的作用是把 ESLint 里所有与格式化相关的规则关掉,避免它和 Prettier 的规则互相冲突。如果顺序不对,你可能发现代码被 Prettier 格式化成一种样式,ESLint 又报错要求改成另一种样式。
然后配置 package.json 的 scripts:
json复制{
"scripts": {
"format": "prettier --write .",
"format:check": "prettier --check .",
"lint": "eslint . --fix",
"lint:check": "eslint ."
}
}
本地开发时,我习惯手动跑 format 和 lint 把整个仓库扫一遍,让工具自动修复能修的问题。CI 里跑步带 --fix 的 check 命令,只校验不做修改。
3.2 Python 项目的配置示例与关键参数说明
接下来看 Python 项目的配置。创建 pyproject.toml,内容如下:
toml复制[tool.black]
line-length = 100
target-version = ["py311"]
[tool.isort]
profile = "black"
line_length = 100
[tool.ruff]
line-length = 100
target-version = "py311"
[tool.ruff.lint]
select = ["E", "F", "W", "I"]
这里有一个很容易踩的坑:isort 的 profile 必须设置为 black,否则它默认的排序规则和 Black 的格式化规则会在个别场景下冲突,明明 import 顺序已经改好了,再跑一遍 Black 还会做调整。把 profile 对齐之后,两个工具会用同一套审美标准处理 import 块。
line-length 我统一设置为 100,和前端项目保持一致。如果你之前用的项目都用 88 或者 120,其实也没问题,关键是整个团队在一个仓库里只能有一个值。我的建议是别选 80,在现在普遍使用宽屏显示器的环境下,80 太容易触发换行,严重影响阅读连续逻辑时的体验。
接着配置 pre-commit,在项目根目录创建 .pre-commit-config.yaml:
yaml复制repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.6.0
hooks:
- id: trailing-whitespace
- id: end-of-file-fixer
- id: check-yaml
- repo: https://github.com/psf/black
rev: 24.4.2
hooks:
- id: black
- repo: https://github.com/pycqa/isort
rev: 5.13.2
hooks:
- id: isort
args: ["--profile", "black"]
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.5.0
hooks:
- id: ruff
args: [--fix, --exit-non-zero-on-fix]
安装并激活钩子的命令是:
bash复制pip install pre-commit
pre-commit install
这里唯一需要解释的是 --exit-non-zero-on-fix 这个参数。默认情况下 Ruff 自动修复完代码后,会返回 0 让提交继续。加上这个参数后,只要它修改了文件,就会返回非零状态,让那一次提交被打断。这样做的目的是强制开发者盯一下被修改的代码,防止自动修复在你不注意的时候引入了意外改动。
3.3 把整理动作嵌进 Git 提交流程
前端项目嵌入 Git 钩子,我用 Husky 加 lint-staged。先装依赖:
bash复制pnpm add -D husky lint-staged
npx husky init
Husky 初始化后会在 .husky 目录下生成 pre-commit 文件,打开它写入:
bash复制pnpm exec lint-staged
然后在 package.json 中配置 lint-staged 的行为:
json复制{
"lint-staged": {
"*.{js,ts,jsx,tsx}": ["eslint --fix", "prettier --write"],
"*.{json,md,css,html}": ["prettier --write"]
}
}
lint-staged 的核心价值在于只处理暂存区里的文件。大型项目文件很多,全量跑一遍 ESLint 和 Prettier 可能要几十秒甚至几分钟,但一次提交通常只涉及几个文件,lint-staged 能把耗时压缩到一两秒。另外它还有一个隐藏优点:自动把格式化后的文件重新加进暂存区,不会出现“你提交了但没存上格式化结果”的尴尬。
Python 项目已经在 pre-commit 里配好了钩子,逻辑是一样的:每次提交前,先跑 pre-commit 检查所有钩子,通过之后才允许 commit。如果某个钩子改动了文件,你需要重新 git add 再 commit 一次。
3.4 一键整理脚本和 CI 里的只读检查
为了让日常操作尽量简单,我在项目根目录维护了一个 Makefile,把常用的整理命令集中起来,写出来供你参考:
makefile复制.PHONY: format lint fix
format:
prettier --write .
black .
isort .
lint:
eslint .
ruff check .
fix:
eslint . --fix
ruff check . --fix
prettier --write .
black .
isort .
如果你所在团队不习惯用 Makefile,写一个 shell 脚本或者 npm scripts 完全可以,效果一样。我的习惯是把 fix 作为日常开发的主入口,写完一段代码跑一次,让工具把所有能修的小问题一次性处理完毕。
CI 端的检查我用 GitHub Actions 实现,配置如下:
yaml复制name: code-style-check
on:
pull_request:
push:
branches: [main]
jobs:
check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- run: pnpm install
- run: pnpm format:check
- run: pnpm lint:check
CI 这一步的工作方式很简单:format:check 和 lint:check 都是只读模式,发现问题直接报错,不会自动改文件。这样做的意义在于,本地工具偶尔会因为各种原因没跑,或者被人直接跳过,CI 就成了最后一道防线,保证合并到主分支的代码永远符合团队规范。
4. 常见问题与排查技巧实录
4.1 Prettier 和 ESLint 规则冲突怎么办
这套工具链里最常见的冲突,就是 Prettier 把代码格式化成一种样子,ESLint 却报格式错误要求改成另一种样子。比如 Prettier 默认会在函数参数超出换行长度时把每个参数单独放一行,但 ESLint 的 max-len 或 indent 规则可能会有不同要求。
解决思路有两个。第一,在 ESLint 配置末尾加上 eslint-config-prettier,这是绝大多数场景下的标准解法,它会把 ESLint 中所有跟格式化相关的规则全部关掉,让格式问题完全交给 Prettier 处理。第二,如果你用了一些额外的插件或自定义规则,并且它们跟 Prettier 存在冲突,优先以 Prettier 为准,因为格式化的判断交给更专业的工具是合理的分工。
我在实际项目中还遇到过一种情况:某次升级依赖后,突然出现一堆格式相关的报错,排查半天才发现是新版本 ESLint 增加了新的规则,且默认开启。处理办法很简单,查看 changelog,在配置里显式关掉冲突规则。所以升级依赖后跑一次全量检查,是很有必要的习惯。
4.2 格式化工具导致巨大 diff,Code Review 没法做
这个问题在老项目里尤其常见。你第一次把所有文件都跑了一遍 Prettier 或 Black,然后创建了一个拉取请求,结果几千行 diff 全是格式变化,真正要 review 的逻辑改动淹没在茫茫的换行和引号修改中,这种体验我经历过很多次。
稳妥做法是:把“纯格式化”和“逻辑改动”拆分成两个独立的提交。第一个提交只做格式化,不包含任何逻辑调整;第二个提交才是真正改需求的内容。这样 review 第一个提交时只需确认没有明显问题,第二个提交的 diff 才会是干净的。
还有一个值得注意的细节:格式化会改变大量行的哈希值,导致 git blame 追踪不到真正的改动来源。你可以约定在项目文档里记录“全量格式化日期”,之后排查历史时就不会被误导。如果你的 Git 版本较新,也可以把那次格式化提交配置为 blame 忽略提交,具体做法是执行:
bash复制git config blame.ignoreRevsFile .git-blame-ignore-revs
然后在该文件中写入那次格式化提交的哈希值。
4.3 自动修复改坏了代码怎么办
自动修复不是绝对安全的。ESLint 和 Ruff 里大部分 fix 规则是安全的,但少数规则会改变语义。我遇到过的真实案例是:某条规则自动删掉了一个“看似没用到”的变量,结果那个变量在运行时通过闭包被外部访问,删除后直接线上报错。
我的规避策略有两条。第一条,只开启安全的自动修复规则,保守的、不确定的规则一律只设 warn 或 error,但不要加 fix。Ruff 的规则文档会明确标注哪些规则支持 autofix,ESLint 的规则文档也有同样的信息,选择时留意一下。第二条,在 lint-staged 和 pre-commit 里,一旦工具改了文件,用 diff 确认后再提交。这不是仪式感,这是对生产环境负责。
4.4 老项目不敢动,一跑全是报错怎么办
很多读者应该会遇到这种情况:项目维护了几年,代码质量堪忧,跑一遍检查工具,几百个错误铺天盖地。这时候千万别想着一次性全修完,正确做法是渐进式引入。
先配置 ignore 路径,把暂时不想处理的目录排除在外。然后在 lint 配置里把规则级别设为 warn,让检查结果不阻断提交。等团队的整改节奏稳定下来后,再逐步把 warn 提升为 error。还有一个很实用的技巧:只对新增或修改的文件生效,这样新代码必须符合规范,存量代码慢慢消化,不会因为整改压力太大导致方案被搁置。
我曾经在一个老项目上实践过这套思路,第一天只修复了 30 多个文件,两周后就实现了全仓库零错误,团队没有任何一个人因为整改感到痛苦。
4.5 常见问题排查速查表
| 问题现象 | 可能原因 | 解决方案 |
|---|---|---|
| 本地格式化后 CI 仍然报格式错误 | 本地和 CI 的工具版本不一致 | lock 依赖版本,或者用 CI 直接安装 lock 文件 |
| Prettier 和 ESLint 对同一段代码反复要求不同格式 | 没有引入 eslint-config-prettier | 在 ESLint 配置最后加入 prettier 插件关闭格式规则 |
| 提交时钩子一直不生效 | Husky 安装后没有执行 prepare 脚本,或者钩子被跳过 | 执行 npx husky init,检查 .husky/pre-commit 文件是否存在 |
| isort 和 Black 冲突 | isort 的 profile 没设为 black | 在 pyproject.toml 中设置 profile = "black" |
| 自动修复改了没有预料到的代码 | 开启了一些非安全修复规则 | 查看规则文档,关闭不安全的 autofix 规则 |
5. 实测效果与个人经验总结
5.1 落地一段时间的实际变化
我在这套体系用了一个月之后,特意做过一次统计。以当时维护的一个中型项目为例,总共 80 多个源文件,全量跑完格式化加 lint 修复后,清掉了 40 多个未使用的 import,修复了 20 多处潜在的可空引用问题,还有 3 个隐藏很深的错误分支写法。这些不是炫技,是实实在在从代码里挖出来的隐患。
更明显的变化在 code review 环节。格式化问题从 review 中被彻底移除之后,讨论的焦点基本都会集中在架构设计、边界条件处理和测试覆盖上。整个团队的提交记录也干净多了,每个 commit 的意图清晰可辨,回滚某个功能时再也不用在同一个 commit 里区分哪些是格式改动、哪些是逻辑改动。
5.2 我踩过之后总结的几条经验
第一,工具链永远比人可靠,但它需要被谨慎使用。格式化工具和 linter 不是越多越好,规则不是越严越好。过度配置的后果就是开发者的正常提交被反复打断,最后有人绕过钩子,导致整个机制形同虚设。合理的姿态是:以能自动修复的规则为主,以 warn 级别的引导为辅,留给开发者足够的呼吸空间。
第二,任何自动整理动作都要保留人工确认的出口。我现在的习惯是,即便 lint-staged 已经帮我处理好了文件,我也会在提交前扫一眼 diff。这不是不信任工具,而是把“看一眼”当作对代码负责的基本素养。
第三,不要试图一个晚上解决所有历史问题,把整理当作日常习惯而不是一次性的“大扫除”。每次提交多花三十秒跑一遍整理动作,长期积累下来的收益远超那几十秒钟的投入。
5.3 这个方案后续还能怎么扩展
如果你觉得目前这套方案已经满足需求,可以尝试进一步升级。我下一步计划做的是把整理流水线和代码生成器结合,让新创建的模块直接具备符合规范的骨架代码,从源头减少需要整理的内容。另一个方向是引入语义化版本检查工具,在 package 升级时自动提示破坏性变更,把整理从“代码层面”延伸到“依赖层面”。
不过这些都是锦上添花,核心的工作依然是那三件事:格式化、检查、把流程变成一个无人能绕过的门禁。把这三件事做到位,你的代码库就能在很长一段时间里保持干净整洁,不会随着时间和人员的流动重新堕入混沌。
