第3章:版本号管理规范

版本号这东西,看着简单,其实坑不少。我见过太多团队因为版本号乱打,导致用户升级出问题,甚至线上崩溃。今天咱们就把语义化版本控制(SemVer)、版本号比较算法、以及存储读取这些事儿,一次性讲透。

3.1 语义化版本控制(SemVer)

说白了,SemVer 就是一套版本号的命名规则。它用三个数字来表示:主版本号.次版本号.修订号。比如 2.1.3,2 是主版本,1 是次版本,3 是修订号。

规则其实很简单:

  • 主版本号:当你做了不兼容的 API 修改时,加 1。比如从 v1.x 升级到 v2.x,用户必须改代码。
  • 次版本号:当你新增了向下兼容的功能时,加 1。比如加了新接口,但旧接口还能用。
  • 修订号:当你做了向下兼容的问题修正时,加 1。比如修了个 bug,功能没变。

嗯,这里要注意:版本号不能有前导零。比如 01.2.3 是不合法的,必须写成 1.2.3。

核心原则:一旦发布了某个版本,就永远不能修改它的内容。任何修改都必须发布新版本。

我个人习惯在版本号后面加预发布标识,比如 1.0.0-alpha.1、2.0.0-beta.2。这样用户能清楚知道这是测试版还是正式版。预发布版本的优先级低于正式版本,这个后面会讲。

3.2 版本号比较算法

版本号比较,说白了就是比大小。但你不能直接拿字符串比,因为 "10.0.0" 比 "2.0.0" 小?不对,10 比 2 大。所以得按数字逐段比较。

比较算法其实很简单:

  1. 从左到右,依次比较主版本号、次版本号、修订号。
  2. 每个部分都按数值比较,不是字符串比较。
  3. 如果某个版本缺少某一段,默认该段为 0。比如 1.2 等同于 1.2.0。
  4. 如果数值都相等,再看预发布标识。有预发布标识的版本,优先级低于正式版本。

举个例子:

比较 1.2.3 和 1.2.4:
主版本都是 1,相等
次版本都是 2,相等
修订号 3 < 4,所以 1.2.3 < 1.2.4

比较 2.0.0 和 1.9.9:
主版本 2 > 1,所以 2.0.0 > 1.9.9

比较 1.0.0-alpha 和 1.0.0:
前三个数字都相等,但 1.0.0-alpha 有预发布标识
所以 1.0.0-alpha < 1.0.0

我在项目中遇到过一个问题:用户本地版本是 1.0.0,服务器最新版本是 1.0.0-beta。按 SemVer 规则,1.0.0-beta 比 1.0.0 小,所以不会触发更新。但用户以为 beta 版是新功能,一直抱怨为什么收不到更新。后来我加了逻辑:如果当前版本是正式版,就不提示预发布版本的更新。

避坑指南:我曾经在比较版本号时直接用字符串比较,结果 "10.0.0" 被判定小于 "2.0.0"。因为字符串比较是按字符顺序,'1' 小于 '2'。所以一定要用数值比较,或者用现成的库。

你想想看,如果版本号比较写错了,后果是什么?用户可能永远收不到更新,或者反复收到同一个版本的更新。这两种情况都很糟糕。

3.3 版本号存储与读取

版本号存哪儿?怎么存?这得看你的应用场景。

常见存储位置:

存储位置 适用场景 注意事项
本地文件(如 version.json) 桌面应用、移动应用 防止被篡改,建议加签名校验
注册表(Windows) Windows 桌面应用 注意权限问题,32位和64位路径不同
SharedPreferences(Android) Android 应用 轻量级,适合存储简单数据
UserDefaults(iOS) iOS 应用 类似 SharedPreferences
数据库 需要记录版本历史 可以存多条记录,方便回滚

我个人习惯用 JSON 文件存储版本信息,结构清晰,跨平台也方便。比如:

{
  "version": "2.1.3",
  "buildNumber": 456,
  "releaseDate": "2024-03-15",
  "minSupportedVersion": "1.5.0"
}

这里有个关键点:minSupportedVersion 表示最低支持版本。如果用户当前版本低于这个值,就必须强制更新。我在项目中就遇到过用户用着 v1.0.0,但服务器已经不支持 v1.x 的 API 了,结果各种报错。后来加了最低版本检查,低于 v1.5.0 的直接弹窗强制更新。

重要提醒:版本号存储时,一定要考虑并发写入的问题。比如两个线程同时写版本文件,可能导致数据损坏。建议用原子操作,或者加文件锁。

读取版本号时,要注意异常处理。文件可能损坏、可能不存在、可能格式不对。我曾经遇到过用户手动删除了版本文件,结果应用启动时读取失败,直接崩溃。所以读取代码一定要健壮:

function readVersion() {
  try {
    const data = fs.readFileSync('version.json', 'utf8');
    const parsed = JSON.parse(data);
    // 校验字段完整性
    if (!parsed.version || !parsed.buildNumber) {
      throw new Error('版本文件格式错误');
    }
    return parsed;
  } catch (error) {
    // 读取失败时,返回默认版本
    console.error('读取版本文件失败:', error.message);
    return { version: '0.0.0', buildNumber: 0 };
  }
}

嗯,这里要注意:默认版本号设为 0.0.0 是个好习惯。因为 0.0.0 比任何正式版本都小,这样后续更新逻辑能正常触发。如果你默认返回 99.99.99,那用户就永远收不到更新了。

最后说一句:版本号管理看似小事,但一旦出问题,影响面很大。我见过一个团队因为版本号比较写错,导致全量用户推送了错误的更新包,回滚花了整整两天。所以,写版本号相关的代码时,多写几个单元测试,准没错。