[pnpm] pnpm 与 npm/yarn 的对比
JavaScript 应用程序通常依赖于许多外部库,这些依赖项通常通过包管理器来管理。默认情况下,Node.js 使用 NPM 作为包管理器。
由于早期的 NPM 存在各种不足,社区后来开发了 Yarn 和 pnpm 作为替代品。
如果要使用 Yarn 和 pnpm,则需要先通过 NPM 进行安装。
早期 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 npm/yarn/... 1. 依赖解析。 仓库中没有的依赖都被识别并获取到仓库。
2. 目录结构计算。node_modules
目录结构是根据依赖计算出来的。
3. 链接依赖项。 所有以前安装过的依赖项都会直接从仓库中获取并链接到node_modules
。1. 解析所有依赖。
2. 获取所有依赖。
3. 将所有依赖写入node_modules
。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结构
文件结构示例:comparing-node-modules/pnpm5-example at master · zkochan/comparing-node-modules (github.com)
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中文网