语义化版本规范速查表 - SemVer 版本号格式大全

一个不谨慎的 ^ 或 ~ 约束,往往就暗中决定了这次部署会不会引入破坏性升级。本表覆盖 MAJOR.MINOR.PATCH 结构、先行版本号、比较运算符,以及你在 package.json 里真正会写的范围写法(^ ~ >= < * 与组合)。写依赖时对照,能精确表达"允许升哪种版本、锁死哪些",也便于看懂"为什么 npm 装了 X 这个版本"。读完可带着意图读写版本范围。

参考手册·共 33 条命令·最后更新 2026-07-21
semver版本npm规范

典型使用场景

面向维护 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.PATCH
主版本.次版本.修订号
1.2.3
示例:主版本 1,次版本 2,修订号 3
MAJOR:不兼容的 API 变更
破坏性更新时递增
MINOR:向下兼容的功能新增
新增功能时递增
PATCH:向下兼容的问题修复
修复 bug 时递增

先行版本号与构建元数据 6

1.0.0-alpha.1
内部测试版(alpha)
1.0.0-beta.2
公开测试版(beta)
1.0.0-rc.1
候选发布版(release candidate)
alpha < beta < rc < 正式版
先行版本号优先级顺序
1.0.0-alpha < 1.0.0-alpha.1
同标识按数字递增比较
1.0.0+20260721
构建元数据(+ 号,不影响优先级)

版本比较运算符 5

=1.2.3
精确匹配指定版本
>1.2.3
大于指定版本(不含)
>=1.2.3
大于等于指定版本
<2.0.0
小于指定版本(不含)
<=1.9.9
小于等于指定版本

npm 版本范围符号 7

^1.2.3
允许 1.x.x,不允许 2.0.0(兼容版本)
~1.2.3
允许 1.2.x,不允许 1.3.0(接近版本)
1.2.3 - 2.3.4
区间范围(>=1.2.3 且 <=2.3.4)
1.x
通配符,等价于 >=1.0.0 <2.0.0
1.2.x
通配符,等价于 >=1.2.0 <1.3.0
*
任意版本(不推荐,易引入破坏性升级)
latest
最新稳定版(dist-tag)

npm 高级范围语法 5

1.2.3 || >=2.0.0
或运算,匹配任一范围
>=1.2.3 <2.0.0
空格表示 AND,需同时满足
~1.2 || ^2.0.0
复合范围组合使用
github:owner/repo
从 Git 仓库安装
file:./local-pkg
从本地路径安装

版本变更与发布规则 5

feat: 新功能
对应 MINOR 递增
fix: 修复 bug
对应 PATCH 递增
BREAKING CHANGE: 破坏性变更
对应 MAJOR 递增
0.x.x
主版本号为 0 表示初始开发,API 不稳定
1.0.0
首个正式版,标志着公共 API 稳定

参数矩阵

参数作用示例
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. 1看某个范围表达式实际覆盖哪些版本

    npm view lodash@'^1.2.3' versions --json

    输出即该范围可解析到的版本列表,超出列表说明锁得太窄或写法有误。

  2. 2确认已安装版本与依赖树是否一致

    npm ls --depth=0

    出现 invalid 或 UNMET DEPENDENCY 说明锁定版本与声明范围冲突。

  3. 3按提交信息推导下一版本号

    npx standard-version --dry-run

    dry-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日,内容持续校对官方文档。

联系我们

命令或描述有误?提交反馈、商务合作或产品建议都可发送邮件给我们。

联系我们