01|这篇真正要先立住的,不是“semver 不靠谱”,而是 npm 生态为什么必须先相信它

经历过 left-pad 之后,

大家很容易顺着往下得出一个更悲观的结论:

  • 依赖链太深
  • 维护者不一定稳定
  • 平台也会改规则
  • 开放生态本来就不可靠

可如果真是这样,

现代 npm 世界根本不可能正常运转这么久。

因为只要一个项目里开始有很多依赖,

你迟早会碰到另一个更硬的问题:

这些依赖更新时,到底哪些可以放心跟,哪些必须当作危险变更来处理?

这就是 semver 出场的地方。

很多人今天说起 semver

第一反应通常只是:

  • major
  • minor
  • patch

可如果只把它理解成版本号格式,

还是会把这条线写浅。

因为 semver 在 npm 世界里真正承担的,

从来不只是编号作用。

它承担的是:

开放生态里关于“兼容性应该怎样被沟通”的基本承诺。

也就是说,

semver 之所以重要,

不是因为三个数字很优雅。

而是因为 npm 世界在大规模复用、自动安装和深依赖同时成立之后,

必须要有一种办法,

把“这次更新大概会不会把你搞炸”压缩成一个足够便于机器和人一起处理的信号。

所以第四篇真正的起点不是:

“为什么 semver 老出问题?”

而是先要问:

为什么 npm 生态根本离不开一种像 semver 这样的兼容承诺。


02|因为 semver 最早要解决的,就是开放生态最古老也最致命的噩梦:dependency hell

semver.org 在规范开头写得很直白:

软件管理里有个著名噩梦,叫 dependency hell

而且它把这场噩梦拆成了两种典型形态:

  • 依赖卡得太死,升级不了,叫 version lock
  • 依赖放得太松,以为将来都兼容,结果被打脸,叫 version promiscuity

这段话非常关键。

因为它点出了开放生态最难处理的那个矛盾:

你既不想每次更新都重新手工验证全世界,也不想因为怕出事就永远钉死版本。

换句话说,

开放生态如果没有某种“兼容承诺”,

它会很快陷进两种极端:

第一种极端是大家都钉死版本,不敢动。

第二种极端是大家都放很宽,结果动一次炸一次。

semver 提供的,正是一种看起来很合理的中间道路:

  • patch 是兼容性修复
  • minor 是兼容性新增
  • major 才代表破坏性变化

这套设计为什么会如此有吸引力?

因为它几乎像是在说:

你不需要每次都重新理解全部变更,只要理解版本号的语义,就能对更新风险做基本判断。

对于一个依赖动辄成百上千的生态来说,

这简直像氧气一样必要。

所以别忘了,

semver 最早并不是大家无聊发明出来的版本礼仪。

它是一种试图让开放依赖网络继续可动的治理工具。


03|npm 为什么会特别依赖 semver?因为 npm 不只是要装包,它还要自动替你判断“哪些未来版本理论上可以接”

这一步特别重要。

因为很多语言生态也会有版本号。

可 npm 世界对 semver 的依赖程度格外高,

是有现实原因的。

原因就在于:

npm 不只是记录版本,它还让你在 package.json 里直接声明一个“未来可接受的版本范围”。

比如:

  • 精确版本
  • ~
  • ^
  • x

这些写法表面看是语法。

本质上都是在表达一种判断:

我相信某一段未来版本,大概率仍然兼容。

一旦这种范围声明成立,

npm 的世界就会变得极其高效。

因为你不需要每次等依赖作者和下游使用者人工逐个谈判。

机器可以直接据此解析、拉取、安装。

也就是说,

npm 真正把 semver 推到核心位置,

不是因为它单纯喜欢规范。

而是因为 npm 想要的那个世界本来就高度自动化:

  • 依赖自动解析
  • 安装自动完成
  • 版本范围自动匹配

而只要自动化想跑起来,

你就必须先给机器一套关于兼容性的压缩语言。

semver 恰好提供了这套语言。

所以第四篇必须记住这一点:

npm 生态对 semver 的依赖,本质上来自它对“自动化兼容判断”的依赖。


04|可真正的问题也恰恰出在这里:兼容从来不是纯数字事实,而是一种带有解释空间的社会承诺

这就是 semver 一直反复失灵的根子。

因为规范本身当然写得很清楚:

  • 不兼容改动升 major
  • 向后兼容新增升 minor
  • 向后兼容修复升 patch

可现实里最难的地方恰恰不是规则句子写不清。

而是:

什么算“向后兼容”,很多时候并不是一个毫无争议的纯技术事实。

比如这些情况就很常见:

  • 维护者觉得只是修 bug,用户觉得你改掉了他赖以生存的旧行为
  • 作者觉得只是内部重构,使用者却依赖了你没写进文档的副作用
  • 发布者觉得 minor 很安全,结果某个边角行为恰好把下游构建打爆

这说明什么?

说明 semver 表面像数学,

本质上更像一种社会契约。

它要求:

  • 维护者正确理解自己的 public API
  • 使用者别依赖未承诺的隐式行为
  • 双方对“breaking”这件事有足够接近的判断

可开放生态偏偏最难保证的,

就是所有人对这些边界的理解都一致。

所以 semver 最大的问题不是逻辑错误。

而是它把一个高度依赖上下文和共同理解的现实,

压缩成了三个数字。

这在大多数时候很好用。

可一到边界情况,

就会立刻露出裂缝。


05|更麻烦的是,npm 世界还天然放大了这种裂缝:因为大多数人依赖的不是“你写给自己看的 API”,而是“别人无意间也可能碰到的所有行为”

这一步特别关键。

很多人讨论 semver

总爱把问题说得过于理性:

“只要声明 public API,再严格遵守,就没问题。”

可开放生态的现实远没有这么干净。

因为下游项目真正依赖的,

往往不只是你文档里郑重写出来的那几条接口。

它还可能依赖:

  • 一种历史行为
  • 一个边角输出格式
  • 一个错误信息
  • 一个默认值
  • 一个内部实现偶然带来的副作用

在你看来,

这些也许都不算正式 API。

可在下游看来,

它们可能已经是系统正常运行的一部分。

这就是为什么现实里经常会出现一种很熟悉的冲突:

维护者说:

“这不是 breaking change。”

使用者说:

“可我的东西就是被你搞坏了。”

也就是说,

semver 失灵最常见的方式,

不是有人故意撒谎。

而是:

开放生态里真正被依赖的行为边界,常常远大于维护者自己以为正在承诺的边界。

而只要这件事成立,

版本号就迟早会跟真实兼容面发生偏差。


06|所以 npm 生态后来一边越来越依赖 semver,一边又越来越长出 lockfile、npm ci 这类“别只信 semver”的补丁制度

这点特别有意思。

因为它说明整个行业其实并没有真的彻底相信 semver

如果大家真的完全信它,

那理论上只要写好范围就够了。

可现实不是这样。

现实是大家后来又不断补出一整套额外机制:

  • package-lock.json
  • npm shrinkwrap
  • 更强调可复现安装
  • CI 里尽量用固定解析结果

这些机制存在本身就说明了一件事:

semver 提供的是方向感,不是最终确定性。

它可以告诉你“理论上哪些更新应该安全”,

但它不能保证:

  • 上游一定严格遵守
  • 你的项目刚好不踩边角
  • 今天解析出来的依赖树和下次完全一样

所以 npm 世界后来的真实治理思路其实越来越像:

发布层面继续讲 semver,安装层面再靠 lockfile 把现实钉住。

这本身就说明,

行业已经逐渐接受了一个事实:

单靠 major / minor / patch

并不能真正兜住开放生态的兼容性焦虑。


07|这也是为什么很多人后来会说:semver 并没有错,它只是被拿去承担了一个远比它能力边界更重的任务

这一层很重要,

因为它能避免把第四篇写成简单的“semver 无用论”。

更准确的说法其实是:

semver 解决的是“如何表达兼容性意图”,但很多人后来想让它直接等于“现实兼容性保证”。

这两者差别非常大。

表达意图这件事,

它做得不错。

毕竟没有它,

版本号会更混乱,

依赖范围会更不可控。

可要它单独负责整个生态的升级安全,

就太重了。

因为升级安全还取决于太多东西:

  • 测试覆盖
  • 变更说明
  • 发布节奏
  • 下游使用方式
  • 是否有锁文件
  • 是否有 CI 验证

也就是说,

semver 更像一种必要但不充分的秩序。

它能提供“最低限度的共同语言”,

却不能替代:

真正的验证、治理与谨慎。

所以第四篇要立住的,不是“semver 一无是处”。

而是:

开放生态后来对 semver 失望,很大程度上是因为大家最初把太多本应由测试、锁定、发布纪律共同承担的责任,全都压给了版本号。


08|为什么 semver 明明是规则,却总在开放生态里失灵

把前面几层叠一起看,第四篇最重要的判断可以压成一句:

semver 之所以会反复失灵,不是因为它没有规则,而是因为开放生态真正需要压缩的“兼容”本来就不是纯规则能够单独定义的东西。

这句话里有几层意思。

第一,npm 生态必须先有 semver。

没有这种兼容承诺,自动解析依赖范围几乎没法安全运行。

第二,semver 规范本身并不荒唐。

它对 dependency hell 的诊断非常准确,也确实提供了比纯随意版本号更强的秩序。

第三,真正的裂缝来自现实世界。

维护者承诺的边界、用户依赖的边界、工具解析的边界,常常并不完全重合。

第四,这就是为什么开放生态后来必须靠 lockfile、CI、固定安装、谨慎升级等制度继续补位。

所以理解 npm 的第四步,

最不该只记住:

“有人不遵守 semver。”

更该记住的是:

开放生态把兼容性问题放大到了一个版本号语义永远只能部分兜底、无法完全兜底的程度。


09|semver 能压缩规则,却压不平兼容的社会性

npm 江湖 的第四篇,最值得记住的,不是“semver 也会骗人”这种浅层抱怨。

更值得记住的是:

它原本是开放生态对抗 dependency hell 的秩序尝试,可当整个 npm 世界越长越大、越长越深时,大家最终发现:兼容性既是技术事实,也是社会解释,而版本号最多只能压缩后一种复杂性的很小一部分。

也正因此,

后来真正改变 npm 生态秩序的,

不再只是“怎么给版本号命名”。

而是另一类更具现实感的问题:

  • 安装能不能更稳定
  • 磁盘模型能不能更合理
  • 锁定机制能不能更强
  • workspace 和 monorepo 现实能不能更顺

这也就是第五篇要接上的地方。

因为当 npm 赢下中央地位之后,

人们接下来最强烈的不满,

已经不再只是“版本号不可信”。

而是:

整个安装和依赖管理现实,能不能被重新组织。

这也正是 Yarnpnpm 会长出来的背景。


编者注(事实核对):文中关于 dependency hellversion lockversion promiscuity 的表述,主要依据 semver.org 官方规范 Semantic Versioning 2.0.0,其导言部分即以这三者来说明 semver 试图解决的问题,并明确要求使用 semver 的软件声明 public API。关于 npm 如何解释 semver,主要依据 About semantic versioning | npm Docspackage.json | npm Docs,其中明确说明 major / minor / patch 的推荐含义,以及 ^~ 等范围语法如何用于表达依赖可接受的未来版本。关于 npm 生态后来并未只靠 semver,而是进一步依赖锁文件与更可复现安装策略来补位,主要依据 npm 对 package-lock.jsonnpm versionpackage.json 与版本范围解析的文档说明。正文将这些材料综合概括为“兼容性是社会承诺,不只是数字语义”,属于对 semver 规范目标、npm 自动解析机制与开放生态实践之间张力的综合判断。


关键人物速览

  • Tom Preston-WernerSemantic Versioning 规范的原始作者。理解 semver 最初到底想解决什么,绕不开他。
  • Isaac Z. Schlueternode-semver 的核心作者之一,也是 npm 世界里版本范围语义的重要实践者。理解 semver 为什么会在 npm 世界里被推到如此核心的位置,绕不开他。
  • Laurie Voss:npm 早期核心推动者之一。理解 npm 生态为什么长期把 semver 当成默认兼容秩序的一部分,绕不开他。

参考与延伸阅读

  1. Semantic Versioning 2.0.0
    https://semver.org/

  2. About semantic versioning | npm Docs
    https://docs.npmjs.org/about-semantic-versioning

  3. package.json | npm Docs
    https://docs.npmjs.com/cli/v11/configuring-npm/package-json/

  4. semver | npm Docs
    https://docs.npmjs.com/cli/v6/using-npm/semver/

  5. Updating your published package version number | npm Docs
    https://docs.npmjs.com/updating-your-published-package-version-number


下篇预告:Yarn / pnpm 为什么会长出来,因为当 npm 已经成为中央入口之后,大家接下来最受不了的,已经不是有没有入口,而是这个入口的安装现实、锁定能力和依赖组织方式太不理想。