语义化版本管理:主版本、次版本、补丁版本规则

版本号这东西,看着简单,用起来全是坑。我见过太多团队因为版本号乱发,导致依赖冲突、线上崩溃、排查半天最后发现是版本不匹配。说白了,版本号就是一套严肃的契约——你告诉下游:这次升级到底有多「伤」。

语义化版本规范(SemVer)是目前业界最通用的版本管理方案,格式是 MAJOR.MINOR.PATCH。每个数字背后都有明确的语义,不是拍脑袋写的。

核心原则:一旦发布,版本号就不可更改。如果发现问题,必须发布新版本。

版本号的三段结构

先看一个标准例子:2.4.1

  • 主版本号(MAJOR)——做了不兼容的 API 变更,比如删了某个公开方法、改了函数签名
  • 次版本号(MINOR)——新增了向后兼容的功能,以前写的代码还能跑
  • 补丁版本号(PATCH)——只修了向后兼容的 bug,不影响外部行为

我个人习惯用一张表来记规则,清晰明了:

版本段 何时递增 示例变化 对下游的影响
MAJOR 不兼容的 API 变更 删除接口、修改参数类型 必须修改调用代码
MINOR 向后兼容的新功能 新增方法、增加可选参数 无需修改,可平滑升级
PATCH 向后兼容的 bug 修复 修复崩溃、纠正返回值 完全透明,升级无风险

这里有个容易踩的坑:如果你在主版本号递增时,同时也新增了功能,那要不要同时把次版本号清零?规约说了:每次递增 MAJOR,MINOR 和 PATCH 都要归零。比如从 1.3.2 到 2.0.0,而不是 2.3.2。

避坑指南:我曾经见过团队把不兼容变更(比如修改了 HTTP 接口的返回字段)发成了 MINOR 版本。下游自动依赖更新后全线崩溃,回滚花了整整一个通宵。所以,每次发布前一定要问自己:这个变更是否「破坏」了已有的契约?

预发布版本与构建元数据

除了三段数字,SemVer 还支持附加信息,格式是 MAJOR.MINOR.PATCH-预发布标识+构建元数据。比如 1.0.0-alpha.1+build.20250301。

  • 预发布标识:用连字符连接,比如 alpha、beta、rc.1。这类版本优先级低于正式版。
  • 构建元数据:用加号连接,只存构建信息,不影响版本优先级。比如 +20250301。

我在项目中遇到过一个问题:团队把预发布版本直接发到生产环境,结果依赖管理器自动匹配到了更高的正式版本,导致回滚困难。所以我的建议是——预发布标识只在内部测试或 CI 流水线中使用,上线前一定去掉。

版本比较与依赖范围

版本比较规则很简单:从左到右逐段比较数字。预发布版本小于正式版本。比如 1.0.0-alpha < 1.0.0。

在实际的依赖管理文件(比如 package.json)中,我们常用范围符号来锁定版本:

// 精确版本
"lodash": "4.17.21"

// 兼容补丁更新(>=1.0.0 <1.1.0)
"lodash": "~1.0.0"

// 兼容次版本更新(>=1.0.0 <2.0.0)
"lodash": "^1.0.0"

// 允许主版本变更(不推荐)
"lodash": "*"

你想想看,^1.0.0 这种写法虽然方便,但一旦有人发了错误的 MINOR 版本(本应是 MAJOR),你的项目可能就无辜躺枪。所以我个人偏向使用 ~ 来精确控制 PATCH 级别更新,对于核心依赖甚至直接锁精确版本。

小技巧:如果你在维护一个公共 SDK,尽量在 CHANGELOG 里写明每次版本的变更类型(Breaking、Feature、Fix)。配合 SemVer,用户一眼就能判断能否直接升级。我习惯在 CI 里加一个钩子,自动校验版本号变更是否与 commit message 一致——比如 feat 必须升 MINOR,fix 必须升 PATCH。

关于 0.x.x 版本

主版本号为 0 表示还在开发初期,不保证 API 稳定。这时候 MINOR 递增实际上相当于「不兼容变更」,PATCH 递增则是 bug 修复。很多项目直到发布第一个稳定版 (1.0.0) 前,内部版本号都乱得一塌糊涂。

我的建议是:对于 0.x.x 版本,让依赖方明确锁定 0.1.x 这种精确范围,不要用 ^0.1.0(它实际匹配 0.1.0 到 0.2.0 之间的版本,但 SemVer 对 0.x 的 ^ 行为有点特殊,容易搞混)。

语义化版本号结构 MAJOR . MINOR . PATCH 不兼容 API 变更 删除接口 / 改参数 / 改行为 → 递增 MAJOR 向后兼容的新功能 新增方法 / 可选参数 → 递增 MINOR 向后兼容的 bug 修复 修崩溃 / 修返回值 → 递增 PATCH 版本号递增规则 每次递增 MAJOR 后,MINOR 和 PATCH 归零 每次递增 MINOR 后,PATCH 归零 预发布版本:MAJOR.MINOR.PATCH-alpha.1 优先级低于正式版

上面这张图把版本号结构和触发条件串在了一起。你拿到一个版本号,拆开看三段,再对照变更内容,就知道该升哪一段。嗯,记住这个逻辑,后面讲兼容性测试范围时还会用到。

总结一下:语义化版本不是约束,而是你和用户之间的承诺。每次更新前问自己——这次是「修 bug」「加功能」还是「改接口」?答案决定了版本号,也决定了下游的升级风险系数。