node api 学习2:pnpm的原理,软硬连接

0 阅读6分钟

pnpm 的实现机制,相比 yarn 和 npm 可以说是降维打击。

那具体好在哪里呢? 我们一起来看一下。

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

npm2

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

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

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

image.png

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

这很正常呀?有什么不对么?

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

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

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

yarn

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

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

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

这时候 node_modules 就是这样了:

image.png

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

image.png

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

image.png

为什么还有嵌套呢?

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

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

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

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

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

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

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

但是这样是有隐患的,因为没有显式依赖,万一有一天别的包不依赖这个包了,那你的代码也就不能跑了,因为你依赖这个包,但是现在不会被安装了。

这就是幽灵依赖的问题。

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

那社区有没有解决这俩问题的思路呢?

当然有,这不是 pnpm 就出来了嘛。

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

pnpm

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

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

那先弄懂什么是软硬连接。

要理解软硬链接,必须先搞懂 Linux/macOS 文件系统的核心结构(Windows 也有类似逻辑):

  • 磁盘上真正存数据的是 inode(索引节点) :每个文件对应唯一的 inode,里面存了文件的元数据(大小、权限、磁盘块位置等)和实际数据。
  • 我们平时看到的「文件名」,只是指向 inode 的「目录项(dentry)」,相当于一个「别名」。
  • 一个 inode 可以被多个目录项指向,这就是硬链接的本质

所以,硬链接是同一个 inode 的多个不同文件名,和源文件完全等价,没有主次之分。

  • 你可以理解为:同一个文件有了多个名字,不管删哪个名字,只要还有一个名字存在,文件数据就不会丢。
  • 所有硬链接地位完全平等,没有源和链接的区别,所以你无法区分源文件和链接
  • 命令:ln 源文件 硬链接名

执行ls -i就能看到文件的inode:

image.png

左侧的数字就是inode。

image.png

软链接(Symbolic Link / Symlink):文件的快捷方式

软链接是一个独立的新文件,它的 inode 里存的是源文件的路径,相当于 Windows 里的快捷方式。

命令:ln -s 源文件 软链接名

image.png

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

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

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

这个是是一个使用pnpm安装的项目依赖:

image.png

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

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

展开 .pnpm 看一下: image.png

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

那我们就来分析下express这个包是怎么链接的。

首先,在项目的node_modules中,执行ls -al

image.png

可以看到是一个软连接,连接到了.pnpm/express@5.2.1/node_modules/express,我们进入到这个目录看下:

image.png

可以看到.pnpm/express@5.2.1/node_modules/express这里就是真实express的文件,express的package.json中的dependencies又依赖了很多其他的包,这些包存在express@5.2.1/node_modules下,但是他们都是软连接,软连接到那里呢?

accepts为例:

image.png

可以看到它也是软连接,然后软连接到.pnpm/accepts@2.0.0/node_modules/accepts,这里面存在真实的文件。

我们从官网的一张图就能看清楚:

image.png

项目依赖了bar,这个bar依赖了foo这个依赖,所以,现在.pnpm把这两个包从全局store硬链接过来,可以看图中的红色ln线。

项目依赖的bar的真实文件存在于bar@1.0.0/node_modules/bar下,bar这个包依赖的foo在node_modules存在一个软连接,这个软连接有链接到.pnpm/foo@1.0.0上。

一句话:所有的依赖包从全局的store硬链接到.pnpm目录下,然后包和包之间的依赖关系就通过软链接来进行引用。

这就是 pnpm 的实现原理。

那么回过头来看一下,pnpm 为什么优秀呢?

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

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

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

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

这就已经足够优秀了,对 yarn 和 npm 可以说是降维打击。