pnpm 对比 npm 的改变
简介
pnpm 相比 npm 提高了安装速度、降低了存储空间,这得益于其在路径结构和存储方式上所做的改进。
pnpm 解决的问题:
/.pnpm
路径的存在解决了“幽灵依赖”问题(或者可以称为隐形依赖,也就是未在package.json
中声明,但代码中引用了某个依赖包;一开始看到这个词还以为是指项目已经不再依赖某个包了,但包还在硬盘的全局仓库中保存着,毕竟依赖包实际并不是保存在项目路径下了,在项目层面删除依赖并不会影响全局的依赖包文件)- “顶层打平+嵌套依赖的依赖” 的结构
- 解决了“依赖分身”问题:多个依赖包继续依赖某个依赖的不同版本,导致部分版本需要重复安装
- 同时这种结构又避免了早期的包管理器对“依赖的依赖”全部嵌套带来的文件夹路径过长问题。比如:项目 app 依赖 moduleA,而 moduelA 依赖 moduleB,moduelB 又依赖 moduelC,这就已经存在 3 层依赖了,从文件夹来看就是 6 层嵌套,算上 app 本身的路径就更长了
依赖结构
pnpm 在顶层与 npm 新版类似,打平了所有的依赖包;但具体到各个依赖的目录中,还是会嵌套保存各自的依赖包关系。
同时, pmpm 将顶层的依赖项多嵌套了一层 /.pnpm
,也就是所有依赖实际的是在 /node_modules/.pmpm
路径下,这样就避免了“幽灵依赖”的问题。
链接与统一存储
npm 都是根据项目维度将所有依赖保存在项目路径下,不仅不同项目对于同一个版本的依赖会重复安装,甚至一个项目下的不同依赖间,继续存在相同依赖时也会重复安装,导致大量空间的浪费。
而 pnpm 是将所有依赖同一保存在某个位置,也就是全局安装。项目只是引用了这个全局位置中的实际依赖包,而不是直接复制一份,因此有效降低了重复的依赖。
软链接(符号链接)与硬链接
软链接和硬链接是来自于 Linux 系统的概念,是两种特殊的文件。
软链接可以看做是文件的快捷方式,与源文件是两个独立的文件;而硬链接在系统层面上其实是一个文件。这是因为软链接保存的是源文件的地址,而硬链接保存的就是文件在系统中的编号(inode,通过这个 inode 编号可以获取硬盘上的实际数据)。因此,通过硬链接可以直接获取源文件的内容,而通过软链接只能获取到源文件,还需要通过源文件进一步获取其编号才能得到真实内容。
打个不恰当的比方:有个人找朋友借钱,朋友会从卡里取钱然后借给他;但这个人不能直接从朋友的卡里拿钱。前者相当于软链接,后者相当于硬链接。硬链接享有数据的控制权,而软链接不行。
层级结构示例
以下示例取自官网:
node_modules
├── foo -> ./.pnpm/foo@1.0.0/node_modules/foo
└── .pnpm
├── bar@1.0.0
│ └── node_modules
│ ├── bar ->
│ └── qar -> ../../qar@2.0.0/node_modules/qar
├── foo@1.0.0
│ └── node_modules
│ ├── foo ->
│ ├── bar -> ../../bar@1.0.0/node_modules/bar
│ └── qar -> ../../qar@2.0.0/node_modules/qar
└── qar@2.0.0
└── node_modules
└── qar ->
分析:
/node_moduels
是根路径不变- 根路径下,项目的直接依赖(
package.json
中声明的依赖项)会各自生成一个软链接文件,指向/.pnpm
路径下的对应硬链接文件 - 根路径下,
/.pnpm
目录下,是项目中所有直接依赖以及依赖的间接依赖- 直接依赖对应的路径是
/.pnpm/<name>@<version>/node_modules/<name>
(其中 name 表示一个依赖包的名字),此文件为硬链接,指向 pnpm 实际在硬盘中的存储地址 - 间接依赖也是在各依赖的
/.pnpm/<name>@<version>/node_modules/
路径下,但这些文件都是软链接,引用了 /.pnpm 路径下的实际硬链接文件
- 直接依赖对应的路径是
- 根路径下,项目的直接依赖(
总结:除了各 /.pnpm/<name>@<version>/node_modules/<name>
文件是硬链接外,其他依赖文件都是软链接。如果把项目中的依赖文件跟全局依赖比作前后端,则硬链接相对于 API ,它们可以直接与全局依赖包文件进行链接;而软链接相当于在各个模块中调用了 API ,通过 API 间接获取全局的实际依赖包文件。
至于为什么要保留硬链接而不是全部使用软链接,似乎与 Node.js 的路径解析模块有关,但具体的原因还没看懂,只知道软链接在 Node.js 中可能会先找到依赖的实际存储路径,此时若没有使用 --preserve-symlinks
,Node.js 解析模块路径时会从实际路径出发找依赖项,则根据依赖的相对路径解析就可能失败。
以下是作者对该问题的讨论:https://github.com/nodejs/node-eps/issues/46