1. 版本管理基础:语义化版本控制(SemVer)

做软件开发这么多年,我见过太多因为版本混乱导致的线上事故。有一次,一个同事升级了依赖库,结果整个服务启动不了——后来发现是破坏性变更没被标记出来。从那以后,我特别重视版本号的规范。

语义化版本控制,英文叫 SemVer,是目前业界最通用的版本管理方案。说白了,它就是一套关于「版本号该怎么写」的规则。

版本号长什么样?

一个标准的 SemVer 版本号长这样:

主版本号.次版本号.补丁号

举个例子:2.5.1

  • 2 是主版本号
  • 5 是次版本号
  • 1 是补丁号

每个数字之间用英文句点分隔。嗯,就这么简单。

三个数字各自代表什么?

我习惯用「兼容性」这个角度来理解它们:

位置 名称 什么时候增加 代表含义
第一位 主版本号 做了不兼容的 API 修改 升级有风险,需要改代码
第二位 次版本号 新增了功能,但向后兼容 升级很安全,可以放心用
第三位 补丁号 修复了 bug,没有新增功能 只是修修补补,完全无感

你想想看,如果每次升级都不知道会不会出问题,那得多痛苦?SemVer 就是来解决这个问题的。

主版本号:破坏性变更

主版本号变了,说明这次升级「不兼容」。什么意思?就是旧代码可能跑不起来了。

我在项目中遇到过这样的情况:一个 HTTP 客户端库从 1.x 升级到 2.0,请求方法的签名全改了。原来传三个参数,现在要传一个对象。这种改动,就必须升主版本号。

什么时候升主版本号?

  • 删除了某个公开的 API
  • 修改了方法的参数列表
  • 改变了返回值的结构
  • 移除了某个配置项

我曾经踩过的坑:有个库从 1.9.9 直接跳到 2.0.0,我以为只是大版本更新,结果发现它把核心类的命名空间都改了。那次排查花了我整整一个下午。所以,主版本号变更时,一定要仔细看 changelog。

次版本号:新增功能

次版本号增加,意味着「有新东西,但旧东西还能用」。这是最让人放心的升级类型。

举个例子:你用的日志库从 3.1.0 升级到 3.2.0。新版本加了一个 JSON 格式化输出功能,但你原来用的 log.info() 方法完全没变。这就是典型的次版本号升级。

// 3.1.0 版本
logger.info("用户登录成功");

// 3.2.0 版本,新增了 JSON 格式
logger.json({"event": "login", "user": "admin"});
// 原来的 info 方法依然能用
logger.info("用户登录成功");

我个人习惯是:只要看到次版本号变了,就看看新功能有没有我需要的。没有的话,升级基本没风险。

补丁号:修 Bug

补丁号是最小的版本变动。它只修 bug,不改功能,不增 API。

比如 2.3.0 有个 bug:在 Windows 系统上文件路径处理错了。修复后版本号变成 2.3.1。这种升级,闭着眼睛升就行。

小技巧:在 package.json 或 pom.xml 里,我通常把依赖写成 ^2.3.0 这种形式。意思是「允许自动升级补丁号和次版本号,但不升主版本号」。这样既能拿到 bug 修复,又不会被破坏性变更坑到。

版本号的递增规则

版本号不是随便改的。有一套严格的递增逻辑:

  1. 补丁号:只修 bug,不新增功能。从 1.0.0 到 1.0.1
  2. 次版本号:新增功能,但向后兼容。从 1.0.0 到 1.1.0
  3. 主版本号:做了不兼容的修改。从 1.0.0 到 2.0.0

而且要注意:

  • 升了主版本号,次版本号和补丁号归零
  • 升了次版本号,补丁号归零
  • 补丁号升了,其他不变

举个例子:

1.2.3 → 修复 bug → 1.2.4
1.2.3 → 新增功能 → 1.3.0
1.2.3 → 破坏性变更 → 2.0.0

预发布版本和构建元数据

有时候你会看到这样的版本号:2.0.0-alpha.1 或 1.3.0-beta.2+sha.12345。

这是 SemVer 支持的扩展格式:

  • 预发布标签:用连字符跟在版本号后面,比如 -alpha、-beta、-rc.1
  • 构建元数据:用加号跟在后面,比如 +build.20240301

预发布版本有个特点:它的优先级低于正式版本。也就是说 1.0.0-alpha 比 1.0.0 要「小」。这在依赖管理里很重要——默认情况下,包管理器不会自动安装预发布版本。

一句话总结 SemVer 的核心思想:

版本号不只是数字,它是你和用户之间的一个承诺。主版本号变了,意味着「你要改代码了」;次版本号变了,意味着「有新功能可以试试」;补丁号变了,意味着「放心升级,只是修了个 bug」。

一个简单的版本号决策流程

我平时判断版本号怎么改,就靠下面这个流程:

这次改动是否破坏现有 API?
├── 是 → 升主版本号(x.0.0)
└── 否 → 是否新增了功能?
    ├── 是 → 升次版本号(x.y.0)
    └── 否 → 升补丁号(x.y.z)

嗯,就是这么简单。但实际项目中,很多人会搞混「破坏性变更」的定义。比如改了一个内部私有方法,算不算破坏性?我个人认为不算——只要公开 API 没变,就不该升主版本号。

我的建议:刚开始用 SemVer 时,可以保守一点。拿不准该升主版本还是次版本时,就升主版本。宁可让用户觉得「升级麻烦」,也别让他们升级后出问题。信任一旦打破,很难重建。

下一章,我们会聊聊在实际项目中,怎么用包管理工具来锁定版本、处理版本范围,以及最常见的版本冲突场景。到时候我会分享几个我亲身经历的「血泪史」。