我要成为node_modules大师!(一):包管理器选择,依赖关系分析

好家伙

1.npm曾经的一些问题

1. 依赖地狱(Dependency Hell)

  • 嵌套依赖结构:早期版本的 npm 采用嵌套的 node_modules 结构,依赖层级极深,容易导致路径过长问题(尤其在 Windows 上),甚至触发文件系统限制。

  • 版本冲突:依赖的版本管理不够严格,容易出现"同一个包多个版本"共存的情况,导致项目体积膨胀或难以调试。


2. 性能问题

  • 安装速度慢:npm 的安装算法(尤其是 v3 之前)效率较低,依赖解析和下载时间较长。

  • 全局锁问题:npm 的锁文件(package-lock.json)设计曾被诟病与其他工具(如 Yarn)不兼容,且早期版本存在锁文件冲突问题。


3. 安全性历史问题

  • 依赖链风险:npm 允许依赖包自动安装任意子依赖,曾引发多起安全事件(例如 event-stream 恶意包注入事件)。

  • 权限问题:过去 npm 的包发布机制容易被滥用,出现过"包名抢注"(squatting)或低质量包泛滥的情况。


4. 设计哲学争议

  • 集中式 registry:npm 的官方 registry 是单点故障,一旦宕机(如 2020 年的服务中断),全球开发者受影响。

  • 语义化版本(SemVer)的滥用:许多包过度依赖 ^~ 版本范围,导致不同环境安装的依赖版本不一致,可能引发意外问题。


5. 竞争对手的对比

  • Yarn 的冲击:Yarn 在 2016 年推出后,凭借离线缓存、并行安装、更稳定的锁文件等特性,直接暴露了 npm 的短板。

  • pnpm 的改进:pnpm 通过硬链接和符号链接优化存储空间和安装速度,进一步凸显了 npm 的冗余问题。

尽管如此

对于大多数普通项目,npm 已足够稳定,尤其是新版(v7+)吸收了 Yarn 和 pnpm 的优点。

2.包管理工具

|----------|---------------------|
| npm | 官方默认,兼容性无敌 |
| Yarn | 稳定可靠,锁文件严谨 |
| pnpm | 省空间、快、无依赖冲突 |
| Bun | 宇宙最快,All-in-One |

3.具体的依赖关系实例分析

现在 有两个项目,

项目1,依赖需求: a,b,c a依赖于b,c , c无依赖依赖

项目2.依赖需求: a,b,c,d a依赖于b,c , c依赖于d ,d依赖于b

这是两个典型项目,
第一个,代表直接依赖
第二个,代表嵌套依赖

现在我分别使用npm,yarn,pnpm,bun,

我们分别分析器其node_modules文件夹结构,以及package文件,和lock文件

3.1.第一个项目非常简单

安装结果对比

包管理器 node_modules 结构 锁文件格式
npm 扁平化(hoisting): - a, b, c(顶层) - a/node_modules 无嵌套(依赖已提升) package-lock.json(嵌套结构,标记依赖来源)
Yarn 类似 npm 的扁平化: - a, b, c(顶层) - 无重复依赖 yarn.lock(扁平列表,记录所有依赖的精确版本)
pnpm 隔离结构: - 顶层只有 a, b, c(符号链接) - 真实依赖存储在 ~/.pnpm-store,通过硬链接引用 pnpm-lock.yaml(内容寻址,记录依赖的存储路径)
Bun 类似 pnpm 的硬链接优化: - 扁平化但共享依赖存储 - 依赖通过硬链接复用 bun.lockb(二进制锁文件,记录依赖树和哈希)

3.2.我们重点关注第二个项目

看看各家工具如何处理嵌套依赖

包管理器 node_modules 结构 关键区别
npm 扁平化 + 部分嵌套: - a, b, c, d(顶层) - 如果 b 有多个版本,低版本会嵌套在 d/node_modules package-lock.json 会标记 db 是否嵌套
Yarn 完全扁平化: - a, b, c, d(顶层) - 若版本冲突,Yarn 会选择一个版本,可能导致问题 yarn.lock 会记录所有依赖的解析版本
pnpm 严格隔离: - a, b, c, d(顶层符号链接) - cdb 不会冲突,各自引用正确版本 pnpm-lock.yaml 会记录每个包的独立存储路径
Bun 类似 pnpm: - 共享存储 + 硬链接 - 依赖版本冲突时,Bun 会优先兼容 bun.lockb 会优化存储,避免重复

来看示例图:

(1)NPM

node_modules

复制代码
node_modules/
├── a/               # [email protected]
│   └── package.json # 依赖: b, c
├── b/               # [email protected] (被 a 和 d 依赖)
├── c/               # [email protected]
│   └── package.json # 依赖: d
├── d/               # [email protected]
│   └── package.json # 依赖: b
└── .bin/            # 可执行文件(如果有)

锁文件

package-lock.json

复制代码
{
  "name": "project2",
  "version": "1.0.0",
  "lockfileVersion": 2,
  "requires": true,
  "packages": {
    "node_modules/a": {
      "version": "1.0.0",
      "dependencies": { "b": "^1.0.0", "c": "^1.0.0" }
    },
    "node_modules/b": { "version": "1.0.0" },
    "node_modules/c": {
      "version": "1.0.0",
      "dependencies": { "d": "^1.0.0" }
    },
    "node_modules/d": {
      "version": "1.0.0",
      "dependencies": { "b": "^1.0.0" }
    }
  }
}

(2)Yarn

node_modules

复制代码
node_modules/
├── a/               # [email protected]
│   └── package.json # 依赖: b, c
├── b/               # [email protected] (提升到顶层)
├── c/               # [email protected]
│   └── package.json # 依赖: d
├── d/               # [email protected]
│   └── package.json # 依赖: b
└── .bin/

锁文件

yarn.lock

复制代码
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
[email protected]:
  version "1.0.0"
  dependencies:
    b "^1.0.0"
    c "^1.0.0"

[email protected]:
  version "1.0.0"

[email protected]:
  version "1.0.0"
  dependencies:
    d "^1.0.0"

[email protected]:
  version "1.0.0"
  dependencies:
    b "^1.0.0"

(3)pnpm

node_modules

复制代码
node_modules/
├── a -> .pnpm/[email protected]/node_modules/a      # 符号链接
├── b -> .pnpm/[email protected]/node_modules/b
├── c -> .pnpm/[email protected]/node_modules/c
├── d -> .pnpm/[email protected]/node_modules/d
└── .pnpm/
    ├── [email protected]/
    │   └── node_modules/
    │       ├── a       # a 的真实文件
    │       ├── b -> ../../[email protected]/node_modules/b  # 硬链接
    │       └── c -> ../../[email protected]/node_modules/c
    ├── [email protected]/
    │   └── node_modules/
    │       ├── c       # c 的真实文件
    │       └── d -> ../../[email protected]/node_modules/d
    ├── [email protected]/
    │   └── node_modules/
    │       ├── d       # d 的真实文件
    │       └── b -> ../../[email protected]/node_modules/b  # 硬链接
    └── [email protected]/
        └── node_modules/
            └── b       # b 的真实文件

锁文件

pnpm-lock.yaml

复制代码
lockfileVersion: 5.4
dependencies:
  a:
    specifier: 1.0.0
    version: 1.0.0
    dependencies:
      b: 1.0.0
      c: 1.0.0
  b:
    specifier: 1.0.0
    version: 1.0.0
  c:
    specifier: 1.0.0
    version: 1.0.0
    dependencies:
      d: 1.0.0
  d:
    specifier: 1.0.0
    version: 1.0.0
    dependencies:
      b: 1.0.0

(4)bun

复制代码
node_modules/
├── a/               # [email protected] (硬链接到全局存储)
├── b/               # [email protected] (硬链接)
├── c/               # [email protected]
├── d/               # [email protected]
└── .bin/

锁文件

bun.lockb
为二进制

总结,

特性 npm Yarn pnpm Bun
依赖结构 扁平化(可能嵌套冲突) 完全扁平化(可能版本冲突) 隔离 + 硬链接(无冲突) 扁平化 + 硬链接优化
安装速度 较快 最快(复用存储) 极快(内置优化)
磁盘占用 高(每个项目独立存储) 较高 极低(全局共享存储) 低(共享存储)
锁文件格式 package-lock.json(嵌套) yarn.lock(扁平列表) pnpm-lock.yaml(内容寻址) bun.lockb(二进制高效)
幻影依赖 严重(依赖提升) 存在 无(严格隔离) 较少(但比 pnpm 宽松)

4.一个问题

对于项目2提出一个新的情况

假设项目本身依赖的b包为1.0.0
d包依赖的b包版本为:2.0.0

node_modules和锁文件会发生什么?

复制代码
node_modules/
├── a/               # [email protected]
│   └── package.json # 依赖: [email protected], [email protected]
├── b/               # [email protected] (被提升到顶层)
├── c/               # [email protected]
│   └── package.json # 依赖: [email protected]
├── d/               # [email protected]
│   ├── node_modules/
│   │   └── b/       # [email protected] (嵌套)
│   └── package.json # 依赖: [email protected]
└── .bin/

由于依赖提升,所以b包版本1.0.0(先遇到)于是,被提升到顶层

npm 会尽量将依赖提升到顶层,但同一包的不同版本只能提升一个,其余版本会嵌套。