语义化版本规范速查表 - SemVer 版本号格式大全
一个不谨慎的 ^ 或 ~ 约束,往往就暗中决定了这次部署会不会引入破坏性升级。本表覆盖 MAJOR.MINOR.PATCH 结构、先行版本号、比较运算符,以及你在 package.json 里真正会写的范围写法(^ ~ >= < * 与组合)。写依赖时对照,能精确表达"允许升哪种版本、锁死哪些",也便于看懂"为什么 npm 装了 X 这个版本"。读完可带着意图读写版本范围。
典型使用场景
面向维护 npm 包、CLI 工具和依赖锁定的开发者。语义化版本把版本号拆成 MAJOR.MINOR.PATCH 三段,每段的递增对应不同性质的变更;表里同时给出「什么变更该动哪一段」的判定(feat → MINOR、fix → PATCH、BREAKING CHANGE → MAJOR)。下半部分是本表真正的重点:范围表达式怎么写才安全。^1.2.3 允许整个 1.x、~1.2.3 只允许 1.2.x,两者的边界差一个数量级;预发布版本与构建元数据在比较时另有规则。锁定版本则用 =1.2.3 或 1.2.3 - 2.3.4。
版本号结构 5
MAJOR.MINOR.PATCH1.2.3MAJOR:不兼容的 API 变更MINOR:向下兼容的功能新增PATCH:向下兼容的问题修复先行版本号与构建元数据 6
1.0.0-alpha.11.0.0-beta.21.0.0-rc.1alpha < beta < rc < 正式版1.0.0-alpha < 1.0.0-alpha.11.0.0+20260721版本比较运算符 5
=1.2.3>1.2.3>=1.2.3<2.0.0<=1.9.9npm 版本范围符号 7
^1.2.3~1.2.31.2.3 - 2.3.41.x1.2.x*latestnpm 高级范围语法 5
1.2.3 || >=2.0.0>=1.2.3 <2.0.0~1.2 || ^2.0.0github:owner/repofile:./local-pkg版本变更与发布规则 5
feat: 新功能fix: 修复 bugBREAKING CHANGE: 破坏性变更0.x.x1.0.0参数矩阵
| 参数 | 作用 | 示例 |
|---|---|---|
MAJOR.MINOR.PATCH | 三段版本号,分别对应不兼容变更、兼容新增、兼容修复 | 1.2.3 :: 示例:主版本 1,次版本 2,修订号 3 |
^1.2.3 | 兼容范围:允许 1.x 全部补丁与次版本,挡在 2.0.0 之前 | ^1.2.3 :: 允许 1.x.x,不允许 2.0.0(兼容版本) |
~1.2.3 | 接近范围:只允许 1.2.x,1.3.0 就会被拒 | ~1.2.3 :: 允许 1.2.x,不允许 1.3.0(接近版本) |
预发布标识 alpha/beta/rc | 先行版本优先级低于正式版,同标识按数字递增比较 | 1.0.0-alpha < 1.0.0-alpha.1 |
+ 构建元数据 | 记录构建信息但完全不参与优先级比较 | 1.0.0+202****0721 |
区间与或运算 | 空格表示 AND,双竖线表示 OR,可表达跨主版本的多段需求 | ~1.2 || ^2.0.0 |
提交信息前缀 | 配合 conventional commits 自动推导下一版本号 | BREAKING CHANGE: 破坏性变更 :: 对应 MAJOR 递增 |
易错点与避坑指南
现象以为 1.0.0-alpha 排在 1.0.0 前面装到了预发布版。
原因带预发布标识的版本优先级低于正式版,且预发布版本默认不会被自动选中。
处置显式写 1.0.0-alpha 才会装到它;正式发布走 1.0.0 而非继续沿用 alpha。
现象两个构建号不同的版本被认为有区别,或反过来被当成同一个优先版本。
原因构建元数据(+ 之后的内容)只做标识用途,不参与优先级比较。
处置需要区分构建产物就靠 + 号后的标识,需要比较优先级则只看前三段与预发布标识。
现象依赖写成 * 或 ^ 之后,某天 CI 突然挂了,因为上游发布了破坏性升级。
原因* 接受任意版本,^ 允许整个次版本线,上游一旦动 MAJOR 就会引入不兼容改动。
处置CI 环境用锁定文件(1.2.3 - 2.3.4 或精确版本)固定,应用代码才用 ^。
现象依赖某个 0.x 的包时,装到新补丁版本行为就变了。
原因主版本号为 0 表示 API 尚未稳定,此时 MINOR 段的递增同样可能是破坏性变更。
处置对 0.x 依赖用 ~0.x.y 把范围锁在同一个次版本内。
排障路径
1看某个范围表达式实际覆盖哪些版本
npm view lodash@'^1.2.3' versions --json输出即该范围可解析到的版本列表,超出列表说明锁得太窄或写法有误。
2确认已安装版本与依赖树是否一致
npm ls --depth=0出现 invalid 或 UNMET DEPENDENCY 说明锁定版本与声明范围冲突。
3按提交信息推导下一版本号
npx standard-version --dry-rundry-run 只打印 feat/fix/BREAKING CHANGE 各自会推动哪一段递增,不写文件。
命令示例
按 SemVer 规则排出预发布到正式版的优先级
node -e "
function cmp(a, b) {
const A = a.split(/[.+-]/), B = b.split(/[.+-]/);
const pa = a.includes('-'), pb = b.includes('-');
for (let i = 0; i < 3; i++) { const d = +A[i] - +B[i]; if (d) return d; }
if (pa !== pb) return pa ? -1 : 1;
if (pa && pb) {
const x = a.split('-').slice(3), y = b.split('-').slice(3);
for (let i = 0; i < Math.max(x.length, y.length); i++) {
if (x[i] === undefined) return -1;
if (y[i] === undefined) return 1;
if (x[i] !== y[i]) return x[i] < y[i] ? -1 : 1;
}
}
return 0;
}
const v = ['1.0.0-alpha', '1.0.0-alpha.1', '1.0.0-beta.2', '1.0.0-rc.1', '1.0.0', '1.0.1', '1.2.0', '2.0.0'];
console.log(v.sort(cmp).join(' < '));
"逐段比较前三个数字,再按「有预发布标识的排在正式版之前」处理,最后同标识按数字递增。
输出
1.0.0-alpha < 1.0.0-alpha.1 < 1.0.0-beta.2 < 1.0.0-rc.1 < 1.0.0 < 1.0.1 < 1.2.0 < 2.0.0
把 ^ 与 ~ 展开成真实的上下界
node -e "
function expand(r) {
const m = r.match(/^([\^~]?)(\d+)\.(\d+)\.(\d+)$/);
const [, op, M, mi] = m;
const MA = +M;
if (op === '^') return MA > 0 ? '>=' + r.slice(1) + ' <' + (MA + 1) + '.0.0' : '>=' + r.slice(1) + ' <' + MA + '.' + (+mi + 1) + '.0';
if (op === '~') return '>=' + r.slice(1) + ' <' + MA + '.' + (+mi + 1) + '.0';
return '=' + r;
}
for (const r of ['^1.2.3', '~1.2.3', '^0.2.3', '^0.0.3']) console.log(r.padEnd(8), '==>', expand(r));
"主版本非 0 时 ^ 只挡住下一个主版本;主版本为 0 时 ^ 改为挡住下一个次版本,这就是 0.x 阶段必须谨慎升级的原因。
输出
^1.2.3 ==> >=1.2.3 <2.0.0 ~1.2.3 ==> >=1.2.3 <1.3.0 ^0.2.3 ==> >=0.2.3 <0.3.0 ^0.0.3 ==> >=0.0.3 <0.1.0
验证构建元数据不参与优先级比较
node -e "
function cmp(a, b) {
const pa = a.includes('-'), pb = b.includes('-');
const A = a.split(/[.+-]/), B = b.split(/[.+-]/);
for (let i = 0; i < 3; i++) { const d = +A[i] - +B[i]; if (d) return d; }
if (pa !== pb) return pa ? -1 : 1;
return 0;
}
console.log(cmp('1.0.0+build.1', '1.0.0+build.2'), cmp('1.0.0', '1.0.0'));
"两个返回值都是 0,说明带不同构建号的版本与不带构建号的版本优先级完全相同。
输出
0 0
按提交信息前缀推导下一版本号
node -e "
const R = [['fix: x', '1.2.3'], ['feat: x', '1.2.3'], ['BREAKING CHANGE: x', '1.2.3']];
for (const [msg, v] of R) {
const [M, m, p] = v.split('.').map(Number);
const next = msg.startsWith('BREAKING') ? (M + 1) + '.0.0' : msg.startsWith('feat') ? M + '.' + (m + 1) + '.0' : M + '.' + m + '.' + (p + 1);
console.log(msg, '->', next);
}
"三条提交信息从同一个 1.2.3 出发,分别落到 PATCH、MINOR、MAJOR 三段,与 conventional commits 的约定一致。
输出
fix: x -> 1.2.4 feat: x -> 1.3.0 BREAKING CHANGE: x -> 2.0.0
常见坑与注意事项
- 预发布版本默认不装:写 ^1.0.0 拿到的是正式版,要 alpha 或 rc 必须在范围里显式写出标识。
- 构建元数据不影响优先级,但 npm 仍把不同构建号当成不同构件,混用容易触发重复依赖冲突。
- 把依赖写成 * 或完全省略版本,等于把解析结果交给上游,某天上游发破坏性升级 CI 就会突然变红。
- 0.x 阶段的 ^ 实际挡住的是下一个次版本,升级 0.2.x 到 0.3.0 就已包含不兼容变更,别按主版本直觉判断。
- 破坏性变更必须递增主版本号,哪怕改动只有一行;用 MINOR 蒙混会让下游在自动升级时炸掉。
提示
- ^1.2.3 等价于 >=1.2.3 <2.0.0,是 npm install 的默认行为。
- ~1.2.3 等价于 >=1.2.3 <1.3.0,只允许补丁版本更新。
- package-lock.json 锁定精确版本,避免 ^ 和 ~ 带来的不确定性。
- 破坏性变更必须递增主版本号,即使只是小改动。
- 主版本号为 0(0.x.x)时,任何向下不兼容的变更都应递增次版本号。
常见问题
预发布版本(1.0.0-alpha)为什么不参与普通的版本范围匹配?
因为规范把预发布版本单独隔离。范围表达式如 ^1.0.0 只匹配正式版,1.0.0-alpha 虽然在数值上小于 1.0.0,却不满足条件,除非你在范围里显式写出预发布标识(例如 ^1.0.0-alpha)。这个设计是为了防止 npm install 意外装到测试版:用户写 ^1.0.0 期待的是稳定渠道,只有明确表达意愿才能拿到 alpha / beta / rc。
构建元数据(1.0.0+build.1)既然不影响优先级,那它的用处是什么?
它是给人看的标签。+ 号之后的内容完全不参与版本比较——1.0.0+build.1 与 1.0.0+build.2 优先级完全相同,判断为同一个版本——用途是记录构建时间、提交哈希或打包环境。唯一的坑在 npm:即使优先级相同,两个不同构建号的包在包管理器眼里仍算不同的构件,可能被当成互相冲突的重复依赖。
主版本号为 0 时(0.x.x)应该怎么定版本号?
0 表示公共 API 尚未稳定,此时 MINOR 承担破坏性变更的职责。把 ^0.2.3 展开就是 >=0.2.3 <0.3.0——次版本号一旦递增,旧代码就装不上了,这是刻意的保护。所以 0.x 阶段应当把 MINOR 当作「下一个不兼容代际」来用,PATCH 才是安全的修复位;等到 API 稳定了再发布 1.0.0,把兼容性承诺正式交给消费者。
为什么依赖写成 * 或不写版本会突然让 CI 挂掉?
因为范围是持续解析的,而不是一次锁定。写 * 意味着每次安装都取当时的最新版本,上游某天发布 2.0.0 后,你的内容不变但解析结果变了。锁文件(package-lock.json)会把依赖钉在精确版本上,把解析变成一次性的;因此库作者适合用宽松范围(让使用者更自由),而应用项目则必须提交锁文件(把结果固定下来)。
1.x 和 1.2.x 这两个通配写法分别等价于什么范围?
1.x 等价于 >=1.0.0 <2.0.0,放开整个主版本;1.2.x 等价于 >=1.2.0 <1.3.0,只放开次版本内的修订号。通配符本质上是把某一段写成任意值,因此写 1.x 时那个位置的版本也会被忽略——包括忽略它已有的补丁号。通配范围虽然方便,但可复现性差,依赖它的项目应当同时提交锁文件。
由 巧匠 维护
公开更新于 2026年7月21日,内容持续校对官方文档。