语义化版本控制 版本号系统:你的 npm 安装所依赖的系统
语义化版本(Semver)的三个数字是一种协议:主版本号表示重大变更,次版本号表示新增功能,补丁版本号表示错误修复——当您的构建在运行 npm install 后失败时,有九成的情况是有人忽视了这一协议。本文将介绍版本号系统的运作方式,package.json 中 ^ 和 ~ 实际上代表的含义,以及为何必须提交锁定文件。
你的构建失败了。周五还能正常运行。 npm install 周一拉取了更新 react-query@5 现在你一半的钩子都消失了。你面对的堆栈跟踪之前并不存在,而某个地方的变更日志正在积灰。
这是一个语义化版本(semver)的故事。具体来说,这是你的责任。
这三个数字实际上意味着什么
MAJOR.MINOR.PATCH —— 就是这些。三个槽位,三条规则:
- PATCH (1.2.3 → 1.2.4):修复了错误。你的代码无需更改。你只是获得了更少的错误行为。
- ——仅修复错误。安全升级,无需 API 变更。 (1.2.3 → 1.3.0):新增了一个功能,且向后兼容。你不需要使用它,但它已经存在。
- MINOR (1.2.3 → 2.0.0):某些内容发生了变化。一个函数被重命名、移除或改变了签名。旧的 API 已经消失或行为不同。
这三条规则中的关键词是 向后兼容。MINOR 和 PATCH 是承诺:“我们没有破坏你正在使用的任何内容。” MAJOR 是警告:“我们确实破坏了。”
当维护者提升 MAJOR 版本,而你没有注意到,因为你将版本锁定在 package.json 中,且锁文件过时——那是因为你。 ^1.0.0 规范完全按设计运行。
语义化版本的社会契约
语义化版本(semver)是一种约定,而非法律。包可以声称遵循它,然后发布一个带有破坏性变更的 MINOR 版本。当这种情况发生时,就是维护者失信。但当一个包正确地提升 MAJOR 版本以表示破坏性变更,而你盲目地拉取它——那么你就是破坏了你自己构建的稳定性。
这就是变更日志存在的原因。一个条目说明“移除了已弃用的 CHANGELOG.md —— 请使用 v1Api 代替”是维护者履行了他们的责任。不阅读它,就是你忽视了自己的责任。变更日志只需两分钟阅读。它所避免的调试会话,是无法估量的。 v2Api “instead”是维护者在履行其责任时的体现。不阅读它,就是你忽视了自己的责任。变更日志只需两分钟即可阅读。它所避免的调试会话,并不真正存在。
^ 与 ~ —— 实际机制
在 package.json, ^ (caret) 和 ~ (tilde) 定义了版本范围。它们看起来相似,但行为完全不同。
Caret (^): 允许任何不提升 MAJOR 的版本。当你运行 npm 时,这是 npm 的默认行为。 npm install some-package.
^1.2.3解析为>=1.2.3 <2.0.0^0.2.3解析为>=0.2.3 <0.3.0—— 特例:0.x将 MINOR 视为破坏性变更^0.0.3解析为>=0.0.3 <0.0.4—0.0.x精确锁定,没有浮动空间
Tilde (~): 仅允许 PATCH 更新,且在指定的 MINOR 范围内。
~1.2.3解析为>=1.2.3 <1.3.0~1.2解析为>=1.2.0 <1.3.0—— 与~1.2.0~1解析为>=1.0.0 <2.0.0—— 等同于^1.0.0在这一点上
版本范围示例
| 范围 | 它允许的内容 | 精确匹配 |
|---|---|---|
1.2.3 | 正好是这个版本 | 仅 1.2.3 |
^1.2.3 | 任何 MINOR/PATCH ≥ 1.2.3 | 1.2.4、1.3.0、1.99.0 —— 不包括 2.0.0 |
^0.2.3 | 仅在 0.2.x 范围内的 PATCH | 0.2.4、0.2.99 —— 不包括 0.3.0 |
~1.2.3 | 仅在 1.2.x 范围内的 PATCH | 1.2.4、1.2.99 —— 不包括 1.3.0 |
~1.2 | 1.2.x 任意补丁版本 | 1.2.0, 1.2.1, 1.2.99 |
>=1.2.3 <2.0.0 | 显式范围 | 与 ^1.2.3 相同的结果 |
1.2.x | 1.2 任意补丁版本 | 1.2.0, 1.2.1, 1.2.99 |
* | 任何版本 | npm 感觉今天想安装的任何版本 |
这 * 范围是一种“相信我,兄弟”的版本策略。你没有锁定任何版本。如果一个库发布了完全重写的 API,你将在下一次安装时获得它,前提是缓存是干净的。仅在顶层应用中使用,且这些应用不会被其他包依赖——即使如此,也仅当可重复性真的不重要时(实际上很重要)。 v9.0.0 拥有一个完全重写的API,你将在下一次获得它。 npm install 带有干净的缓存。仅在顶层应用程序中使用,且这些应用程序不会被其他包所依赖——即便如此,也只有当你真正不关心可重现性时(实际上,你确实需要关心)才可使用。
预发布标识符
在稳定版本发布前,维护者会为版本打上预发布标签:
1.0.0-alpha.1—— 早期,不稳定,API 可能仍在变化1.0.0-beta.2—— 功能完整,仍在测试中,预期存在一些粗糙边缘1.0.0-rc.1—— 发布候选,除非最终测试中出现异常,否则应可投入生产使用
预发布版本排序 低于 稳定版本: 1.0.0-alpha.1 < 1.0.0并且关键的是, ^1.0.0 —— 预发布版本只有在你显式指定范围内才会匹配。这种行为阻止了你意外地选择一个 alpha 版本而本意是跟踪稳定版本。 不是 安装 2.0.0-beta.1 ——预发布版本只有在你明确指定其范围时才会匹配。这种行为可以防止你无意中选择一个alpha版本,而本意是跟踪稳定版本。
如果你消费的包只有预发布版本,请锁定完整的版本字符串: "some-package": "1.0.0-beta.2"。不要在预发布版本上使用 ^ 或 ~ ,除非你知道维护者会谨慎对待它们——大多数维护者都不会。
在提交前检查范围
在将版本范围锁定在 package.json之前,值得确认你实际同意安装的内容。版本范围计算器 接收一个版本范围和一组候选版本,并显示哪些版本匹配——当你不确定 是否覆盖你需要的特定版本,或在审查 PR 时发现范围有误时非常有用。 ~2.3 covers a specific version you need, or when you’re reviewing a PR and the range looks off.
三种失败模式
大多数与语义化版本相关的构建失败遵循以下三种模式之一:
^+ MAJOR 版本提升 + 删除了锁文件: 你锁定过^1.0.0,维护者发布了2.0.0,锁文件被删除或从未提交,CI 安装了 2.0.0。解决方法:提交你的锁文件。每个项目都必须如此,没有例外。*在你发布的库中: 你是库作者,使用了*作为依赖项。每个下游用户都继承了你的通配符。你让他们的依赖图变成了你的问题。解决方法:在你发布的任何内容中使用显式范围。- 没有锁文件的预发布版本: 一个宽松范围拉取了
1.0.0-alpha.3,API 从alpha.1改变,结果无法工作。解决方法:显式锁定预发布版本,并——说一遍——提交锁文件。
阅读变更日志
当你的依赖树中的任何包发布 MAJOR 版本时,请花两分钟查看变更日志。维护者编写了它,就是为了让你不必从凌晨三点的堆栈跟踪中逆向推断破坏性变更。
如果一个库在 MINOR 版本中发布破坏性变更却没有变更日志——那就是失信行为。提交问题。公开指出。但如果 MAJOR 版本明显存在,迁移指南详细,而你未查看就拉取了它——工具链正是按你告诉它的行为运行的。契约写在了三个数字里。你只是没有读它。
