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 会标记 d 的 b 是否嵌套 |
Yarn | 完全扁平化: - a , b , c , d (顶层)- 若版本冲突,Yarn 会选择一个版本,可能导致问题 |
yarn.lock 会记录所有依赖的解析版本 |
pnpm | 严格隔离: - a , b , c , d (顶层符号链接)- c 和 d 的 b 不会冲突,各自引用正确版本 |
pnpm-lock.yaml 会记录每个包的独立存储路径 |
Bun | 类似 pnpm: - 共享存储 + 硬链接 - 依赖版本冲突时,Bun 会优先兼容 |
bun.lockb 会优化存储,避免重复 |
来看示例图:
(1)NPM
node_modules
node_modules/ ├── a/ # a@1.0.0 │ └── package.json # 依赖: b, c ├── b/ # b@1.0.0 (被 a 和 d 依赖) ├── c/ # c@1.0.0 │ └── package.json # 依赖: d ├── d/ # d@1.0.0 │ └── 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/ # a@1.0.0 │ └── package.json # 依赖: b, c ├── b/ # b@1.0.0 (提升到顶层) ├── c/ # c@1.0.0 │ └── package.json # 依赖: d ├── d/ # d@1.0.0 │ └── package.json # 依赖: b └── .bin/
锁文件
yarn.lock
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. a@1.0.0: version "1.0.0" dependencies: b "^1.0.0" c "^1.0.0" b@1.0.0: version "1.0.0" c@1.0.0: version "1.0.0" dependencies: d "^1.0.0" d@1.0.0: version "1.0.0" dependencies: b "^1.0.0"
(3)pnpm
node_modules
node_modules/ ├── a -> .pnpm/a@1.0.0/node_modules/a # 符号链接 ├── b -> .pnpm/b@1.0.0/node_modules/b ├── c -> .pnpm/c@1.0.0/node_modules/c ├── d -> .pnpm/d@1.0.0/node_modules/d └── .pnpm/ ├── a@1.0.0/ │ └── node_modules/ │ ├── a # a 的真实文件 │ ├── b -> ../../b@1.0.0/node_modules/b # 硬链接 │ └── c -> ../../c@1.0.0/node_modules/c ├── c@1.0.0/ │ └── node_modules/ │ ├── c # c 的真实文件 │ └── d -> ../../d@1.0.0/node_modules/d ├── d@1.0.0/ │ └── node_modules/ │ ├── d # d 的真实文件 │ └── b -> ../../b@1.0.0/node_modules/b # 硬链接 └── b@1.0.0/ └── 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/ # a@1.0.0 (硬链接到全局存储) ├── b/ # b@1.0.0 (硬链接) ├── c/ # c@1.0.0 ├── d/ # d@1.0.0 └── .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/ # a@1.0.0 │ └── package.json # 依赖: b@1.0.0, c@1.0.0 ├── b/ # b@1.0.0 (被提升到顶层) ├── c/ # c@1.0.0 │ └── package.json # 依赖: d@1.0.0 ├── d/ # d@1.0.0 │ ├── node_modules/ │ │ └── b/ # b@2.0.0 (嵌套) │ └── package.json # 依赖: b@2.0.0 └── .bin/
由于依赖提升,所以b包版本1.0.0(先遇到)于是,被提升到顶层
npm 会尽量将依赖提升到顶层,但同一包的不同版本只能提升一个,其余版本会嵌套。