pnpm 理解

69 阅读7分钟

我们按照包管理工具的发展历史,从 npm2 开始讲起:

npm2

用 node 版本管理工具把 node 版本降到 4,那 npm 版本就是 2.x 了。

然后找个目录,执行下 npm init -y,快速创建个 package.json。

然后执行 npm install express,那么 express 包和它的依赖都会被下载下来:

图片

展开 express,它也有 node_modules:

图片

再展开几层,每个依赖都有自己的 node_modules:

图片

也就是说 npm2 的 node_modules 是嵌套的。

这样其实是有问题的,多个包之间难免会有公共的依赖,这样嵌套的话,同样的依赖会复制很多次,会占据比较大的磁盘空间。

另外还有致命问题是 windows 的文件路径最长是 260 多个字符,这样嵌套是会超过 windows 路径的长度限制的。

当时 npm 还没解决,社区就出来新的解决方案了,就是 yarn:

yarn

yarn 是怎么解决依赖重复很多次,嵌套路径过长的问题的呢?

铺平。所有的依赖不再一层层嵌套了,而是全部在同一层,这样也就没有依赖重复多次的问题了,也就没有路径过长的问题了。

我们把 node_modules 删了,用 yarn 再重新安装下,执行 yarn add express:

这时候 node_modules 就是这样了:

图片

全部铺平在了一层,展开下面的包大部分是没有二层 node_modules 的:

图片

当然也有的包还是有 node_modules 的,比如这样:

图片

`为什么还有嵌套呢?` 
`因为一个包是可能有多个版本的,提升只能提升一个,所以后面再遇到相同包的不同版本,依然还是用嵌套的方式。`

npm 后来升级到 3 之后,也是采用这种铺平的方案了,和 yarn 很类似:

图片

当然,yarn 还实现了 yarn.lock 来锁定依赖版本的功能,不过这个 npm 也实现了。

yarn 和 npm 都采用了铺平的方案,这种方案就没有问题了么?

并不是,扁平化的方案也有相应的问题。

最主要的一个问题是幽灵依赖(phantfom dependencies),也就是你明明没有声明在 dependencies 里的依赖,但在代码里却可以 require 进来。

这个也很容易理解,因为都铺平了嘛,那依赖的依赖也是可以找到的。

但是这样是有隐患的,因为没有显式依赖到packake.json中

比如项目安装了M包 ,它依赖A包,如果平铺或者嵌套的方式,我们可以做自己的代码中这样用
const A = require('A')   // `这就是幻影依赖`
如果改天M包 不依赖A包了,那么项目其它地方同事clone使用我们代码的时候,不会安装A包,
就导致提示缺少A包问题`

而且还有一个问题,就是上面提到的依赖包有多个版本的时候,只会提升一个,那其余版本的包不还是复制了很多次么,依然有浪费磁盘空间的问题。

那 pnpm 是怎么解决这俩问题的呢?

pnpm

回想下 npm3 和 yarn 为什么要做 node_modules 扁平化?不就是因为同样的依赖会复制多次,并且路径过长在 windows 下有问题么?

那如果不复制呢,比如通过 link。

首先介绍下 link,也就是软硬连接,这是操作系统提供的机制,硬连接就是同一个文件的不同引用,而软链接是新建一个文件,文件内容指向另一个路径。当然,这俩链接使用起来是差不多的。

如果不复制文件,只在全局仓库保存一份 npm 包的内容,其余的地方都 link 过去呢?

这样不会有复制多次的磁盘空间浪费,而且也不会有路径过长的问题。因为路径过长的限制本质上是不能有太深的目录层级,现在都是各个位置的目录的 link,并不是同一个目录,所以也不会有长度限制。

没错,pnpm 就是通过这种思路来实现的。

再把 node_modules 删掉,然后用 pnpm 重新装一遍,执行 pnpm install。

你会发现它打印了这样一句话:

20220927-162619.jpeg

包是从全局 store 硬连接到虚拟 store 的,这里的虚拟 store 就是 node_modules/.pnpm。

我们打开 node_modules 看一下:

图片

确实不是扁平化的了,依赖了 express,那 node_modules 下就只有 express,没有幽灵依赖。

展开 .pnpm 看一下:

图片

所有的依赖都在这里铺平了,都是从全局 store 硬连接过来的,然后包和包之间的依赖关系是通过软链接组织的。

比如 .pnpm 下的 expresss,这些都是软链接,

图片

也就是说,所有的依赖都是从全局 store 硬连接到了 node_modules/.pnpm 下,然后之间通过软链接来相互依赖。

官方给了一张原理图,配合着看一下就明白了:

图片

这就是 pnpm 的实现原理。

首先,最大的优点是节省磁盘空间,一个包全局只保存一份,剩下的都是软硬连接。

其次就是快,因为通过链接的方式而不是复制,自然会快。

这也是它所标榜的优点:

图片

相比 npm2 的优点就是不会进行同样依赖的多次复制。

相比 yarn 和 npm3+ 呢,那就是没有幽灵依赖,也不会有没有被提升的依赖依然复制多份的问题。

总结

npm2 是通过嵌套的方式管理 node_modules 的,会有同样的依赖复制多次的问题。

npm3+ 和 yarn 是通过铺平的扁平化的方式来管理 node_modules,解决了嵌套方式的部分问题,但是引入了幽灵依赖的问题,并且同名的包只会提升一个版本的,其余的版本依然会复制多次。

pnpm 则是用了另一种方式,不再是复制了,而是都从全局 store 硬连接到 node_modules/.pnpm,然后之间通过软链接来组织依赖关系。 不但节省磁盘空间,也没有幽灵依赖问题,安装速度还快

PS: 何为软连接(符号连接)与 硬连接?

要理解 pnpm 软硬链接的设计,首先要复习一下操作系统文件子系统对软硬链接的实现。

硬链接通过 ln originFilePath newFilePath 创建,如 ln ./my.txt ./hard.txt,这样创建出来的 hard.txt 文件与 my.txt(源文件) 都指向同一个文件存储地址,因此无论修改哪个文件,都因为直接修改了原始地址的内容,导致这两个文件内容同时变化。 hard.txt的指针 与 my.txt 的指针都是指向相同文件inode

进一步说,通过硬链接创建的 N 个文件都是等效的,通过 ls -li ./ 查看文件属性时,可以看到通过硬链接创建的两个文件拥有相同的 inode 索引:

ls -li ./
84976912 -rw-r--r-- 2 author staff 489 Jun 9 15:41 my.txt
84976912 -rw-r--r-- 2 author staff 489 Jun 9 15:41 hard.txt

其中第三个参数 2 表示该文件指向的存储地址(节点) 有两个硬链接引用(分别对应my.txt,hard.txt的指针)。硬链接如果要指向目录就麻烦多了,第一个问题是这样会导致文件的父目录有歧义,同时还要将所有子文件都创建硬链接,实现复杂度较高,因此 Linux 并没有提供这种能力。

软链接通过 ln -s originFilePath newFilePath 创建,可以认为是指向文件地址指针的指针,即它软连接本身拥有一个新的 inode 索引,但 ## 文件内容仅包含指向的文件路径 ##, 所以相同文件的软连接打开显示内容是相同的(都是文件inode的指针),如: 软连接相当于windows系统的快捷方式

84976913 -rw-r--r-- 2 author staff 489 Jun 9 15:41 soft.txt -> my.txt

源文件被删除时,软链接也会失效,但硬链接不会,软链接可以对文件夹生效。因此 pnpm 虽然采用了软硬结合的方式实现代码复用,但软链接本身也几乎不会占用多少额外的存储空间,硬链接模式更是零额外内存空间占用,所以对于相同的包,pnpm 额外占用的存储空间可以约等于零。