恒美微站
首页
关于我们
建站服务
主题模板
案例展示
资讯中心
联系我们
Conventional Commits 规范详解:常用前缀与工具链落地
首页
资讯中心
/
Conventional Commits 规范详解:常用前缀与工具链落地
Conventional Commits 规范详解:常用前缀与工具链落地
发布时间:2026/9/13 20:57:33
作为一名常年被烂提交信息折磨的开发者我见到太多这样的场景git log --oneline一拉下来全是update、fix bug、wip、123这种毫无信息量的提交代码评审的时候全靠猜上线后要回滚某个改动不得不把 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.xci持续集成CI 配置、自动化脚本、流水线文件变更ci: 增加 PR 自动预览环境chore日常杂项不属于以上所有类别的变更比如代码生成、配置微调chore: 更新 .gitignore 忽略规则revert回滚提交撤销某次提交revert: 回滚 feat(login) 的扫码登录这张表是基础但实际落地时我见过最多的错误就是把chore当垃圾桶。什么乱七八糟的改动都往chore里扔最后git log --grepchore拉出来什么都有。判断是否要归入chore可以先问自己这个改动是否属于其他十个前缀之一如果都不是再考虑chore。如果答案是改了一个脚本让 CI 快一点那应该归入ci而不是chore。2.2 最容易混淆的三组前缀style、refactor、perfstyle、refactor、perf这三者经常被人搞混因为表面看都是改代码但不改功能。我提供一个非常实用的判断口径这次改动是否会改变对外行为或用户可感知的结果style单纯格式化比如 IDE 自动整理了缩进、把双引号换成单引号。行为完全不变纯外观调整。refactor重构内部实现行为不变但代码结构变了。比如抽公共函数、换数据结构、调整模块依赖方向。perf也要做内部改动但改动的目的和效果是让性能指标发生变化比如响应时间从 200ms 降到 80ms。虽然功能可能没变但运行速度本身就是用户可感知的。举个真实例子之前我们有个列表页卡顿同事花了一下午把Array.filter改成Map预索引还把双层循环拆了。这次提交的 message 写的是refactor(list): 优化列表过滤逻辑其实它的核心收益是性能正确的写法应该是perf(list): 优化列表过滤逻辑处理 5w 条数据耗时降低 60%。refactor和perf的区别不在改了多少代码而在改动目的是什么。目标是为了可维护性用refactor目标是为了性能指标用perf。2.3 前缀选择的实战判断流程我总结了一个一个一个排除的选择流程团队新人照着走基本不会选错这次提交是否回滚了之前的改动是 →revert。这次提交是否修改了用户可见的功能或 API是 → 判断是新增还是修复。新增 →feat修复 →fix。这次提交是否纯粹修改文档是 →docs。这次提交是否修改了测试代码/测试配置是 →test。这次提交是否动到构建工具或依赖是 →build。这次提交是否动到 CI 配置是 →ci。这次提交是否只改了格式而没有逻辑变化是 →style。这次提交是否为了实现同样的功能但内部结构更好是 →refactor。这次提交是否为了让同样功能跑得更快/用得更省是 →perf。以上都不是 →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。保持动词始终是add、fix、update这类原形整个 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.1feat类提交新增了向后兼容的功能 → 增加次版本号minor如1.0.1 → 1.1.0提交中包含BREAKING CHANGE→ 增加主版本号major如1.1.0 → 2.0.0这种映射不是约定俗成而是standard-version、semantic-release等工具自动计算版本号的核心规则。工具会把你上次发布之后的所有提交扫描一遍看有没有feat、fix、BREAKING 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.jsmodule.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] } };再初始化 huskynpx 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 自带 adapternpm install --save-dev commitizen cz-conventional-changelogpackage.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被误写成featchangelog 里就会多出一个 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.js的type-enum里补充该 typeheader 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 都写清楚变成肌肉记忆而不是再靠意志力去坚持。