案例:
假设在开发一个项目,package.json 中非常严谨地“写死”了直接依赖的版本:
"dependencies": {
"awesome-ui": "1.0.0"
}
第一阶段:开发环境(正常)
当第一次安装时,awesome-ui@1.0.0 内部依赖了一个处理颜色的库 tiny-color,它在 awesome-ui 的配置里是这样写的:"tiny-color": "^2.1.0"。
此时,包管理器下载了 tiny-color@2.1.0。项目运行完美,颜色显示正常。
第二阶段:线上部署(有问题)
两周后,要进行线上发布。由于没有提交 Lock 文件,服务器执行 npm install 时会重新解析依赖树:
- 识别到 awesome-ui 依然是 1.0.0(因为写死了)。
- 扫描 awesome-ui 的依赖,发现它需要 tiny-color@^2.1.0。
- 关键点: 此时 tiny-color 刚刚发布了 2.2.0 版本,这个版本虽然号称是兼容更新,但由于作者疏忽,引入了一个针对旧版浏览器的 Bug,或者更改了某个 API 的返回格式。
- 包管理器根据 ^ 规则,自动安装了最新的 tiny-color@2.2.0。
结果: 代码一行没改,直接依赖的版本号也没变,但线上版本因为间接依赖的升级直接黑屏或样式错乱。
对比:Lock 文件的“救命”作用
如果当时提交了 Lock 文件(如 package-lock.json),情况会完全不同。
1. 结构化快照
Lock 文件会像下面这样记录:
"awesome-ui": {
"version": "1.0.0",
"dependencies": {
"tiny-color": "2.1.0" // 注意:这里被强制固定在了 2.1.0
}
}
即便 tiny-color 发布了 10.0.0,服务器在安装时也会无视最新版,严格按照 Lock 文件里的记录下载 2.1.0。
2. 内容指纹(Integrity)
假设 tiny-color 的作者不是发布了新版,而是偷偷在原有的 2.1.0 版本里注入了恶意脚本并重新上传(虽然极罕见,但理论存在)。
- 只写死版本: 包管理器检测到版本号匹配,直接下载恶意代码。
- 有 Lock 文件: Lock 文件记录了原始代码的 Sha512 哈希值。安装时包管理器会发现下载的文件哈希对不上,立即报错并停止安装,保护了系统安全。
总结
| 变更点 | 只写死版本号 | 有 Lock 文件 |
|---|---|---|
| 项目依赖 A | 保持 1.0.0 | 保持 1.0.0 |
| A 的依赖 B | 被自动升级到 2.2.0 (可能不兼容) | 被锁定在 2.1.0 (安全) |
| 依赖安全性 | 无法验证文件内容是否被篡改 | 通过 Hash 校验确保文件未变 |
“写死版本号”只能保证package.json的直接依赖没变,但不能保证其间接依赖不变。而 Lock 文件会把所有依赖的版本号都确定下来,保证所有的包的版本都是不变的。