版本号怎么定?从 1.0.0 理解语义化版本
给项目发一个新版本,代码改完了,版本号却不一定好定。
修复了一个影响很大的 Bug,要不要直接升到 2.0.0?新增了不少代码,但旧功能照常使用,又该怎么编号?还有常见的 1.0.0-beta.1,它和 1.0.0 到底差在哪里?
语义化版本(Semantic Versioning,简称 SemVer)提供了一套约定:用三段数字说明一次更新对使用者的影响。判断时,先看原有使用方式是否仍然成立,再看这次增加了功能,还是修正了错误。
这篇从 SemVer 2.0.0 中文规范出发,用一个虚构的文章列表组件举例。它已经发布了 1.4.2,其他项目通过 listPosts() 读取文章。下面分别看看几种改动该怎样编号。
三段数字分别是什么
1 | 1 . 4 . 2 |
对于已经进入 1.0.0 及以后阶段的项目,可以先按这张表判断:
| 这次发布的变化 | 从 1.4.2 升到 | 使用者需要关注什么 |
|---|---|---|
| 兼容原有用法的问题修复 | 1.4.3 |
哪个错误得到了修正 |
| 兼容原有用法的功能新增 | 1.5.0 |
新功能怎样使用 |
| 公共 API 出现不兼容修改 | 2.0.0 |
旧代码需要怎样迁移 |
这里的公共 API,是项目明确提供给别人依赖的接口和行为。它可以是函数的参数、返回值,也可以是有文档约定的命令行选项或配置格式。
先写清楚这些约定,才能判断更新是否兼容。假如文档承诺 listPosts() 返回文章数组,调用方就有理由写出这样的代码:
1 | const posts = listPosts(); |
后续修改要考虑这段代码还能不能按约定工作。

三个独立的升级选择,按本次变更对公共 API 的影响判断。
修复 Bug 后,版本号怎么升
假设组件约定按发布时间从新到旧排列文章,但某些日期被当成字符串错误比较,导致排序错乱。修正比较逻辑后,函数参数、返回类型和排序约定都保持原样。
这次可以发布为:
1 | 1.4.2 → 1.4.3 |
即使排查花了三天,最后修改了几百行,判断仍然取决于修复对使用者的影响。严重 Bug 也可能通过修订版本修复。
不过,写着“修复 Bug”并不代表一定适合只升修订号。
例如,原先公开约定文章 ID 可以是字符串,现在为了简化校验,只允许传数字。原本合法的 getPost("42") 开始报错,这就改变了使用约定。不能因为提交说明写了 fix,便忽略兼容性变化。
另一种更难处理的情况是:某个行为确实不符合文档,但已经被大量使用者依赖。此时需要评估迁移成本,说明受影响范围,必要时通过主版本发布。规范的 FAQ 也讨论了这种修复对使用者造成重大影响的情况。参考:SemVer FAQ
新增功能后,版本号怎么升
接下来,希望调用方可以选择文章分类,于是给 listPosts() 增加一个可选参数:
1 | // 原有调用仍然返回全部文章 |
只要原有调用的行为保持兼容,这属于功能新增。如果上一个版本已经是 1.4.3,这次就发布 1.5.0。
注意最后一段归零了,不是接着写 1.5.3。次版本递增时,修订号从零开始。
如果这次同时修了几个 Bug,也可以一起包含在 1.5.0 中。一批变更可以在同一个版本发布,版本号按其中需要的最高级别确定。
还有一种容易漏掉的变化:把公共 API 标记为弃用,也需要递增次版本号。 “弃用”表示建议使用者开始迁移,旧接口此时仍然可用;直接删除则是另一回事。参考:SemVer 第 7、8 条
哪些改动需要升主版本号
组件后来要支持分页。开发者决定把返回值从数组改成包含总数的对象:
1 | // 旧返回值 |
新结构看起来更方便,但之前的 posts.map(...) 已经不能用了。调用方必须改成 result.items.map(...)。
如果保留 listPosts() 这个名字却直接改变它的返回约定,就应该通过新的主版本发布,例如从 1.5.0 升到 2.0.0,并提供迁移说明。
也有兼容的做法:保留原函数,另加 listPostsPage() 返回分页对象。新增能力没有破坏旧接口,就可以按次版本发布。同一个功能需求,采用不同的接口设计,可能对应不同的版本级别。
删除公开函数、把可选参数改成必填、改变文档承诺的默认行为,都需要检查旧代码是否仍然有效。一次很小的接口修改,也可能需要升主版本号。
0.x.y 和 1.0.0 有什么区别
0.x.y 表示初始开发阶段,公共 API 尚未形成稳定承诺。使用者不能仅凭主版本号没变,就认为所有更新都兼容。
1.0.0 则是一个明确的起点:项目开始围绕已定义的公共 API 管理兼容性。它不意味着软件没有 Bug,也不要求把设想中的所有功能都实现。
以这个组件为例,如果参数、返回结构还在频繁试验,保留 0.x.y 很合理。等到其他项目已经依赖这些接口,就需要认真决定哪些行为要长期支持,并为它们补上文档和验证。
不同生态还可能有额外约定。例如 Go 的模块版本文档说明,v2 及以上主版本通常要在模块路径中体现版本后缀。这是 Go 的模块管理规则,不能照搬为所有项目的要求。
版本号后面的 alpha、beta、rc 是什么
正式发布 2.0.0 前,可以先提供试用版本,让调用方提前检查迁移是否顺利:
1 | 2.0.0-alpha.1 |
这些带连字符的后缀属于先行版本标识,实际项目也常称为预发布版本。alpha、beta、rc 是常见命名习惯;各阶段具体做多少测试、开放给哪些人,要看项目自己的说明。
比较版本时,相同三段数字下的先行版本低于正式版本。标识中的独立数字按数值比较,所以 beta.2 排在 beta.11 前面。
还可能看到这样的编号:
1 | 2.0.0+build.123 |
加号后面是构建元数据,可以用于记录构建身份,但不参与 SemVer 的优先级比较。不能据此把 2.0.0+build.124 判为比 2.0.0+build.123 更高的语义化版本。参考:SemVer 第 9—11 条

预发布命名示例;构建元数据不参与版本优先级比较。
升级依赖时,版本号能告诉我们什么
给自己的项目定版本,是在向使用者说明变化;安装别人的依赖,则是在解读对方的承诺。
以 npm 为例,package.json 中的范围符号决定了哪些版本允许被选择。下面只比较正式版本,预发布版本另有匹配条件:
| 声明 | 允许的正式版本范围 |
|---|---|
1.4.2 |
仅 1.4.2 |
~1.4.2 |
大于等于 1.4.2,小于 1.5.0 |
^1.4.2 |
大于等于 1.4.2,小于 2.0.0 |
^0.2.3 |
大于等于 0.2.3,小于 0.3.0 |
这也解释了为什么不能把 ^ 一概理解成“固定主版本号”。在主版本为零的情况下,它的范围更窄。具体匹配规则见 npm/node-semver 文档。
允许安装的范围也不等于当前实际安装的版本;检查项目时还要结合锁文件与已安装的依赖。
即使版本号表示兼容,升级也可能遇到回归错误或维护者判断失误。实际操作时,我更建议先看变更记录,再验证项目依赖的关键行为。例如这个文章组件,就至少要检查旧调用、排序结果和分页返回值。
发布前,把变化写给使用者看
给上述虚构组件整理一份简短发布记录,可以写成:
1 | 1.4.3 |
版本号提示变化级别,发布记录说明具体影响。Keep a Changelog建议为人整理有意义的变更,而不是把所有提交记录直接堆给读者。
如果项目采用约定式提交,fix、feat 和 BREAKING CHANGE 可以为发布工具提供判断依据。但自动化依赖准确的提交描述;接口已经不兼容时,仅仅写上 fix 无法让它变得兼容。
已经发布的版本内容也应保持不变。发现问题后发一个新版本,并交代修正内容,避免两位使用者拿着相同版本号,却得到不同代码。
以后再纠结该升哪一位,可以先把这次更新写成一句面向使用者的话:原有用法是否要改?如果不用改,这次是增加了能力,还是修好了原本承诺的行为?版本号通常就有了答案。