npm中有一个package-lock.json
的文件,即npm依赖锁文件,用来描述npm依赖生成的确切树,这样不管你的依赖有何种更新,都会按照这个确切树来安装使用。
- 不同的包管理工具对应不同的锁文件:
- npm => package-lock.json
- yarn => yarn.lock
- pnpm => pnpm-lock.yaml
整体上大同小异,本文主要以npm为例。
一、semver 版本控制规范
在软件管理的领域里存在着被称作“依赖地狱”的死亡之谷,系统规模越大,加入的包越多,你就越有可能在未来的某一天发现自己已深陷绝望之中。
为了解决依赖包的版本混乱问题,制定了这个 semver 规范。
1. 版本格式
- 主版本号.次版本号.修订号
- major.minor.patch
- X.Y.Z
2. 版本号递增规则
- 主版本号 major:当你做了不兼容的 API 修改,(不兼容更新)
- 次版本号 minor:当你做了向下兼容的功能性新增,(功能新增)
- 修订号 patch:当你做了向下兼容的问题修正。(bug修复)
(更详细内容参考官网文档:https://semver.org/lang/zh-CN/)
二、安装依赖时的版本规则
npm依赖规则中,还有 >、>=、<、<=、x、*、-、||、~、^ 等符号。通过在版本号前面加上这些符号,可以有效的限制依赖的版本。
1. 版本规则
- version:必须依赖某个具体的版本。如:2.5.2,表示必须安装2.5.2版本。
- ^version:向上兼容某个版本。
- 从版本号最左侧开始,到首个非零数字,这些位置固定,其余位置任意。如果缺少某个位置,则这个位置可以任意。
- npm i 安装新包时的默认方式
"^2.5.2" // 表示 >=2.5.2 且 <3.0.0
"^0.1.3" // 表示 >=0.1.3 且 <0.2.0,主版本为0的比较特殊
"^0.0" // 表示 >=0.0.0 且 <0.1.0
- ~version:大概匹配某个版本。
- 如果次版本号(Y)指定了,那么次版本号(Y)不变,而修订号(Z)任意。
- 如果次版本号(Y)和修订号(Z)未指定,那么次版本号(Y)和修订号(Z)任意。
"~2.5.2" // 表示 >=2.5.2 且 <2.6.0
"~2.5" // 表示 >=2.5.0 且 <2.6.0
"~2" // 表示 >=2.0.0 且 <3.0.0
- >version:必须大于某个版本。
- >=version:大于或等于某个版本。
- <version:必须小于某个版本。
- <=version:小于或等于某个版本。
- version1 - version2:大于等于version1,小于等于version2。
- …
(一般项目里前三种比较常用,后面几种在npm包里可能会用到)
2. 存在的问题
使用npm安装依赖时在 package.json 里生成的默认是 ^version 形式,安装时会安装匹配的最新依赖。
- 这意味着,在多人协作时,如果一个人安装了某个版本,另一个人在安装时这个依赖更新了此版本号,就会安装另一个版本,不同的版本会有差异。
- 另外在本地和服务器构建时可能也会有类似的差异。
三、依赖锁定
如果所有的node包都严格符合语义化版本(semver)管理的规则,那么npm的最优版本号就能保证所下载的依赖包一定是与代码兼容的。
由于无法保证这一前提,如果想要保证他人下载的依赖包与我们的代码绝对兼容,就需要锁定项目中依赖包的版本号。
1. 锁死版本号
最简单的方法,就是指定具体版本号,可以将package.json中版本号开头的 ^ 和 ~ 等标记去掉。
后续安装新的依赖包时,则使用 npm i --save-exact <package_name> 或者 npm i --save <package_name>@1.2.3(指定依赖的具体版本),这样package.json中就不会出现最优版本的标记。
- 缺陷:无法锁定次级依赖的版本号,即依赖包的依赖包。
2. oversides
oversides 字段允许开发者指定依赖的具体版本,当安装依赖时,npm会以指定的版本为准进行安装。这主要用于解决嵌套依赖中的版本冲突问题,确保项目中的所有依赖都使用一致的版本。
"oversides": {
"package-c": "^1.2.0"
}
- 常见场景:
- 子依赖版本冲突:多个包依赖于不同版本的同一子包时,可通过 overrides 统一锁定版本。
- 控制版本兼容性:强制指定某些子依赖的版本,以避免运行时错误。
- 修复安全漏洞:上游库未及时更新依赖库,而你需要修复子依赖中的漏洞。
- yarn 中有类似的解法,字段名为 resolutions
- 需要 npm v8.3.0(node 16)及以上版本支持。
3. 锁文件 package-lock.json
由于在重新安装依赖时,依赖树模块的版本存在着不确定性,为了解决这个问题,npm提供了 package-lock.json 文件,被称为锁文件。
当项目中已存在package-lock.json文件,再安装项目依赖时,将以该文件为主进行解析安装指定版本的依赖包,而不是使用package.json来解析和安装。
因为package-lock.json为每个模块及其每个依赖项都指定了版本、位置和完整性哈希,所以它每次创建的安装都是相同的。

3.1 锁文件的作用
- 锁定版本:
- package-lock.json 文件锁定了所有直接和间接依赖的版本号,不论是 major、minor 还是 patch 版本。这意味着即使 package.json 中使用了版本范围符号(如 ^、~),package-lock.json 仍然会记录下当前安装的确切版本。
- 加速安装:
- 通过记录依赖关系和版本信息,npm 可以在后续安装中跳过解析步骤,从而加快安装速度。
- 一致性:
- 确保在不同机器上执行 npm install 时安装的依赖完全一致,不会因为发布了新的不兼容版本而导致项目无法正常工作。
3.2 锁文件的结构
- 常见字段:
- dependencies:包含所有直接和间接依赖的详细信息,包括版本号、来源等。
- version:每个包的版本号。
- resolved:包的下载地址。
- integrity:用于验证包完整性的哈希值。
- dev:标识该依赖是否为开发环境依赖。
3.3 锁文件的版本
package-lock.json 文件的格式和内容会随着 npm 版本的变化而有所不同。
lockfileVersion 字段定义了锁文件的版本。


3.4 锁文件的最佳实践
- 始终提交锁文件到版本控制系统
- 提交 package-lock.json 可以确保开发环境和生产环境安装的依赖版本一致。
- 避免手动编辑锁文件
- 锁文件是自动生成的,手动编辑可能导致安装时出错。应通过修改 package.json 然后运行 npm install 来更新锁文件。
- 使用相同的 npm(node)版本
- 不同的 npm(node)版本可能生成的锁文件会有细微差异,特别是锁文件格式(如 lockfileVersion)。
- 团队成员可以统一使用某一版本的 npm(node),例如通过 .nvmrc 文件指定 node.js 版本,从而确保一致性。
- 生产环境使用 npm ci
- 在生产环境中部署项目时,建议使用 npm ci 替代 npm install。
- npm ci 会完全按照 package-lock.json 中的版本安装依赖,而忽略 package.json 中的版本范围。
四、要不要锁
一直以来这都是个有争议的问题。
1. 建议
- 当项目的维护可能陷入停滞或者很少更新时,通过锁文件来锁住依赖,保证项目即使过了很长时间依然能稳定跑起来。
- 当项目和依赖有专门的维护团队来长期维护时,不锁依赖,一般这种依赖的版本更新会比较规范,有问题也能及时发现和修复。
- 如果是个别依赖的维护不稳定,这部分依赖可以通过package.json写死版本号或oversides来锁定。
最终还是需要综合考量,利弊都有,没有绝对的说法。