语义化版本控制 package.json 中数字的实际含义
每个 package.json 文件都包含版本字符串,但大多数开发者只是信任 caret(^)符号而不知道它允许什么。本指南将解释 MAJOR.MINOR.PATCH 协议,以及 ^、~、>= 和 * 符号分别会接受哪些更新。
每个 JavaScript 项目都有一个 package.json。大多数开发者已经输入过数百次,却从未真正思考过版本字符串的含义。但如果你问他们 npm install some-library 实际上允许什么——也就是说,npm 会乐意拉取哪些版本——他们通常只会耸耸肩。 ^1.2.3 这并不是一个微不足道的差距。一个误解的版本范围,会导致一个原本在新机器上运行正常的 CI 流水线突然中断,而这个流水线在几个月前是完全正常的。理解这些数字背后的协议,是那些投入少、回报高的事,它能区分那些几分钟内就能调试版本问题的开发者,和那些花费数小时才解决的开发者。
MAJOR.MINOR.PATCH 协议 npm install 语义化版本控制(semver)是一种三数字格式:
每个位置都承载着关于变更的具体承诺:
MAJOR MAJOR.MINOR.PATCH——破坏性变更。升级到一个主要版本可能需要你更新代码。
- MINOR ——新增功能,向后兼容。你的现有代码应继续正常运行。
- ——仅修复错误。安全升级,无需 API 变更。 这就是
- PATCH 协议
包发布所遵循的。从 升级到 应添加新功能而不破坏现有行为。升级到 2.3.1 到 2.4.0 是你的警告信号:升级前请务必查看变更日志。 3.0.0 在实践中,维护者有时会不小心在次要版本中引入破坏性变更。语义化版本控制并不强制执行——它只是一个约定。但它为你提供了一个评估风险的框架,而所有版本范围操作符都是基于这个框架构建的。
什么构成破坏性变更?
破坏性变更是指任何迫使消费者更新其代码的情况:
移除或重命名导出的函数、类或常量
- 改变函数的签名——移除参数、添加必需参数或改变返回类型
- 以一种导致调用代码行为错误的方式改变可观察行为
- 以不兼容的方式改变配置文件格式
- 添加一个新可选参数?那是 MINOR。添加一个新导出?那是 MINOR。修复一个有人错误地将其当作功能依赖的 bug?这需要判断,但通常归为 PATCH。如有疑问,请提升 MINOR 并清晰地记录说明。
package.json 中的版本范围操作符
打开任何
,你都会看到类似 package.json 的版本字符串。这些不是精确的固定值——它们是 "^4.17.21" 或 "~1.0.4"范围 ,告诉 npm 或 yarn 在解析依赖树时可以接受哪些版本。 caret ^ —— 兼容版本
当运行
时,caret 是默认操作符。它表示:“接受任何不改变最左侧非零数字的版本。”在实践中,对于稳定包来说,这意味着相同主要版本,任意次要或补丁版本: npm install零主要版本的行为是故意设计的。位于
^1.2.3 → >=1.2.3 <2.0.0 (any 1.x.x at or above 1.2.3)
^0.2.3 → >=0.2.3 <0.3.0 (zero-major: pins to minor)
^0.0.3 → >=0.0.3 <0.0.4 (zero-zero: pins to exact patch)
的包表示“不稳定 API”——任何次要版本都可能破坏功能。caret 尊重这一信号,变得更为保守。 0.x.x tilde ~ —— 仅补丁级别更新
tilde 更加保守。它接受新的补丁版本,但不会触及次要版本:
当你希望获取补丁修复,但又不确定该库对次要版本的语义化版本控制是否遵守,或者你的代码与特定 API 表面紧密耦合,而新功能发布通常伴随细微行为变化时,就应使用
~1.2.3 → >=1.2.3 <1.3.0 (any 1.2.x at or above 1.2.3)
~1.2 → >=1.2.0 <1.3.0
~1 → >=1.0.0 <2.0.0 (when only major given — same as ^1)
reach for ~ 。
比较操作符:>=, <=, >
你可以使用比较操作符编写任意范围。两个约束之间用空格分隔表示“AND”:
>=1.2.0 # at least 1.2.0, no upper bound
>=1.2.0 <2.0.0 # same as ^1.2.0 (explicit AND)
>1.2.0 <=1.5.0 # between these values, exclusive/inclusive
这些在 peerDependencies中最为常见,其中某个库会声明其兼容的主机版本。 "react": ">=16.8.0 <19.0.0" 通配符:* 和 x
通配符形式主要用于文档可读性;npm 将其视为零基础的 caret/tilde 操作符:
——任意版本。不要在生产环境中使用。
*或""——任意 1.x.x(等同于1.x或1.x.x——任意 1.2.x(等同于^1.0.0)1.2.x预发布版本~1.2.0)
预发布版本看起来像
。它们被视为 1.0.0-alpha.1, 2.0.0-beta.3, 或者 3.1.0-rc.1低于 具有相同数字的正式版本—— 关键的是, 1.0.0-alpha.1 < 1.0.0.
范围操作符不会自动包含预发布版本 。一个范围不会拉入 ^1.0.0 ,即使它在技术上满足 1.1.0-beta.1。你需要明确选择: >=1.0.0 <2.0.0这是有意的安全措施。你很少希望 CI 会默默地拉取一个依赖项的预发布构建,因为其恰好满足版本范围。如果你在测试预发布版本,应主动进行。
npm install some-library@next
npm install some-library@2.0.0-beta.3
锁定文件不是可选的 -alpha 当 npm 解析你的
范围时,它会选择当前可用的最高兼容版本
在那一刻 ^1.2.3 今天你得到 。六个月后再次运行,你可能会得到。运行 npm install 。相同的 1.5.0,不同的依赖树,潜在的行为差异。 1.9.2这就是锁定文件解决的问题。 package.json(npm) 和
(yarn) 记录每个依赖项(直接和间接)的确切版本。当其他人克隆你的仓库并运行 package-lock.json 时,他们将获得完全相同的依赖树。 yarn.lock 将锁定文件提交到代码库。始终如此。 npm ci不提交锁定文件意味着:
不同开发者可能运行不同的依赖版本而不知情 你的 CI 环境可能悄然偏离你的本地环境
- 一个间接依赖项的更新可能会改变生产行为,而你的
- 没有任何明显变化
- 主要例外:已发布的库(而非应用程序)通常不将锁定文件提交到源代码控制,以便消费者可以自行解析其依赖树。如果你在开发一个应用程序,就没有理由将锁定文件排除在源代码控制之外。
git diff
package.json 中的 "latest" 始终是一个错误
偶尔你会在
中看到这种写法 package.json:
"dependencies": {
"some-package": "latest"
}
不要这样做。 "latest" 映射到 npm 上当前标记的版本——每当维护者发布新版本时,它都会改变。在一台新机器上运行的 latest 可能会拉取一个与你测试时完全不同的主要版本。 npm install 它可能在几周内运行良好,然后在包发布新主要版本时突然中断。更糟糕的是,它使
成为一个不可复现的规范——你无法在不手动检查 npm 的情况下了解你正在运行的版本。请锁定到一个真实版本,并让 caret 处理该范围内的安全更新。 package.json 检查某个版本是否满足一个范围
如果你不确定某个版本是否满足给定范围——特别是针对零主要版本包或不寻常的复合表达式——使用
在 iotools.cloud 上可以立即得到答案。输入范围( SemVer 版本计算器及范围测试器 )和候选版本,它会告诉你该约束是否满足。^1.2.3, ~0.5.0, >=2.0.0 <3.0.0这在审查依赖项升级的 PR、调试为何
解析到意外版本,或在发布库前检查一个 npm install 范围时非常有用。 peerDependencies 操作符
快速参考
| 解析为 | 例子 | 1.2.0 或更高,无上限 |
|---|---|---|
^ | ^1.2.3 | >=1.2.3 <2.0.0 |
~ | ~1.2.3 | >=1.2.3 <1.3.0 |
>= | >=1.2.0 | 任意版本(避免使用) |
* | * | 任意 1.2.x 补丁 |
x | 1.2.x | 精确匹配 |
| 正好是 1.2.3 | 1.2.3 | 语义化版本控制:package.json 中数字的实际含义 2 |
