本节摘要:package.json 是项目的身份证兼合同,SemVer 语义化版本则是合同里关于「变化」的条款。本节逐字段拆解清单文件,把 SemVer 的三段位、范围符号、^ 与 ~ 的精确含义讲透,并用一次真实的「小版本升级炸生产」事故说明 lockfile 为什么必须进版本库。
一个服务端项目典型的 package.json:
{ "name": "order-service", "version": "2.3.1", "description": "订单中心 API 服务", "main": "app.js", "type": "commonjs", "scripts": { "start": "node app.js", "test": "jest" }, "engines": { "node": ">=20" }, "dependencies": { "express": "^4.19.2" }, "devDependencies": { "jest": "^29.7.0" } }
按重要程度划三档:
exports 字段值得多说一句:它比 main 强在能收窄可访问面——只暴露你愿意承诺的路径,内部文件外部 require 不到,等于给包加了一道封装墙。
版本号三段位 MAJOR.MINOR.PATCH,各自对应一种「变化幅度」的承诺:

两个高频误解必须点破。其一,「^ 表示自动升级安全」——它只承诺不跨大版本,不承诺没坑:minor 也可能引入行为微妙变化(尤其 0.x 区间)。其二,「package.json 写了版本为什么还会变」——清单里写的是范围,真正锁定的是 lockfile。
某个周一早上,订单服务的错误率从 0.1% 跳到 7%。回查变更:周五下午的一次紧急修复,工程师在新机器上重新执行了 install——没有使用 lockfile(当时它被 .gitignore 了)。一个下游依赖在周末发布了新的 minor 版本,其中一处时区处理的实现细节变了。范围符号允许它被装进来,没有锁文件拦住它。
修复只花了十分钟(恢复旧版本、补上锁定),但复盘出的规约值十年:
1. lockfile 必须进版本库,无例外 2. 升级依赖必须走独立分支:改范围 → install → 跑测试 → 连 lockfile 一起提交 3. 生产构建只认 lockfile(npm ci),禁止在生产机上做版本仲裁 4. 依赖变更的 PR 里,diff 必须包含 lockfile 的变化说明
发过包的人对 SemVer 的理解会深一层。我的三条实践:首版用 0.1.0 明示「接口随时会动」;API 稳定前的第一次破坏性调整就是升 1.0.0 的时机;每个 minor 都该有可追溯的变更记录,哪怕只有三行。发布流程本身只要一条命令:
npm version patch # 自动 bump 版本并打 git tag npm publish # 发布到 registry
💡 关键直觉:把 SemVer 当合同读,而不是当数字读——你声明的每一段位,都是在替使用你包的人预判升级成本。
第一次把自己写的包发出去之前,过一遍三件事:名字是否在全库唯一且符合命名空间规范(组织包加 scope 前缀可以大幅降低撞名概率);files 字段或打包忽略文件是否只包含该发布的产物——把测试夹具、内部脚本一起发出去是常见的体积事故;README 的安装与用法示例是否真的可运行——它会被展示在包页面的最显眼处,也是使用者的第一印象。发包容易收回难,unpublish 有严格的时间与依赖限制,宁可在预发布版本上多演练几轮。
如果项目走 monorepo 路线,内部包之间也要守 SemVer 的约:内部包虽然不经公网分发,但版本号乱写同样会让各子项目的锁定关系失去意义。工具链通常提供自动联动——某个内部包升版本时,依赖它的兄弟包同步刷新范围。守住这条约,内部依赖才能像外部依赖一样被 lockfile 精确治理。
版本号是接口承诺的一部分,lockfile 是承诺的公证书——两样都认真对待,依赖这头猛兽就变成了驯兽。