📚 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 工程化时代