📚 npm / Yarn / pnpm:包管理演进
一句话定性
这是一部”如何安放
node_modules”的史诗。npm 从嵌套走向扁平化,顺手制造了”幽灵依赖”这个幽灵;Yarn 补上了确定性(lockfile)并尝试激进的 PnP;pnpm 用硬链接 + 内容寻址 + 符号链接嵌套,从第一性原理上同时干掉了磁盘浪费和幽灵依赖。
一、它是什么 & 出现的时代
包管理器解决两个问题:从哪里下载依赖,以及如何把依赖摆放在磁盘上(node_modules 结构)。
- npm(2010) — 随 Node.js 诞生,官方包管理器,世界上最大的软件仓库。
- Yarn(2016,Facebook) — 针对 npm 早期的速度、确定性、安全问题而生。
- pnpm(2017) — “performant npm”,用全新的磁盘结构解决根本问题。
这条线贯穿 2013-2018 SPA时代 到 2018-2023 工程化时代,是工程化”依赖管理”主线的核心。
二、为什么会出现(解决上一代什么痛点)
node_modules 的两次危机
危机一:嵌套地狱(npm v2)
早期 npm 把依赖嵌套安放:A 依赖 B,B 依赖 C,就是 A/node_modules/B/node_modules/C。结果:
- 路径深到爆:Windows 的路径长度限制直接被突破。
- 磁盘爆炸:同一个包(比如 lodash)在不同依赖下被重复安装无数份。
危机二:扁平化与幽灵依赖(npm v3+)
为解决嵌套,npm v3 改成扁平化(flat):尽量把依赖都提升到顶层 node_modules。这缓解了重复,却制造了一个新幽灵:
幽灵依赖(Phantom Dependency)
因为依赖被提升到顶层,你的代码能
import一个你从没在package.json里声明过的包(它只是恰好是某个依赖的依赖,被提升上来了)。今天能跑,哪天那个间接依赖升级、不再带它了,你的代码就突然崩溃——而你根本不知道自己依赖了它。
还有”依赖分身(doppelganger)“问题:扁平化解决不了版本冲突时,又退化成局部嵌套,导致同一个包的多个版本副本。
三、核心机制 & 为什么流行
三代演进对比
| 维度 | npm(扁平化) | Yarn(Classic) | pnpm |
|---|---|---|---|
| node_modules 结构 | 扁平,提升到顶层 | 扁平(同 npm) | 符号链接嵌套 + 全局 store |
| 磁盘占用 | 每个项目各存一份 | 同左 | 全局一份,硬链接复用 |
| lockfile | 后期才有 | 一开始就有 | 有 |
| 幽灵依赖 | 有 | 有 | 没有(只能访问声明过的) |
| 安装速度 | 慢→改善 | 当年比 npm 快很多 | 快(硬链接几乎零拷贝) |
Yarn 的贡献:
yarn.lock:锁定整棵依赖树的精确版本,保证”每个人、每次安装”结果一致(确定性)。逼得 npm 后来也加了package-lock.json。- 并行安装 + 缓存:当年速度碾压 npm。
- PnP(Plug’n’Play):更激进——干脆不要
node_modules,用一个映射表告诉 Node “每个包在缓存里的哪个位置”。彻底消灭幽灵依赖,但兼容性代价大,生态没完全跟上。
pnpm 的核心机制(为什么它对):
全局内容寻址 store(~/.pnpm-store)
│ 每个包按内容 hash 存,全机器只存一份
▼
项目 node_modules/.pnpm/ ◄── 通过【硬链接】指向全局 store(几乎不占额外磁盘)
│
▼
项目 node_modules/<pkg> ◄── 通过【符号链接】指向 .pnpm 里的真实位置
│
▼
关键:node_modules 顶层【只放 package.json 声明的包】
──► 你 import 不到没声明的包 ──► 幽灵依赖被根除
三个机制各司其职:
- 内容寻址 + 硬链接 → 解决磁盘浪费(全机器一份,项目间共享)。
- 符号链接的嵌套结构 → 既扁平又精确,每个包只能看到自己声明的依赖 → 解决幽灵依赖。
详细论证见 为什么pnpm解决了依赖问题。
四、带来的新问题 / 副作用
每代都有自己的坑
- npm 扁平化:幽灵依赖、依赖分身、
node_modules巨大(被戏称”宇宙最重物体”)。- Yarn PnP:与假设”
node_modules真实存在”的老工具不兼容,迁移阻力大。- pnpm 的符号链接:极少数假设”扁平
node_modules”的工具会不兼容(可用 hoist 配置兜底);符号链接在某些环境(部分打包/容器场景)需要注意。- 通用问题:供应链安全(恶意包、依赖投毒)、
postinstall脚本风险——这是包管理永恒的痛。
五、为什么会衰落 / 现状
现状(2026):
- npm:仍是默认、是 registry 的事实标准,但作为”安装器”在性能/严格性上落后。
- Yarn:Classic(v1)基本停更;Yarn Berry(v2+,PnP)在特定团队使用,未成主流。
- pnpm:Monorepo 和注重正确性的项目的首选,因严格(无幽灵依赖)+ 省磁盘 + 快而持续增长。
- Bun:自带的包管理器主打极致速度,成为新的搅局者。
- 趋势:正确性(严格依赖)+ 速度(Rust/Zig 实现) 成为新共识,pnpm 的设计理念基本胜出。
六、对后续技术的影响(因果链)
npm 嵌套 ──► 路径地狱 + 磁盘爆炸
│
▼
npm 扁平化(v3)──► 解决嵌套,但制造【幽灵依赖】+ 依赖分身
│
├──► Yarn:lockfile(确定性)+ 并行(速度)──► 逼 npm 跟进 package-lock
│ └──► PnP:废弃 node_modules ── 激进但兼容性受阻
│
└──► pnpm:内容寻址 + 硬链接(省磁盘)+ 符号链接嵌套(根除幽灵依赖)
│ ── 从第一性原理重新设计磁盘结构
▼
正确性 + 速度成为新共识(详见 [[为什么pnpm解决了依赖问题]])
│
├──► Monorepo 时代依赖管理的基石
└──► [[Bun]] 等以速度为卖点继续内卷
历史地位
包管理的演进是一个教科书级的”二阶效应”案例:npm 扁平化为了解决 A 问题(嵌套),引入了 B 问题(幽灵依赖)。pnpm 的伟大在于它没有打补丁,而是回到”依赖应该如何在磁盘上组织”这个第一性问题,用硬链接和符号链接的组合,同时干掉了磁盘浪费和幽灵依赖——一个设计,两个根治。
🔗 相关:前端工程化演进史 | Node.js | Bun | 为什么pnpm解决了依赖问题 | 2018-2023 工程化时代