Conventional Commits 规范详解:常用前缀与工具链落地
2026/9/13 20:57:31 网站建设 项目流程

作为一名常年被烂提交信息折磨的开发者,我见到太多这样的场景:git log --oneline一拉下来,全是updatefix bugwip123这种毫无信息量的提交,代码评审的时候全靠猜,上线后要回滚某个改动,不得不把 diff 从头翻到尾。后来团队强制推了一段时间 Conventional Commits 规范,提交历史才终于恢复到"人类可读"的状态。今天这篇就专门把这套规范里最核心的常用前缀讲透,包括每个前缀什么时候用、什么时候坚决不能用、怎么写才规范、怎么用工具守住规范。不管你是个人开发者还是团队负责人,只要还在用 Git 做版本管理,这套东西都值得认真看一遍。

1. Conventional Commits 到底在解决什么问题

1.1 失控的提交信息,正在悄悄消耗你的时间

很多团队对 Git 提交的态度是"能推上去就行",结果就是 commit message 成了全仓库最没人在意的地方。你也许经历过:周五下午接到线上反馈,需要快速定位某个功能是什么时候加进来的、是哪位同事写的、改动了哪些文件。你满怀期待地打开git log --all --oneline,看到的是:

update fix fix2 test 临时提交 修改了一堆东西

整个日志像一本没有目录的字典,翻半天找不到答案。更糟的是,这些"流水账"式提交还会污染自动化流程:你在 CI 里想拦截"纯文档提交"直接发布,却因为无法识别提交类型而不得不全量重新构建;你想根据提交历史自动生成 CHANGELOG,看到的却是一堆"update now"和"bug fix";你用git bisect二分定位问题时,commit message 不足以帮你判断哪些提交需要重点排查。这些问题本质上都是信息缺失导致的。

Conventional Commits 的核心思路其实很简单:给每次提交打上一个语义化前缀,比如feat(新功能)、fix(修复)、docs(文档),后面再跟上清晰扼要的描述。这样人和机器都能在毫秒级内理解一次提交的意图,提交历史从一个垃圾桶变成一台分类清晰的档案柜。

1.2 规范的价值:不仅是"好看",更是让机器能读懂

有人觉得 Commit 规范是形式主义,我完全不认同。一套靠谱的提交规范带来的第一个收益是人读得懂:Code Review 时,看到fix(auth): 修复 token 刷新竞态就知道这是修复类改动,应该重点检查边界条件;看到feat(api): 新增批量导出接口就知道这是功能变更,需要评估接口兼容性和文档更新。第二个收益是机器能解析,这一点很多人忽略了。你注意看 Conventional Commits 的官方定义,它强调约定要足够结构化,目的是让 changelog 生成、语义化版本推断等工具可以直接消费这些信息。

这就像我们做数据处理时给字段加索引,你见过"前缀树"和"前缀和"这类设计吧?它们都是把零散信息变成可快速检索的结构化数据。Conventional Commits 给提交信息加前缀,本质上就是给 Git 历史的"语义检索"能力做索引。工具看到feat就知道该升 minor 版本,看到fix就知道该升 patch 版本,看到BREAKING CHANGE就知道该升 major 版本。有了这些机制,版本发布不再是"拍脑袋定版本号",而是由提交内容自动推导出来的结果。

2. 常用前缀逐一拆解:什么时候用哪个,别再用错

2.1 核心前缀速查表

Conventional Commits 规范并不限定前缀的完整清单,但社区里已经沉淀了一套默认集合,也就是@commitlint/config-conventional里内置的 type。下面这张表把最常用的前缀全部整理出来,建议直接截图存下来。

前缀含义适用场景示例
feat新功能新增对外可见的功能模块、接口、页面feat(login): 新增扫码登录
fix修复 Bug修正已知缺陷、错误行为、崩溃问题fix(cart): 修复优惠券未生效的问题
docs文档变更README、注释、API 文档、博客文档docs: 更新部署章节的链接
style代码格式空格、分号、缩进、格式化,不改变代码逻辑style: 调整 import 排序规则
refactor代码重构重构内部结构,不改变外部行为和功能refactor(utils): 抽离统一的日期解析函数
perf性能优化降低耗时、减少内存占用、优化计算逻辑perf(list): 大数据量渲染改为虚拟滚动
test测试相关新增或修改测试用例、测试配置test(auth): 补充 token 过期场景用例
build构建系统构建工具、依赖版本、编译配置的变更build(deps): 升级 webpack 到 5.x
ci持续集成CI 配置、自动化脚本、流水线文件变更ci: 增加 PR 自动预览环境
chore日常杂项不属于以上所有类别的变更,比如代码生成、配置微调chore: 更新 .gitignore 忽略规则
revert回滚提交撤销某次提交revert: 回滚 feat(login) 的扫码登录

这张表是基础,但实际落地时我见过最多的错误就是chore当垃圾桶。什么乱七八糟的改动都往chore里扔,最后git log --grep=chore拉出来什么都有。判断是否要归入chore,可以先问自己:这个改动是否属于其他十个前缀之一?如果都不是,再考虑chore。如果答案是"改了一个脚本让 CI 快一点",那应该归入ci而不是chore

2.2 最容易混淆的三组前缀:style、refactor、perf

stylerefactorperf这三者经常被人搞混,因为表面看都是"改代码但不改功能"。我提供一个非常实用的判断口径:这次改动是否会改变对外行为或用户可感知的结果?

  • style:单纯格式化,比如 IDE 自动整理了缩进、把双引号换成单引号。行为完全不变,纯外观调整。
  • refactor:重构内部实现,行为不变,但代码结构变了。比如抽公共函数、换数据结构、调整模块依赖方向。
  • perf:也要做内部改动,但改动的目的和效果是让性能指标发生变化,比如响应时间从 200ms 降到 80ms。虽然功能可能没变,但"运行速度"本身就是用户可感知的。

举个真实例子:之前我们有个列表页卡顿,同事花了一下午把Array.filter改成Map预索引,还把双层循环拆了。这次提交的 message 写的是refactor(list): 优化列表过滤逻辑,其实它的核心收益是性能,正确的写法应该是perf(list): 优化列表过滤逻辑,处理 5w 条数据耗时降低 60%refactorperf的区别不在改了多少代码,而在改动目的是什么。目标是为了可维护性,用refactor;目标是为了性能指标,用perf

2.3 前缀选择的实战判断流程

我总结了一个"一个一个排除"的选择流程,团队新人照着走基本不会选错:

  1. 这次提交是否回滚了之前的改动?是 →revert
  2. 这次提交是否修改了用户可见的功能或 API?是 → 判断是新增还是修复。新增 →feat;修复 →fix
  3. 这次提交是否纯粹修改文档?是 →docs
  4. 这次提交是否修改了测试代码/测试配置?是 →test
  5. 这次提交是否动到构建工具或依赖?是 →build
  6. 这次提交是否动到 CI 配置?是 →ci
  7. 这次提交是否只改了格式而没有逻辑变化?是 →style
  8. 这次提交是否为了实现"同样的功能但内部结构更好"?是 →refactor
  9. 这次提交是否为了让"同样功能跑得更快/用得更省"?是 →perf
  10. 以上都不是 →chore

这个流程看起来像一棵判断树,实际用熟了以后 10 秒内就能敲定前缀。

3. 完整提交信息格式与语法细节:不止是前缀

3.1 提交头、正文、页脚的结构化写法

Conventional Commits 的完整格式远不止一个feat: xxx的 header,它由提交头(header)、正文(body)和页脚(footer)三部分组成:

<type>[optional scope]: <description> [optional body] [optional footer(s)]

提交头是核心,必须写,格式是类型(可选作用域): 描述。注意冒号后面必须有一个空格,这也是 commitlint 默认校验的规则之一。描述部分推荐用现在时祈使句,比如feat(api): add batch export endpoint而不是feat(api): added batch export endpoint。保持动词始终是addfixupdate这类原形,整个 history 读下来像读一条连续的命令列表,非常顺畅。

正文用来补充细节:为什么做这个改动?是怎么实现的?涉及哪些设计取舍?对复杂改动来说,正文比提交头还重要。很多提交只有 header,没有 body,三个月后自己回头看都不知道当初为什么这么写。建议任何超过 200 行的 diff,都强制要求写正文。

页脚通常用来记录关联信息,最常见的是 Breaking Changes 说明和关联 Issue 编号,比如:

fix(orders): 修复订单金额计算精度问题 浮点数相乘存在精度丢失,累计多个订单金额时会出现分位差异。 改用整数分存储,前端按需转换展示。 Closes #482

这里Closes #482表示这条提交会关闭编号 482 的 issue。在 GitHub/GitLab 上,commit message 里出现Closes #xxx会自动关联 issue,评审和追溯都方便。

3.2 BREAKING CHANGE:最容易写错的关键标记

破坏性变更(Breaking Change)是提交规范里最容易被忽略、但影响最大的标记。如果你要删除一个公共 API、修改函数的参数签名、调整数据库表结构,必须在页脚里写:

feat(users)!: 移除旧的用户状态更新接口 BREAKING CHANGE: remove the deprecated `updateUserStatus` method, use the new `updateUserStatusV2` instead.

这里有两种等价写法:第一种是在 header 的:前加!,比如feat(users)!:;第二种就是在 footer 里写BREAKING CHANGE:开头的说明。推荐两种都用:!让开发者扫一眼 log 就能发现问题,BREAKING CHANGE里的详细说明则告诉维护者具体应该怎么迁移。

这个标记直接影响语义化版本号的 major 位。工具链看到它就会自动把版本号从2.3.0推到3.0.0。如果漏掉了这个标记,发布时版本号计算会错误,下游可能因为破坏性变更收到一个"只有 minor 升级却完全不兼容"的版本,这在依赖管理里是大事故。我的经验是:只要动了对外暴露的方法签名、删除枚举值、修改配置字段名,就一律加上BREAKING CHANGE,宁可多标不可漏标。

3.3 作用域(scope):什么时候加,怎么定

scope 是可选的作用域,放在类型和冒号之间,比如feat(parser):fix(ui/render):。它解决的问题是:在多模块、多包的项目里,单独看feat:并不知道改的是哪一块。加了 scope 后,日志可以按模块过滤,发布时也可以根据 scope 决定是否需要通知对应模块负责人。

但 scope 不是越多越好。如果团队里每个人都按自己的想法起 scope,最后就会出现feat(utils)feat(公用)feat(公共方法)这种五花八门的标签,等于没有 scope。建议在项目的CONTRIBUTING.md里维护一份允许的 scope 清单,比如 monorepo 中就取 package 名作为 scope。配置 commitlint 时,也可以显式限定 scope 可用的枚举值,从工具层面杜绝乱用。

3.4 规范如何联动语义化版本号

Conventional Commits 和 SemVer(语义化版本)是一对黄金搭档,理解了这个联动逻辑,你就明白为什么每个前缀都那么重要。SemVer 规定版本号格式是主版本.次版本.修订号,对应关系是这样的:

  • fix类提交修复了向后兼容的 bug → 增加修订号(patch),如1.0.0 → 1.0.1
  • feat类提交新增了向后兼容的功能 → 增加次版本号(minor),如1.0.1 → 1.1.0
  • 提交中包含BREAKING CHANGE→ 增加主版本号(major),如1.1.0 → 2.0.0

这种映射不是约定俗成,而是standard-versionsemantic-release等工具自动计算版本号的核心规则。工具会把你上次发布之后的所有提交扫描一遍,看有没有featfixBREAKING CHANGE,然后决定下一个版本号该进位到哪一位。

所以,提交信息写得准不准,直接决定版本发布对错。我踩过一次坑:同事把一次新增功能提交写成了fix(xxx): 新增xxx,自动化工具把版本号从1.2.0升成了1.2.1,结果下游收到新包后发现多了一个功能,但这个功能写在 patch 版本里,按照语义化版本的约定,依赖方完全可以不升级。后来我们干脆用semantic-release接管版本发布流程,才把这种人为误差压到最低。

4. 落地实操:从工具链到团队协作机制

4.1 commitlint + husky:把规范装进 Git 钩子

规范如果只停留在"文档里写写",基本等于没写。要让每个人都遵守,第一个要上的工具就是 commitlint + husky。husky 负责在 commit 时触发钩子,commitlint 负责校验提交信息是否符合规范。安装和配置过程不算复杂:

npm install --save-dev @commitlint/cli @commitlint/config-conventional husky

然后在项目根目录新建commitlint.config.js

module.exports = { extends: ['@commitlint/config-conventional'], rules: { 'type-enum': [2, 'always', [ 'feat', 'fix', 'docs', 'style', 'refactor', 'perf', 'test', 'build', 'ci', 'chore', 'revert' ]], 'subject-case': [0], 'header-max-length': [2, 'always', 100] } };

再初始化 husky:

npx husky add .husky/commit-msg 'npx --no -- commitlint --edit "$1"'

这样每次git commit都会自动校验提交信息,不符合规则会直接报错,根本提交不进去。注意subject-case这个规则我把默认限制关掉了,因为中文描述里首字母大小写问题没有意义,不要因为规则卡掉正常提交。团队里如果有自己的额外要求,比如 scope 必须来自指定列表,可以再加一条'scope-enum'规则。

4.2 commitizen:让开发者不用背规范也能写对

commitlint 解决的是"不符合就拦下来"的问题,但每次提交都被弹回来也挺烦。更好的方案是配合 commitizen,把提交信息变成交互式问答。安装cz-conventional-changelogadapter(下面这个是比较流行的可选方案,也可以直接用 commitizen 自带 adapter):

npm install --save-dev commitizen cz-conventional-changelog

package.json 里加入:

{ "config": { "commitizen": { "path": "cz-conventional-changelog" } } }

然后使用git cz代替git commit,它会一步步问你:选择提交类型、填写影响范围、写简短描述、写详细正文、是否有破坏性变更。跑完一轮,提交信息自动组装好,不会因为手误漏掉冒号或空格。

不过更推荐的做法是给git cz起个别名git c,让团队习惯养成成本更低。我在.gitconfig里加了c = cz,然后告诉所有人:"以后不要打 git commit,要么用带规则的工具生成,要么直接打 git c。"这个习惯一旦养成,团队里提交信息的质量会稳定上一个台阶。

4.3 配合 standard-version 自动生成 CHANGELOG

提交规范落地以后,最后一块拼图就是发布阶段的自动化。我常用的是 standard-version,它直接读取 Conventional Commits 提交记录,自动帮你完成三件事:升版本号、生成或更新 CHANGELOG.md、打 Git tag。安装:

npm install --save-dev standard-version

在 package.json 里加一条脚本:

{ "scripts": { "release": "standard-version" } }

执行npm run release,它会扫描上一次 tag 到现在的所有提交,生成类似:

### 1.3.0 (2025-01-15) ### Features * **login:** 新增扫码登录 ([a1b2c3d](https://...)) ### Bug Fixes * **cart:** 修复优惠券未生效的问题 ([d4e5f6a](https://...))

CHANGELOG 自动生成,文档工作量和人工维护成本瞬间归零。注意 standard-version 默认有一个 hooks 流程,如果你想在版本发布前跑测试或构建,可以在 package.json 里配置standard-version.scripts生命周期钩子。这套流程的关键是提交信息必须准确,否则 changelog 就会出现硬伤:fix被误写成feat,changelog 里就会多出一个 Feature,误导使用者。

4.4 团队推行的关键动作:评审红线 + 提交模板

工具装得再全,如果没有管理手段,照样有人绕过。我经历了多个团队落地这套规范,最后总结出三个关键动作。

第一个动作是**在 Code Review 清单中加一条硬性检查:提交信息是否规范。**我们用的是 GitHub PR 页面,每个 PR 会显示这个分支上的提交列表,reviewer 在评审的时候顺便看一眼type是否匹配变更内容。如果 PR 里有 5 条提交,前 3 条feat、中间 1 条实际是style,直接打回要求修改提交信息。

第二个动作是**在仓库根目录放一个 CONTRIBUTING.md,把前缀表、示例、工具安装方式写清楚。**它不只是给外部贡献者看的,也是给团队新人看的。我每次带新同学,第一件事就是让他们读一遍这个文档,基本十分钟就能上手。

第三个动作是**选择合理的强制范围:新提交强制,历史提交不追溯。**不要想着把仓库历史全部重写一遍,那既不安全也不划算。只需要从某一天起,所有新提交强制走规范;已经推送过的老提交,保持原样,不影响使用。如果确实觉得最近几个提交太乱,可以用git rebase -i原地重写还没推送的提交信息,但只建议在个人分支上操作,禁止对公共分支强制 rebase。

5. 常见问题与排查技巧实录

5.1 真实案例:类型和描述互相矛盾怎么救

我在 Code Review 里见过不少"前缀没错但描述和前缀打架"的提交,比如:

  • feat: 修复了登录接口的超时问题—— 前缀是 feat(新功能),描述却是"修复",这应该改成fix: 修复登录接口超时问题
  • fix: 优化列表渲染速度,首屏快了 30%—— 这是典型的perf作用域。
  • chore: 新增商品详情页—— 新增页面是用户可见的功能,应改为feat
  • docs: 调整按钮样式—— 调样式属于style而不是docs

这背后的问题是提交者没有做"语义对应"。我给团队的建议是:**提交描述永远回答"这次提交做了什么",前缀回答"这次提交属于哪种类型",两者必须指向同一个事实。**写提交信息之前,想想这个改动如果出现在 changelog 里,应该属于哪一段?如果你自己都觉得放 "Features" 下面别扭,那前缀多半选错了。

5.2 commitlint 常见报错与修复速查

报错信息原因修复方法
type must be one of [feat, fix, ...]使用了不在枚举列表里的类型commitlint.config.jstype-enum里补充该 type
header must not be longer than 100 characters提交头超过长度限制精简描述,把细节挪到正文 body 中
subject may not be empty冒号后没有写描述补充提交描述,例如fix: 修复登录问题
footer must have leading word BREAKING CHANGE页脚格式不正确确保是BREAKING CHANGE:开头,冒号后空格

这里要注意,commitlint 的默认配置里面,header-max-length是 72 还是 100 取决于扩展包版本。我们项目统一改成 100,因为很多时候 scope + type 本身就不短,72 容易误伤,导致开发者为了绕过规则把描述写得很简短。规则要服务于清晰度,不是制造路障。

5.3 老项目迁移:不重写历史,也能逐步规范化

很多团队一听要推行规范,第一反应是"仓库里几千条历史提交怎么办?"。我的答案很简单:别碰历史。你真正需要做的是从今天开始,让每个新提交都符合规范。如果担心开发者在多个分支上交叉提交导致混乱,可以让 Git 钩子在所有分支上生效,再去掉那些"紧急时绕过规则"的--no-verify使用习惯。

有一种特殊情况是,在已经功能冻结的 release 分支上,偶尔需要手动合入 hotfix,然后又得把 hotfix 提交信息整理成符合规范的格式。这时候可以用git commit --amend或者git rebase -i来改提交信息,但一定只在推送前做。已经推送到远端共享分支的提交,就不要轻易改历史了,宁可多写一条revert或补丁说明,也不要用 force push 去抹平历史,这是团队协作安全的底线。

5.4 紧急 hotfix 场景下,提交规范怎么保底

最容易被用作不遵守规范借口的场景就是"线上出事故了,赶紧修复,谁还有空写规范?"其实越紧急,提交信息越要写清楚。线上 hotfix 的受众是发布负责人、值班工程师和凌晨被 call 起来的同事,他们最需要从 commit message 里快速判断:这次修复了什么、影响范围在哪、要不要一起发到其他版本。

我的建议是,hotfix 走简化版但不降级:fix(模块): 修复xxx问题,正文可以只有一行,但要带上问题编号和影响范围,比如:

fix(auth): 修复 token 过期后白屏问题 紧急热修,影响 Web 端所有登录会话,已同步 v1.2.x 分支。Closes #512

这虽然比平时少了很多细节,但关键信息全在。为了缩短 hotfix 的发布时间,我们在 CI 里单独开了一条 hotfix 流水线,它会自动校验提交信息格式,但把 block 条件放宽:只要类型是fix且 header 不超过 100 个字符就直接放行,body 允许为空。这样既保住规范底线,又不至于让紧急修复被流程卡死。


我个人的经验是,Conventional Commits 这套规范真正开始时会有不少抵触情绪,大家觉得多写几个字浪费时间。但一旦配合工具跑起来,所有人都会慢慢适应,因为收益太明显了:git log 干净了,changelog 不用手动写了,版本号也不再靠肉眼判断。再分享一个小技巧:如果你觉得 commitizen 的交互式问答太繁琐,可以在本地写一个 pre-commit 的临时脚本,把常用的提交模板输出到终端,直接照着填就行。关键是让"每次 commit 都写清楚"变成肌肉记忆,而不是再靠意志力去坚持。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询