Node_modules相关包管理器对比与依赖关系分析
笔记哥 /
04-01 /
4点赞 /
0评论 /
820阅读
# 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** ```csharp 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`** ```csharp { "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** ```csharp 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`** ```csharp # 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** ```csharp 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`** ```csharp 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** ```csharp 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和锁文件会发生什么?** ```csharp 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 会尽量将依赖提升到顶层,但同一包的不同版本只能提升一个,其余版本会嵌套。**
**- `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** ```csharp 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`** ```csharp { "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** ```csharp 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`** ```csharp # 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** ```csharp 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`** ```csharp 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** ```csharp 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和锁文件会发生什么?** ```csharp 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 会尽量将依赖提升到顶层,但同一包的不同版本只能提升一个,其余版本会嵌套。**
本文来自投稿,不代表本站立场,如若转载,请注明出处:http//www.knowhub.vip/share/2/1899
- 热门的技术博文分享
- 1 . ESP实现Web服务器
- 2 . 从零到一:打造高效的金仓社区 API 集成到 MCP 服务方案
- 3 . 使用C#构建一个同时问多个LLM并总结的小工具
- 4 . .NET 原生驾驭 AI 新基建实战系列Milvus ── 大规模 AI 应用的向量数据库首选
- 5 . 在Avalonia/C#中使用依赖注入过程记录
- 6 . [设计模式/Java] 设计模式之工厂方法模式
- 7 . 5. RabbitMQ 消息队列中 Exchanges(交换机) 的详细说明
- 8 . SQL 中的各种连接 JOIN 的区别总结!
- 9 . JavaScript 中防抖和节流的多种实现方式及应用场景
- 10 . SaltStack 远程命令执行中文乱码问题
- 11 . 推荐10个 DeepSeek 神级提示词,建议搜藏起来使用
- 12 . C#基础:枚举、数组、类型、函数等解析
- 13 . VMware平台的Ubuntu部署完全分布式Hadoop环境
- 14 . C# 多项目打包时如何将项目引用转为包依赖
- 15 . Chrome 135 版本开发者工具(DevTools)更新内容
- 16 . 从零创建npm依赖,只需执行一条命令
- 17 . 关于 Newtonsoft.Json 和 System.Text.Json 混用导致的的序列化不识别的问题
- 18 . 大模型微调实战之训练数据集准备的艺术与科学
- 19 . Windows快速安装MongoDB之Mongo实战
- 20 . 探索 C# 14 新功能:实用特性为编程带来便利