---
title: "版本号怎么定？从 1.0.0 理解语义化版本"
source: https://www.iowill.com/blog/2026/10/semantic-versioning/
date: 2026-10-09
updated: 2026-10-09
tags: [SemVer, 版本管理, 语义化版本, 软件工程]
categories: [编程开发]
---

# 版本号怎么定？从 1.0.0 理解语义化版本

给项目发一个新版本，代码改完了，版本号却不一定好定。

修复了一个影响很大的 Bug，要不要直接升到 `2.0.0`？新增了不少代码，但旧功能照常使用，又该怎么编号？还有常见的 `1.0.0-beta.1`，它和 `1.0.0` 到底差在哪里？

语义化版本（Semantic Versioning，简称 SemVer）提供了一套约定：用三段数字说明一次更新对使用者的影响。判断时，先看原有使用方式是否仍然成立，再看这次增加了功能，还是修正了错误。

这篇从 [SemVer 2.0.0 中文规范](https://semver.org/lang/zh-CN/)出发，用一个虚构的文章列表组件举例。它已经发布了 `1.4.2`，其他项目通过 `listPosts()` 读取文章。各类改动的版本号以此为起点。

## 三段数字分别是什么

```text
1 . 4 . 2
│   │   └─ 修订号 Patch
│   └───── 次版本号 Minor
└───────── 主版本号 Major
```

对于已经进入 `1.0.0` 及以后阶段的项目，可以先按这张表判断：

| 这次发布的变化 | 从 1.4.2 升到 | 使用者需要关注什么 |
| --- | --- | --- |
| 兼容原有用法的问题修复 | `1.4.3` | 哪个错误得到了修正 |
| 兼容原有用法的功能新增 | `1.5.0` | 新功能怎样使用 |
| 公共 API 出现不兼容修改 | `2.0.0` | 旧代码需要怎样迁移 |

这里的公共 API，是项目明确提供给别人依赖的接口和行为。它可以是函数的参数、返回值，也可以是有文档约定的命令行选项或配置格式。

先写清楚这些约定，才能判断更新是否兼容。假如文档承诺 `listPosts()` 返回文章数组，调用方就有理由写出这样的代码：

```javascript
const posts = listPosts();
posts.map(post => post.title);
```

后续修改要考虑这段代码还能不能按约定工作。

![从 1.4.2 出发，修复、新功能和不兼容修改分别对应 1.4.3、1.5.0 和 2.0.0](https://img.iowill.com/file/%E7%BC%96%E7%A8%8B%E5%BC%80%E5%8F%91/%E7%89%88%E6%9C%AC%E5%8F%B7%E6%80%8E%E4%B9%88%E5%AE%9A-%E4%BB%8E1.0.0%E7%90%86%E8%A7%A3%E8%AF%AD%E4%B9%89%E5%8C%96%E7%89%88%E6%9C%AC/body1.png)

*三个独立的升级选择，按本次变更对公共 API 的影响判断。*

## 修复 Bug 后，版本号怎么升

假设组件约定按发布时间从新到旧排列文章，但某些日期被当成字符串错误比较，导致排序错乱。修正比较逻辑后，函数参数、返回类型和排序约定都保持原样。

这次可以发布为：

```text
1.4.2 → 1.4.3
```

即使排查花了三天，最后修改了几百行，判断仍然取决于修复对使用者的影响。严重 Bug 也可能通过修订版本修复。

不过，写着“修复 Bug”并不代表一定适合只升修订号。

例如，原先公开约定文章 ID 可以是字符串，现在为了简化校验，只允许传数字。原本合法的 `getPost("42")` 开始报错，这就改变了使用约定。不能因为提交说明写了 `fix`，便忽略兼容性变化。

另一种更难处理的情况是：某个行为确实不符合文档，但已经被大量使用者依赖。此时需要评估迁移成本，说明受影响范围，必要时通过主版本发布。规范的 FAQ 也讨论了这种修复对使用者造成重大影响的情况。[参考：SemVer FAQ](https://semver.org/lang/zh-CN/#faq)

## 新增功能后，版本号怎么升

为了让调用方可以选择文章分类，给 `listPosts()` 增加一个可选参数：

```javascript
// 原有调用仍然返回全部文章
listPosts();

// 新增按分类筛选的用法
listPosts({ category: "编程开发" });
```

只要原有调用的行为保持兼容，这属于功能新增。如果上一个版本已经是 `1.4.3`，这次就发布 `1.5.0`。

注意最后一段归零了，不是接着写 `1.5.3`。次版本递增时，修订号从零开始。

如果这次同时修了几个 Bug，也可以一起包含在 `1.5.0` 中。一批变更可以在同一个版本发布，版本号按其中需要的最高级别确定。

还有一种容易漏掉的变化：**把公共 API 标记为弃用，也需要递增次版本号。** “弃用”表示建议使用者开始迁移，旧接口此时仍然可用；直接删除则是另一回事。[参考：SemVer 第 7、8 条](https://semver.org/lang/zh-CN/)

## 哪些改动需要升主版本号

组件后来要支持分页。开发者决定把返回值从数组改成包含总数的对象：

```javascript
// 旧返回值
[{ id: 1, title: "第一篇文章" }]

// 新返回值
{
  items: [{ id: 1, title: "第一篇文章" }],
  total: 48
}
```

新结构看起来更方便，但之前的 `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 的模块版本文档](https://go.dev/doc/modules/version-numbers)说明，v2 及以上主版本通常要在模块路径中体现版本后缀。这是 Go 的模块管理规则，不能照搬为所有项目的要求。

## 版本号后面的 alpha、beta、rc 是什么

正式发布 `2.0.0` 前，可以先提供试用版本，让调用方提前检查迁移是否顺利：

```text
2.0.0-alpha.1
2.0.0-beta.1
2.0.0-rc.1
2.0.0
```

这些带连字符的后缀属于先行版本标识，实际项目也常称为预发布版本。`alpha`、`beta`、`rc` 是常见命名习惯；各阶段具体做多少测试、开放给哪些人，要看项目自己的说明。

比较版本时，相同三段数字下的先行版本低于正式版本。标识中的独立数字按数值比较，所以 `beta.2` 排在 `beta.11` 前面。

还可能看到这样的编号：

```text
2.0.0+build.123
```

加号后面是构建元数据，可以用于记录构建身份，但不参与 SemVer 的优先级比较。不能据此把 `2.0.0+build.124` 判为比 `2.0.0+build.123` 更高的语义化版本。[参考：SemVer 第 9—11 条](https://semver.org/lang/zh-CN/)

![从 alpha、beta、rc 到正式版本的顺序，以及独立展示的构建元数据](https://img.iowill.com/file/%E7%BC%96%E7%A8%8B%E5%BC%80%E5%8F%91/%E7%89%88%E6%9C%AC%E5%8F%B7%E6%80%8E%E4%B9%88%E5%AE%9A-%E4%BB%8E1.0.0%E7%90%86%E8%A7%A3%E8%AF%AD%E4%B9%89%E5%8C%96%E7%89%88%E6%9C%AC/body2.png)

*预发布命名示例；构建元数据不参与版本优先级比较。*

## 升级依赖时，版本号能告诉我们什么

给自己的项目定版本，是在向使用者说明变化；安装别人的依赖，则是在解读对方的承诺。

以 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 文档](https://github.com/npm/node-semver#ranges)。

允许安装的范围也不等于当前实际安装的版本；检查项目时还要结合锁文件与已安装的依赖。

即使版本号表示兼容，升级也可能遇到回归错误或维护者判断失误。实际操作时，我更建议先看变更记录，再验证项目依赖的关键行为。例如这个文章组件，就至少要检查旧调用、排序结果和分页返回值。

## 发布前，把变化写给使用者看

给上述虚构组件整理一份简短发布记录，可以写成：

```text
1.4.3
修复：文章发布时间排序错误，原有调用方式保持不变。

1.5.0
新增：listPosts 支持可选的 category 筛选参数。

2.0.0
变更：listPosts 返回值由数组调整为 { items, total }。
迁移：将 posts.map(...) 改为 result.items.map(...)。
```

版本号提示变化级别，发布记录说明具体影响。[Keep a Changelog](https://keepachangelog.com/zh-CN/1.1.0/)建议为人整理有意义的变更，而不是把所有提交记录直接堆给读者。

如果项目采用[约定式提交](https://www.conventionalcommits.org/zh-hans/v1.0.0/)，`fix`、`feat` 和 `BREAKING CHANGE` 可以为发布工具提供判断依据。但自动化依赖准确的提交描述；接口已经不兼容时，仅仅写上 `fix` 无法让它变得兼容。

已经发布的版本内容也应保持不变。发现问题后发一个新版本，并交代修正内容，避免两位使用者拿着相同版本号，却得到不同代码。

以后再纠结该升哪一位，可以先把这次更新写成一句面向使用者的话：原有用法是否要改？如果不用改，这次是增加了能力，还是修好了原本承诺的行为？版本号通常就有了答案。
