深入浅出PNPM
前言
pnpm
是一款当代备受关注的 新兴(问题较多) 包管理工具,使用过的同学们都会被它极快的安装速度、极少的磁盘存储空间所吸引。
首先,为什么会出现pnpm
?作者一开始对yarn
的发布有很高的期待,但是发布后并没有满足作者的一些期待,反而让作者有些失望。
After a few days, I realized that Yarn is just a small improvement over npm. Although it makes installations faster and it has some nice new features, it uses the same flat node_modules structure that npm does (since version 3). And flattened dependency trees come with a bunch of issues 几天后,我意识到 Yarn 只是对 npm 的一个小小的改进。尽管它使安装速度更快,并且具有一些不错的新功能,但它使用与npm相同的平面node_modules结构(自版本 3 起)。
扁平化的依赖树带来了一系列问题
(具体后面会讲)
至于为什么叫pnpm
?是因为pnpm
作者对现有的包管理工具,尤其是npm
和yarn
的性能特别失望,所以起名叫做performance npm
,即pnpm
(高性能npm)。那么它的性能高在哪里,对比npm
和yarn
又有哪些优势呢?下面我们先来聊聊npm
和yarn
存在的一些问题。
NPM
在 npm@3 之前,node_modules结构是干净
、可预测
的,因为node_modules 中的每个依赖项都有自己的node_modules文件夹,在package.json中指定了所有依赖项。例如下面所示,项目依赖了foo
,foo
又依赖了bar
,依赖关系如下图所示:
node_modules
└─ foo
├─ index.js
├─ package.json
└─ node_modules
└─ bar
├─ index.js
└─ package.json
复制代码
上面结构有两个严重的问题:
- package中经常创建太深的依赖树,这会导致 Windows 上的目录路径过长问题
- 当一个package在不同的依赖项中需要时,它会被多次复制粘贴并生成多份文件
为了解决这些问题,npm@3之后的版本采取依赖平铺的策略,把所有的依赖(包括依赖的依赖)平铺到node_modules文件夹下,像这样:
node_modules
├─ foo
| ├─ index.js
| └─ package.json
└─ bar
├─ index.js
└─ package.json
复制代码
可以看到,依赖平铺机制下,bar
被提升到了顶层。如果同一个包的多个版本在项目中被依赖时,node_modules结构又是怎么样的?
例如:一个项目App
直接依赖了A(version: 1.0)
和C(version: 1.0)
,A
和C
都依赖了不同版本的B
,其中A
依赖B 1.0
,C
依赖B 2.0
,可以通过下图清晰的看到npm2
和npm3+
结构差异:
包B 1.0
被提升到了顶层,这里需要注意的是,多个版本的包只能有一个
被提升上来,其余版本的包会嵌套安装到各自的依赖当中(类似npm2
的结构)。
至于哪个版本的包被提升,依赖于包的安装顺序,谁最后安装就提升谁 !
依赖变更会影响提升的版本号,比如变更后,有可能是B 1.0
,也有可能是 B 2.0
被提升上来(但只能有一个版本提升)
细心的小伙伴可能发现,这其实并没有解决之前的问题,反而又引入了许多新的问题。
npm3+和yarn存在的问题
幽灵依赖(Phantom dependencies )
Phantom dependencies 被称之为幽灵依赖或幻影依赖,解释起来很简单,即某个包没有在package.json
被依赖,但是用户却能够引用到这个包。
引发这个现象的原因一般是因为 node_modules 结构所导致的。例如使用 npm或yarn 对项目安装依赖,依赖里面有个依赖叫做 foo
,foo
这个依赖同时依赖了 bar
,yarn 会对安装的 node_modules 做一个扁平化结构的处理,会把依赖在 node_modules 下打平,这样相当于 foo
和 bar
出现在同一层级下面。那么根据 nodejs 的寻径原理,用户能 require 到 foo
,同样也能 require 到 bar
。
nodejs的寻址方式:(查看更多)
- 对于核心模块(core module) => 绝对路径 寻址
- node标准库 => 相对路径寻址
- 第三方库(通过npm安装)到node_modules下的库(可以在node环境中输入
module.paths
查看): 3.1. 先在当前路径下,寻找 currentProject/node_modules/xxx 3.2 递归从下往上,到上级路径寻找,例如 ../node_modules/xxx 3.3 循环步骤3.2 3.4 在全局环境路径下寻找,例如 .node_modules/xxx 3.5 在用户目录下寻找,例如 users/金虹桥程序员/.node_modules/xxx 或者 users/金虹桥程序员/node_libraries/xxx 3.6 node安装目录下查找,例如 nodejs/lib/node/.node_modules/xxx
NPM分身(NPM doppelgangers )
这个问题其实也可以说是 提升
导致的,这个问题可能会导致有大量的依赖的被重复安装.
举个例子:项目中有packageA
、packageB
、packageC
、packageD
。packageA
依赖packageX 1.0和packageY 1.0,packageB
依赖packageX 2.0和packageY 2.0,packageC
依赖packageX 1.0和packageY 2.0,packageD
依赖packageX 2.0和packageY 1.0。
在npm2时,结构如下
- package A
- packageX 1.0
- packageY 1.0
- package B
- packageX 2.0
- packageY 2.0
- package C
- packageX 1.0
- packageY 2.0
- package D
- packageX 2.0
- packageY 1.0
复制代码
在npm3+**和**yarn中,由于存在提升
机制,所以X和Y各有一个版本被提升了上来,目录结构如下
- package X => 1.0版本
- package Y => 1.0版本
- package A
- package B
- packageX 2.0
- packageY 2.0
- package C
- packageY 2.0
- package D
- packageX 2.0
复制代码
如上图所示的packageX 2.0和packageY 2.0被重复安装多次,从而造成 npm 和 yarn 的性能一些性能损失。
这种场景在monorepo 多包场景下尤其明显,这也是yarn workspace
经常被吐槽的点,另外扁平化的算法实现也相当复杂,改动成本很高。 那么pnpm
是如何解决这种问题的呢?
网状 + 平铺的node_modules结构
回想下 npm3 和 yarn 为什么要做 node_modules 扁平化?不就是因为同样的依赖会复制多次,并且路径过长在 windows 下有问题么?
那如果不复制呢,比如通过 link,接下来先介绍下 link,也就是软硬连接,这是操作系统提供的机制。
硬链接(hard link)
硬链接可以理解为同一个文件的不同引用,怎么说呢?就是我们通过文件路径a/b/c去访问存储在磁盘上的某个文件时,操作系统会通过一系列的寻址操作读取到这个文件内容,然而操作系统允许我们通过不同的路径去访问。
举个例子,我们的文件存储在磁盘的A扇区,我们可以通过建立多个硬链接的方式来访问,a/b/c这个路径能访问到,d/e/f这个路径也能访问到,这两个路径就成为文件的硬链接。
软连接(symbolic link)
软链接则是新建一个文件,文件内容指向另一个路径,类似操作系统上的快捷方式和咱们js中对象的存储方式。
可以理解为文件中只是存储另外一个文件的路径,通过这个去访问文件内容。
pnpm是如何利用软硬链接的
回到上文,为了解决依赖安装多次&文件路径过长的问题,pnpm的解决思路是不复制文件,只在全局仓库保存一份 npm 包的内容,其余的地方都 link 过去
这样不会有复制多次的磁盘空间浪费,而且也不会有路径过长的问题。因为路径过长的限制本质上是不能有太深的目录层级,现在都是各个位置的目录的 link,并不是同一个目录,所以也不会有长度限制。
再把 node_modules 删掉,然后用 pnpm 重新装一遍,执行 pnpm install。
你会发现它打印了这样一句话:
包是从全局 store 硬连接到虚拟 store 的,这里的虚拟 store 就是 node_modules/.pnpm。
我们打开 node_modules 看一下:
确实不是扁平化的了,依赖了 express,那 node_modules 下就只有 express,没有幽灵依赖。
展开 .pnpm 看一下:
所有的依赖都在这里铺平了,都是从全局 store 硬连接过来的,然后包和包之间的引用关系是通过软链接组织的。
比如 .pnpm 下的 expresss,这些都是软链接,
也就是说,所有的依赖都是从全局 store 硬连接到了 node_modules/.pnpm 下,然后之间通过软链接来相互引用。
原理图解
如果还是看的不太清晰,下面结合官方的一张原理图,配合着看一下:
这就是 pnpm 的实现原理。
总结
那么回过头来看一下,pnpm 为什么优秀呢?
首先,最大的优点是节省磁盘空间呀,一个包全局只保存一份,剩下的都是软硬连接,这得节省多少磁盘空间呀。
其次就是快,因为通过链接的方式而不是复制,自然会快。
这也是它所标榜的优点:
相比 npm2 的优点就是不会进行同样依赖的多次复制。
相比 yarn 和 npm3+ 呢,那就是没有幽灵依赖,也不会有没有被提升的依赖依然复制多份的问题。
这就已经足够优秀了。