2. 版本号规范:SemVer、CalVer 等主流约定详解

版本号这东西,看着简单,坑其实不少。我记得刚带团队那会儿,有个依赖库从 1.2.3 升到 1.3.0,结果把整个 CI 搞崩了——因为对方偷偷改了接口签名。后来我才意识到,版本号不是随便写个数字就完事的,得有规矩。今天我就把主流的几种规范掰开了讲。

2.1 SemVer:最通用的语义化版本

SemVer 全称 Semantic Versioning,语义化版本控制。说白了,就是用三段数字 MAJOR.MINOR.PATCH 来传递变更意图。

  • MAJOR(主版本号):做了不兼容的 API 修改。你升级的时候得小心,很可能要改代码。
  • MINOR(次版本号):新增了向下兼容的功能。理论上你只管升,不会出问题(但实际嘛…后面说)。
  • PATCH(修订号):做了向下兼容的问题修正。通常就是修 bug,不涉及功能增减。

为什么我会强调“理论上”?因为我遇到过好几次,厂商说“这只是个 MINOR 升级”,结果内部改了核心逻辑。所以,我个人习惯是:即使标了 MINOR,也要跑一遍回归测试。

核心规则:

版本号一旦发布,就不能改内容。任何变更都必须发新版本。

先有公共 API 的定义,才有版本号的意义——连 API 都说不清楚,SemVer 就是空中楼阁。

2.2 预发布版本与构建元数据

除了三部分,SemVer 还可以加后缀:1.0.0-alpha.1、2.3.0-rc.1+build.20240301。这部分很多人用得随意,我建议按以下套路来:

后缀类型 示例 说明
alpha1.0.0-alpha.1内部早期测试,API 可能变
beta1.0.0-beta.2功能完整,继续找 bug
rc1.0.0-rc.3候选版,基本稳定
build metadata1.0.0+20240301主要用于区分不同构建,不参与优先级比较

一个小技巧:预发布版本的优先级 低于 正式版。比如 1.0.0-rc.1 < 1.0.0。这在依赖解析时很关键——如果你用了 ^1.0.0-alpha,npm 可能会给你装上正式版,因为正式版优先级更高。

2.3 CalVer:时间就是版本号

CalVer 全称 Calendar Versioning。不靠语义,靠日期。常见格式:YYYY.MM.DD 或 YY.MM.MICRO。

什么时候用 CalVer?我个人经验是:

  • 持续迭代的产品:比如 Ubuntu 用 24.04,一看就知道是 2024 年 4 月发布的。
  • 没有严格 API 契约的库:比如一些 CLI 工具,用户不太关心你是否破坏了 API,更关心能不能跑起来。
  • 项目快速演进:不想花心思管理语义化版本,日期一眼看出新旧。

CalVer 的缺点是:它不告诉你兼容性。你升级 2024.05.01 到 2024.06.01,可能一切正常,也可能原地爆炸。我曾经踩过这个坑——一个内部工具用了 CalVer,结果跨了俩月升级后,配置格式全变了,没有任何警告。

曾经有次… 我在一个开源项目中看到维护者把 2.3.0 和 2024-03-15 混着用,结果 CI 脚本里写死了版本格式,一到年底就报错。统一规范!

2.4 其他小众但实用的约定

还有一些规范,你可能见过但未必深究过:

  • ZeroVer:永远在 0.x.x,申明不稳定。适合早期原型,但别拖太久,容易让用户失去信心。
  • RomVer:浪漫化版本,比如 1.0.0-beautiful-sunset。不实用,慎用。
  • Sentinel:干脆不写版本号,用哈希或日期戳。一般用于一次性交付的制品。

我个人建议,除非你的项目生命周期极短,否则尽量用 SemVer 或 CalVer。别搞花活。

2.5 版本范围与依赖锁定

光有版本号还不够,还得决定“允许用哪些范围”。来看几种常见写法(以 npm 为例):

// package.json 示例
"dependencies": {
  "react": "^18.2.0",    // 允许 18.x.x,不锁 MAJOR
  "lodash": "~4.17.21",  // 允许 4.17.x,不锁 MINOR
  "express": "4.18.1",   // 锁定精确版本
  "vue": "^3.0.0"        // 允许 >=3.0.0 <4.0.0
}

这里有个实用经验:应用项目(比如 Web App)建议锁定 PATCH 版本,用 ~ 或精确版本,避免第三方热修复引入不兼容。库项目(比如你发布的 npm 包)用 ^ 放宽范围,让用户能自动获得补丁。

我曾经因为一个库用了 ^1.2.0,结果对方在 1.3.0 里偷偷改了方法签名,导致我的 App 上线后崩溃。从那以后,我的主项目一律锁定版本号,并且用 package-lock.json / yarn.lock 锁死全量依赖树。

2.6 一张图看懂版本号选择逻辑

版本号规范决策指南 是否需要语义兼容性信息? SemVer 例子: 2.1.3-rc.1 CalVer 例子: 2024.05.01 项目发布版本锁定方式:^ / ~ / 精确 是否为主应用? → 锁定PATCH版本 check: lockfile + CI 验证 无兼容保证 只能全量覆盖

2.7 版本号规范化落地清单

光知道概念不行,还得落地。我长期实践下来,这几条是必做的:

  1. 项目根目录写 VERSION 或 version.txt:别藏在配置文件的角落里。
  2. CI 中自动校验版本号格式:用正则 /^\d+\.\d+\.\d+/ 跑一遍,不符合直接拒绝合并。
  3. 发布前生成 CHANGELOG:用 keepachangelog 标准,别凭感觉写。
  4. 禁止手动修改发布版本的版本号:全部通过脚本或 CI 工具自动 bump。

常用的自动化工具:semantic-release(配合 commit message 自动升级)、bump2version(适合简单项目)。我个人偏爱 semantic-release,因为它还能自动生成 changelog 和 tag,省心。

说到 tag,Git 的 tag 最好和版本号一一对应。比如 v2.3.1 就对应 2.3.1 版本。不要出现 release-2 这种模糊命名,否则你以后查版本历史会想骂人——我骂过。

2.8 避坑指南

我曾经… 被一个包的预发布版本坑过:2.0.0-alpha.1 到 2.0.0-alpha.2,中间接口变了但版本号前缀没变。从那以后,我强制要求:预发布版本每次发布必须用新的后缀序号,不允许覆盖。

还有一个坑:0.x.x 版本不适用 SemVer 的 MINOR/PATCH 语义。0.x 阶段,MINOR 升级也可能包含破坏性变更。所以如果项目还在 0.x,依赖方最好锁定精确版本。

嗯,版本号规范就聊到这儿。记住一条原则:版本号是给开发者看的承诺,不是留给未来的麻烦。定好规矩,用工具自动化,别靠人工记忆。