npm、yarn 和 pnpm 的 lockfile 机制
前端项目里有一种问题特别磨人:代码没变,环境一换,项目坏了。
有时变化藏在间接依赖里:它发布了新版本,重新安装时被选中。业务代码和 package.json 都没动,node_modules 却已经不一样了。
lockfile 解决的就是这个落差:package.json 只说允许哪些版本,lockfile 记录这一次实际解析出的依赖树,让团队成员、CI 和发布环境尽量装到同一批包。
固定直接依赖,也不等于固定整棵依赖树
假设项目里有这样一条依赖链。
项目依赖 A 的 0.0.1:
{
"name": "project",
"dependencies": {
"A": "0.0.1"
}
}A 自己又依赖 B:
{
"name": "A",
"dependencies": {
"B": "^1.0.0"
}
}项目可以把 A 固定到 0.0.1,但 B 的版本范围由 A 声明。这里的 ^1.0.0 允许安装 1.0.0 及以上、2.0.0 以下的普通发布版本。
今天安装时选中 B@1.0.0,过几天它发布了 1.0.1。如果没有锁定解析结果,重新安装就可能选中新版本。
注意,^ 不是一律允许更新 patch 版本。对于 ^0.0.1,0.0.2 已经不在允许范围内;node-semver 的 caret 规则以最左侧非零数字决定兼容上限。
package.json 说的是「需要什么依赖,以及允许什么版本范围」。lockfile 记的是「这一次实际解析出了哪些版本、从哪里下载、完整性校验是什么」。一个是声明,一个是结果。

yarn.lock 做了什么
Yarn 用 yarn.lock 保存解析结果。下面的锁文件格式和 yarn upgrade、--frozen-lockfile 命令都以 Yarn Classic 为例。
还是上面的例子,简化后的 yarn.lock 大概会像这样:
A@0.0.1:
version "0.0.1"
dependencies:
B "^1.0.0"
B@^1.0.0:
version "1.0.0"第二段记录了这次选择:A 需要 ^1.0.0 范围内的 B,实际选中的是 B@1.0.0。
后面即使 B 发布了 1.0.1,只要声明与锁文件仍然匹配,普通安装就会继续使用锁定版本,而不是追着新版本走。Yarn Classic 的安装说明也区分了锁文件足以满足声明和需要更新两种情况。
lockfile 最有用的地方就在这里:依赖可以更新,但更新要变成一次看得见的文件变更。
那想升级依赖怎么办
lockfile 不会把依赖永远固定住。
如果你明确要把 A 从 0.0.1 升到 0.0.2,可以通过包管理器命令更新:
yarn add A@0.0.2或者:
yarn upgrade A@0.0.2也可以手动修改 package.json 后重新安装。
A@0.0.1 不能满足新的精确版本要求,包管理器就需要为 A@0.0.2 及受影响的依赖更新解析结果。不是每次修改 package.json 都要重新选择全部依赖;已有版本仍满足声明时,可以继续复用。
升级后要一起检查 package.json 和 lockfile。前者显示改了什么声明,后者显示实际带动了哪些依赖变化。
npm 的 package-lock.json
npm 用 package-lock.json 保存解析结果。按当前 npm install 文档,锁定版本满足声明时就复用;不满足时,重新解析相应版本并更新锁文件。
CI 里通常用 npm ci。它要求已有锁文件与声明一致,不一致就报错,不会代替开发者更新;已有的 node_modules 会先被移除。如果生成锁文件时使用了影响依赖树的配置,CI 也需要使用相同配置。
pnpm 的 pnpm-lock.yaml
pnpm 的锁文件叫 pnpm-lock.yaml。
它和 yarn/npm 一样,也会记录依赖解析结果,让安装过程可复现。pnpm 还有一个差异:它的依赖存储和 node_modules 结构更严格。
pnpm 会把包内容放到全局内容寻址存储里,再通过硬链接和符号链接组织项目的 node_modules。这让它更节省磁盘,也更容易暴露那些「明明没有声明依赖,却因为扁平化 node_modules 偶然能 import 到」的问题。
在 CI 中,对 pnpm 项目通常会用:
pnpm install --frozen-lockfile这个命令会要求当前 pnpm-lock.yaml 和 package.json 保持一致。如果不一致,它会失败,而不是悄悄更新 lockfile。
平时怎么对待 lockfile
我现在基本按下面几条处理。
应用项目提交 lockfile。
如果这是一个前端应用、后端服务、网站、桌面应用,或者任何需要部署运行的项目,lockfile 都应该进 Git。否则每个人安装出来的依赖都可能不一样,CI 和线上也可能不一样。
不要把不同包管理器的 lockfile 混在一起。
如果项目用 pnpm,就保留 pnpm-lock.yaml,不要同时提交 package-lock.json 和 yarn.lock。多个 lockfile 会让后来的人不知道该信谁。
升级依赖时看 lockfile diff,不要讨厌它的 diff。
很多风险不是写在 package.json 里的。你可能只改了一个直接依赖,但 lockfile 里会显示它带动了哪些间接依赖变化。这个 diff 是依赖升级 review 的重要部分。
CI 用 frozen 或 clean install。
发布时不应顺便升级依赖。下面分别是 npm、pnpm 和 Yarn Classic 的命令,按项目使用的工具选一条:
npm ci
pnpm install --frozen-lockfile
yarn install --frozen-lockfilemonorepo 通常只保留根 lockfile。
如果整个仓库共用一个 workspace,依赖解析应该由根目录统一管理。独立安装、独立发布的子项目则另当别论,不能仅凭它们在同一个仓库里就删掉锁文件。
锁文件也不能包办整个环境。Node 和包管理器版本、安装配置、平台相关依赖及安装脚本仍会影响结果,需要和项目环境一起维护。
现在的看法
我会把 lockfile 和依赖声明放在一起审查:日常升级时确认解析变化,CI 安装时拒绝临时更新。这样依赖变动发生在一次明确的提交里,而不是某台机器重新装包的时候。