[pnpm] pnpm 与 npm/yarn 的对比
[pnpm] pnpm 与 npm/yarn 的对比
JavaScript应用程序通常依赖于许多外部库,这些依赖项通常通过包管理器来管理。默认情况下,Node.js使用NPM作为包管理器。由于早期的NPM存在各种不足,社区后来开发了Yarn和pnpm作为替代品。本文将详细对比分析这三种主流的JavaScript包管理器的特点和优劣。
早期NPM的不足
在NPM 3.0之前,NPM使用了嵌套依赖树的结构。这意味着如果一个项目的多个依赖项需要同一个包的不同版本,NPM会在每个依赖项的目录中重复安装该包。这种结构会导致node_modules
目录非常深,特别是在Windows系统中,这可能导致路径长度限制的问题。
每次安装包时都会重新从头开始解决依赖关系,并逐个下载和安装包。即使是已经安装过的包,也可能会再次下载,而没有利用缓存机制。这种重复安装的策略会导致安装十分缓慢。
在早期版本的NPM中,没有类似yarn.lock
或package-lock.json
这样的锁文件。这意味着即使package.json
中指定了版本范围(例如^1.0.0
这种表示可以接受一个范围的版本),依赖关系的解析和安装仍然是动态的,可能会因为时间或网络状态的不同而导致不同的版本被安装。
NPM在3.0版本引入了扁平化依赖树,以解决早期版本中嵌套依赖树带来的问题,但是扁平化依赖带来了新的问题:
- 依赖冲突:扁平化依赖树的设计将所有依赖项都安装在项目的根
node_modules
目录中,这意味着多个包可能会共享同一个依赖项的版本。如果不同的包需要不同版本的相同依赖项,就可能会发生冲突。 - 幽灵依赖:依赖的依赖被平铺在根
node_modules
目录中,这意味着即使应用的package.json
中没有声明的依赖,也可以被引入并使用。这种现象会导致依赖关系和依赖版本的不明确。
图中的虚线就代表*幽灵依赖,也叫隐式依赖。依赖E原本是B的依赖,但是被扁平化后提升到node_modules顶层。这个E没有被显式地在package.json中声明,但是结合node.js的模块解析机制可知这个依赖是可以被Project引入的。这种意料之外的依赖关系会使得项目难以维护。*
Yarn
Yarn的提出是为了解决NPM的不足,它具有以下特点:
- 确定性安装:Yarn引入了
yarn.lock
锁文件,明确了依赖的版本。 - 更快更小:Yarn通过并行下载以及引入缓存机制来加快安装速度,并且由于缓存的存在,在离线状态下也可以安装已缓存过的依赖。
- 扁平化依赖结构:减少了路径深度,提高了依赖解析的速度。解决了依赖冲突问题:Yarn会通过将不同版本的依赖项放在各自子目录的
node_modules
中来解决冲突,而不是强制将所有依赖都安装在顶层。
- 可以通过配置workspaces支持monorepo。
不足:
- 没有解决幽灵依赖的问题;
- workspaces配置较繁琐。
pnpm
pnpm的特点:
- 节省磁盘空间:npm和Yarn会在每个项目的
node_modules
目录中为所有依赖项存储完整的文件副本。如果有多个项目依赖相同的包,那么这些包会被重复存储。pnpm使用中心化的store统一存储安装的包,项目内的依赖通过链接指向store中的依赖。如果有多个项目依赖相同的包,都指向store中单一的包。 - 安装速度更快:pnpm的中心化store可以更大程度地复用依赖包,使得安装依赖这一步骤更快完成。
- 支持monorepo,配置比起yarn来说相对简单,并且得益于pnpm的特性,安装依赖很快。
- 非扁平化的node_modules:上文说到yarn和npm为了解决路径过长、依赖管理复杂等问题,将依赖进行扁平化管理。但是也带来了幽灵依赖等新问题。pnpm的创新点在于提出了基于符号链接的非扁平化node_modules结构,解决了幽灵依赖问题。
硬链接和软链接:在Linux操作系统中,每一个文件对应一个inode(索引节点)。链接是一种在共享文件和访问它的用户的若干目录项之间建立联系的一种方法。
- 硬链接是文件的别名,和源文件指向同一个inode。即硬链接和源文件是同一个文件。
- 软连接也叫符号链接,是一种特殊的文件类型,其中包含对另一个文件的引用。软链接可以看作是对一个文件的间接指针,类似于Windows操作系统下的快捷方式。即软链接和源文件是不同文件。
在Windows中也有软硬链接的概念,在cmd中通过mklink
指令创建链接:
- 硬链接:
mklink /H link_name target_file
- 软链接
mklink link_name target_file
pnpm的node_modules结构:pnpm将实际的依赖文件都安装到全局store中,在项目中的node_modules
文件夹内通过创建链接来使用store中的依赖。与yarn和npm直接将所有依赖平铺在node_modules
中的做法不同,pnpm在node_modules
中创建了一个.pnpm
文件夹,再将所有依赖都平铺在这个文件夹中。这样node.js的模块解析算法就无法引入非顶层依赖了,故解决了幽灵依赖问题。
.pnpm中的依赖通过软链接建立依赖之间的父子关系,并通过硬链接指向实际存在于全局store中的依赖包。在package.json
中显式声明的依赖会通过软链接提升到node_modules
文件夹下,因此node.js可以正常解析package.json
中声明的依赖。
在.pnpm
中,依赖通过.pnpm/<name>@<version>/node_modules/<name>
的形式进行记录,可以看到同一个包的不同版本会被分开记录。
如上图,项目中只有express这一个依赖,而express有许多子依赖,这里只列举了qs这一个依赖。可以观察到,这种基于链接的node_modules结构实现了:
- 项目的node_modules只能解析到package.json中显式声明的依赖,解决了幽灵依赖问题;
- 所有依赖都被平铺在.pnpm文件夹中,不会导致过长的文件路径;
- 实际的依赖被安装在全局的store中,项目中仅通过硬链接进行关联,节省了磁盘空间;
- 观察到express和它的依赖同属于一个文件夹层级(图中蓝色区域),express所有的依赖都软链至了node_modules/.pnpm/中的对应目录。把express的依赖放置在同一级别避免了循环的软链。
现在的NPM
yarn和pnpm属于社区产物,NPM作为官方的包管理器,一直在吸收社区好物的优点。现在的NPM也有了锁文件来明确依赖的版本,并且也通过使用缓存、改进依赖解析算法等手段加速了安装。NPM在7.0版本之后也支持配置monorepo了,可以在package.json
中直接配置,但是只支持一些简单的功能。yarn则提供了插件系统。
总结
特点 | NPM | Yarn | pnpm |
---|---|---|---|
安装速度 | 较慢 | 较快 | 大部分情况下比Yarn快 |
依赖管理 | 直接安装到node_modules | 通过缓存加速安装 | 中心化store,依赖通过符号链接安装 |
磁盘空间使用 | 高 | 中等 | 最低,通过去重和链接机制 |
依赖冲突处理 | 容易出现冲突 | 通过锁文件和解析依赖减少冲突 | 严格隔离各依赖版本,减少冲突 |
锁文件 | package-lock.json | yarn.lock | pnpm-lock.yaml |
幽灵依赖问题 | 可能发生 | 可能发生 | 严格依赖树,避免幽灵依赖 |
monorepo支持 | 基础支持 | 功能丰富,包含插件系统 | 高效的工作空间管理,模块共享更优化 |
安装一致性 | 可能由于缓存和平台差异而不一致 | 高,一致性较好 | 更高,通过全局硬链接机制确保一致性 |
性能对比图像来自pnpm官方文档:Benchmarks of JavaScript Package Managers | pnpm中文文档 | pnpm中文网