语义化版本管理:主版本、次版本、补丁版本规则
版本号这东西,看着简单,用起来全是坑。我见过太多团队因为版本号乱发,导致依赖冲突、线上崩溃、排查半天最后发现是版本不匹配。说白了,版本号就是一套严肃的契约——你告诉下游:这次升级到底有多「伤」。
语义化版本规范(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。
预发布版本与构建元数据
除了三段数字,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 级别更新,对于核心依赖甚至直接锁精确版本。
关于 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 的 ^ 行为有点特殊,容易搞混)。
上面这张图把版本号结构和触发条件串在了一起。你拿到一个版本号,拆开看三段,再对照变更内容,就知道该升哪一段。嗯,记住这个逻辑,后面讲兼容性测试范围时还会用到。