前言
在现代化的开发中,一个人可能同时开发多个项目,安装的项目越来越多,所随之安装的依赖包也越来越臃肿,而且有时候所安装的速度也很慢,甚至会安装失败。
因此我们就需要去了解一下,我们的包管理器,在前端比较主流的包管理器主要有三个(当然还有其他优秀的包管理器,本文主要介绍这三个),分别是:npm,yarn,pnpm
幽灵嵌套(Phantom Dependency)
在了解包管理器之前,我们先了解一下包管理的一个难题:幽灵嵌套
幽灵嵌套问题通常发生在依赖之间存在复杂的版本要求时,比如:
-
包
A
依赖于包B@1.0.0
-
包
B
依赖于包C@2.0.0
-
另一个包
D
也依赖于C@3.0.0
在传统的依赖管理中,可能会导致包 C
的不同版本被嵌套在不同的子依赖树中,从而在 node_modules
中形成不同路径的多层嵌套,导致路径非常深。这种情况一旦发生,如果包 A
和包 D
不同意版本要求,可能会导致不同版本的包 C
被分别安装在不同的路径下,出现路径冲突甚至依赖问题,这就是“幽灵嵌套”现象。
NPM (Node Package Manager)
概述:
npm
是 Node.js 默认的包管理工具,最早由 Node 社区开发并捆绑到 Node.js 中,因此使用最为广泛。
从npm v2 到npm v7+的升级过程中,从最初使用递归的方法处理依赖,造成高度嵌套的依赖树,到后来使用扁平化管理,一定程度上解决了依赖嵌套问题,但是出现了算法时间过长的问题,最后引入了package-lock.json机制,作用是锁定依赖结构,一定程度上保持了依赖的稳定性
核心:
-
采用扁平化依赖管理管理方式
-
每个依赖包都会在 node_modules 中单独安装
-
相同的依赖可能会被重复安装多次
特点:
-
优点:
-
Node.js 默认包管理器,使用最广泛,拥有强大的额社区支持
-
最早的包管理器,简单易上手,对初学者友好
-
package-lock.json 保证依赖版本一致性
-
-
缺点:
-
安装速度较慢,占用空间大
-
目录结构:
-
npm v2及以前
-
依赖树可能非常深
-
相同包会重复安装
-
占用大量磁盘空间
-
文件路径可能超过 Windows 限制
-
node_modules
├── A
│ └── node_modules
│ └── B
│ └── node_modules
│ └── C
└── D
└── node_modules
└── B
└── node_modules
└── C
-
npm v3-6,扁平化管理
-
采用扁平化优先的安装策略
-
相同版本的包会被提升到顶层
-
不同版本保留在各自的 node_modules 中
-
安装算法比较复杂,需要计算依赖树
-
// 假设依赖关系:
// package-A 依赖 lodash@4.0.0
// package-B 依赖 lodash@4.0.0
node_modules
├── package-A
├── package-B
└── lodash // 被提升到顶层
当有版本冲突时:
// package-A 依赖 lodash@4.0.0
// package-B 依赖 lodash@3.0.0
node_modules
├── package-A
├── package-B
│ └── node_modules
│ └── lodash // 3.0.0 版本
└── lodash // 4.0.0 版本提升到顶层
-
npm v7+, 改进了扁平化管理,引入peer dependencies 处理
-
自动安装 peer dependencies
-
更严格的版本锁定
-
改进了依赖解析算法
-
workspaces 支持
-
node_modules
├── package-A
├── package-B
├── lodash // 主版本
└── .package-lock.json // 更严格的版本锁定
Yarn
概述:
Yarn
是一个 JavaScript 包管理工具,最早由 Facebook 推出,主要用于管理项目中的依赖包。和 npm
类似,yarn
解决了在 JavaScript 项目中下载、安装和管理依赖的需求,并在一定程度上改进了 npm
的一些缺点,比如性能、稳定性和安全性。
核心:
-
并行下载提升安装速度:
传统的
npm
安装方式是依次下载依赖,而Yarn
可以同时下载多个依赖,称为“并行下载”。这种方式充分利用了网络带宽,显著减少安装依赖所需的时间,使得安装速度更快。并行下载尤其在大型项目中效果显著,能够有效降低整体安装时间。 -
缓存机制减少重复下载:
Yarn
内置了缓存机制,在首次安装依赖时会将其缓存到本地。之后再次安装这些依赖时,如果依赖版本没有改变,Yarn
会直接从本地缓存中读取,而不是重新下载。这样不仅节省了网络请求,还提升了安装速度,特别适合离线开发和持续集成场景。 -
yarn.lock
确保依赖版本一致性:Yarn
使用yarn.lock
文件记录每个依赖的具体版本和来源,确保团队所有成员在不同机器上安装时得到的依赖版本完全一致。这避免了“依赖地狱”问题,即由于版本不一致导致的错误或不兼容情况,从而提高了开发过程的稳定性。 -
更安全的依赖解析机制:
Yarn
在安装依赖时会校验每个包的完整性(如 SHA 校验),以确保包的内容没有被篡改。这种安全机制能在下载依赖时检测到潜在的包篡改或恶意代码的引入,增强了项目的安全性。相比于早期的npm
,Yarn
的这种依赖解析机制更加严谨。 -
Workspace 支持更好的 monorepo 管理:
Yarn
支持Workspace
功能,允许在单个代码库(monorepo)中管理多个项目或包。这种管理方式可以将多个子项目的依赖集中管理、共享,减少重复依赖的安装。此外,Yarn
还能通过Workspace
在多个包之间建立相互依赖关系,使 monorepo 项目的开发、构建和测试更加高效。 -
PnP(Plug'n'Play)模式提供更快的模块加载:
Yarn
2.x 引入了 PnP 模式,这种模式完全去除了node_modules
目录,通过在.pnp.cjs
文件中记录依赖映射关系。PnP 不仅减少了磁盘空间的占用,还提升了依赖的加载速度,因为 Node.js 不再需要递归遍历node_modules
。这样可以加快应用的启动速度,同时在依赖数量庞大的项目中减少文件系统的压力。
特点:
-
优点:
-
采用扁平化优先 + 符号链接(符号链接是一个特殊的文件,它包含对另一个文件或目录的引用路径)的组合策略
-
相同版本的包会被提升并复用
-
不同版本通过符号链接保持正确的引用关系
-
-
缺点:
-
仍然存在幽灵依赖问题: 尽管
Yarn
已经在扁平化和依赖管理上做了优化,但在一些复杂的项目中仍然会出现幽灵依赖问题。所谓幽灵依赖,指的是某个包在项目中使用但并未在package.json
中声明,可能是通过其他依赖的间接依赖引入。这种隐式依赖会导致项目依赖关系难以维护,如果间接依赖被移除,可能会导致项目出错。 -
某些场景下的依赖解析较慢:
Yarn
的依赖解析虽然比传统npm
更快,但在依赖结构复杂、依赖版本冲突较多的情况下,解析和处理依赖关系可能会变慢。尤其在 monorepo 中,Yarn
需要处理多个包之间的依赖关系,可能出现解析速度不如pnpm
的情况。
-
目录结构:
-
基本结构
node_modules/
├── package-A/ # 实际文件
├── package-B/ # 实际文件
├── lodash/ # 提升到顶层的共享包
└── .bin/ # 可执行文件的符号链接
-
依赖共享案例
// 假设有以下依赖关系:
项目
├── package-A (依赖 lodash@4.0.0)
└── package-B (依赖 lodash@4.0.0)
// Yarn 会创建这样的结构:
node_modules/
├── package-A/
│ └── node_modules/
│ └── lodash -> ../../../lodash # 符号链接
├── package-B/
│ └── node_modules/
│ └── lodash -> ../../../lodash # 符号链接
└── lodash/ # 实际文件
-
版本冲突处理
// 当存在版本冲突时:
项目
├── package-A (依赖 lodash@4.0.0)
└── package-B (依赖 lodash@3.0.0)
// Yarn 会这样处理:
node_modules/
├── package-A/
│ └── node_modules/
│ └── lodash -> ../../../lodash # 指向4.0.0
├── package-B/
│ └── node_modules/
│ └── lodash/ # 本地安装3.0.0
└── lodash/ # 4.0.0版本在顶层
PNPM(Performant NPM)
概述:
-
pnpm
是一个更现代化的包管理工具,旨在解决npm
和yarn
的一些效率和资源管理问题。
核心:
-
采用内容寻址存储系统:
pnpm
使用内容寻址(content-addressable storage)来存储依赖包。每个依赖包都会被哈希处理,并根据其内容生成唯一的存储地址。这样,即使多个项目依赖于相同版本的包,pnpm
也只需要存储一份,不会重复存储同样内容的文件。 -
使用硬链接和符号链接共享依赖:
pnpm
通过在node_modules
中创建硬链接或符号链接(symlink),指向内容寻址存储中实际的依赖包。这样每个项目可以“共享”依赖,而不必为每个项目单独存储依赖包内容。-
硬链接(Hard Link) :将文件内容链接到项目文件夹下,不占用额外磁盘空间。
-
符号链接(Symlink) :为特定版本的包创建路径映射,使项目代码能够准确找到每个依赖包版本的地址。
-
特点:
-
优点:
-
显著节省磁盘空间
-
安装速度快
-
更严格的依赖管理
-
pnpm-lock.yaml 确保依赖版本一致
-
-
缺点:
-
不兼容一些使用传统
node_modules
结构的工具和插件:在
pnpm
中,每个依赖都有自己的隔离路径,某些工具、插件或构建系统可能会假设node_modules
目录是扁平的,这可能导致兼容性问题。 -
与本地开发和测试环境的潜在不兼容:
有些项目依赖于本地
node_modules
结构,或者需要直接访问node_modules
中的文件。在pnpm
使用内容寻址和符号链接时,这可能会导致某些工具无法正常运行。
-
目录结构
-
内容寻址存储
.pnpm-store/
└── v3/
└── files/
├── 00/ # 前两位哈希值作为目录名
│ └── deadbeef... # 包内容的哈希值
└── ff/
└── cafebabe... # 另一个包的哈希值
-
依赖结构
node_modules/
├── .pnpm/
│ ├── react@17.0.2/
│ │ └── node_modules/
│ │ ├── react/ # 实际文件(硬链接到 store)
│ │ └── loose-envify/ # react 的依赖
│ └── lodash@4.17.21/
│ └── node_modules/
│ └── lodash/ # 实际文件(硬链接到 store)
├── react -> .pnpm/react@17.0.2/node_modules/react # 符号链接
└── lodash -> .pnpm/lodash@4.17.21/node_modules/lodash # 符号链接
目录说明
-
.pnpm/
文件夹:存放项目的所有依赖包,按包名@版本号
命名,并在其node_modules
文件夹中包含该包的实际文件和依赖项。 -
硬链接:
.pnpm
中的实际文件并不是直接复制到每个项目中,而是通过硬链接指向pnpm
的全局缓存存储目录 (pnpm store
)。这样,不同项目间的相同版本依赖不需要重复下载。 -
符号链接:
pnpm
会在项目的node_modules
根目录创建符号链接,将每个包链接到.pnpm
中实际的包路径。例如:-
node_modules/react
是一个符号链接,指向.pnpm/react@17.0.2/node_modules/react
-
node_modules/lodash
符号链接指向.pnpm/lodash@4.17.21/node_modules/lodash
-
工作原理
-
包安装:
pnpm
会将依赖包下载到全局缓存 (pnpm store
) 中,并将实际文件硬链接到.pnpm
文件夹中的特定版本目录下。 -
创建符号链接:在项目的
node_modules
文件夹内创建符号链接,将包名称指向.pnpm
中的对应路径。 -
引用:项目中的
require('react')
会自动找到node_modules/react
符号链接,并通过符号链接访问实际文件。
总结:
三者同异:
特性 | npm | yarn | pnpm |
---|---|---|---|
依赖管理方式 | 扁平化管理,嵌套依赖树,可能重复安装 | 扁平化管理和符号链接,同版本只安装一次 | 基于硬链接和符号链接的内容寻址存储 |
安装速度 | 最慢 | 中等(并行安装) | 最快(得益于硬链接复用) |
磁盘空间占用 | 最大 | 中等 | 最小 |
依赖管理严格性 | 低(可能存在幽灵依赖) | 中等 | 高(严格的依赖树结构) |
锁文件格式 | package-lock.json | yarn.lock | pnpm-lock.yaml |
缓存机制 | 基础缓存 | 高效缓存 | 基于内容寻址的全局存储 |
并行安装能力 | 不支持 (npm5-) / 支持 (npm5+) | 支持 | 支持 |
依赖提升策略 | 部分提升 | 全量提升 | 不提升(严格按照依赖声明) |
workspace 支持 | 有限支持 | 完整支持 | 完整支持 |
使用选择:
基于这些特点:
-
如果项目体积较小,团队成员 Node.js 经验不同,推荐使用 npm
-
如果需要更好的性能和可靠性,推荐使用 yarn
-
如果需要最严格的依赖管理、最小的磁盘空间占用,推荐使用 pnpm
常用命令:
npm
、yarn
和 pnpm
的常用命令对比表:
操作 | npm | yarn | pnpm |
---|---|---|---|
初始化项目 | npm init | yarn init | pnpm init |
(-y为自动确认默认选项) | npm init -y | yarn init -y | pnpm init -y |
安装依赖 | npm install | yarn | pnpm install |
安装单个依赖 | npm install <pkg> | yarn add <pkg> | pnpm add <pkg> |
安装特定版本 | npm install <pkg>@<ver> | yarn add <pkg>@<ver> | pnpm add <pkg>@<ver> |
全局安装依赖 | npm install -g <pkg> | yarn global add <pkg> | pnpm add -g <pkg> |
安装开发依赖 | npm install <pkg> -D | yarn add <pkg> -D | pnpm add <pkg> -D |
更新依赖 | npm update <pkg> | yarn upgrade <pkg> | pnpm update <pkg> |
卸载依赖 | npm uninstall <pkg> | yarn remove <pkg> | pnpm remove <pkg> |
查看已安装依赖 | npm list | yarn list | pnpm list |
执行脚本 | npm run <script> | yarn <script> | pnpm run <script> |
安装指定注册源 | npm install --registry <url> | yarn add <pkg> --registry <url> | pnpm add <pkg> --registry <url> |
清理缓存 | npm cache clean --force | yarn cache clean | pnpm store prune |
创建锁定文件 | package-lock.json | yarn.lock | pnpm-lock.yaml |
列出全局包 | npm list -g --depth=0 | yarn global list | pnpm list -g --depth=0 |