Skip to content
本页目录

深入浅出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作者对现有的包管理工具,尤其是npmyarn的性能特别失望,所以起名叫做performance npm,即pnpm(高性能npm)。那么它的性能高在哪里,对比npmyarn又有哪些优势呢?下面我们先来聊聊npmyarn存在的一些问题。

NPM

npm@3 之前,node_modules结构是干净可预测的,因为node_modules 中的每个依赖项都有自己的node_modules文件夹,在package.json中指定了所有依赖项。例如下面所示,项目依赖了foofoo又依赖了bar,依赖关系如下图所示:

go
node_modules
└─ foo
   ├─ index.js
   ├─ package.json
   └─ node_modules
      └─ bar
         ├─ index.js
         └─ package.json
复制代码

上面结构有两个严重的问题:

  • package中经常创建太深的依赖树,这会导致 Windows 上的目录路径过长问题
  • 当一个package在不同的依赖项中需要时,它会被多次复制粘贴并生成多份文件

为了解决这些问题,npm@3之后的版本采取依赖平铺的策略,把所有的依赖(包括依赖的依赖)平铺到node_modules文件夹下,像这样:

go
node_modules
├─ foo
|  ├─ index.js
|  └─ package.json
└─ bar
   ├─ index.js
   └─ package.json
复制代码

可以看到,依赖平铺机制下,bar被提升到了顶层。如果同一个包的多个版本在项目中被依赖时,node_modules结构又是怎么样的?

例如:一个项目App直接依赖了A(version: 1.0)C(version: 1.0)AC都依赖了不同版本的B,其中A依赖B 1.0C依赖B 2.0,可以通过下图清晰的看到npm2npm3+结构差异:

image.png

B 1.0被提升到了顶层,这里需要注意的是,多个版本的包只能有一个被提升上来,其余版本的包会嵌套安装到各自的依赖当中(类似npm2的结构)。

image.png

至于哪个版本的包被提升,依赖于包的安装顺序,谁最后安装就提升谁 !

依赖变更会影响提升的版本号,比如变更后,有可能是B 1.0 ,也有可能是 B 2.0被提升上来(但只能有一个版本提升)

细心的小伙伴可能发现,这其实并没有解决之前的问题,反而又引入了许多新的问题。

npm3+和yarn存在的问题

幽灵依赖(Phantom dependencies )

Phantom dependencies 被称之为幽灵依赖幻影依赖,解释起来很简单,即某个包没有在package.json 被依赖,但是用户却能够引用到这个包。

引发这个现象的原因一般是因为 node_modules 结构所导致的。例如使用 npm或yarn 对项目安装依赖,依赖里面有个依赖叫做 foofoo 这个依赖同时依赖了 bar,yarn 会对安装的 node_modules 做一个扁平化结构的处理,会把依赖在 node_modules 下打平,这样相当于 foobar 出现在同一层级下面。那么根据 nodejs 的寻径原理,用户能 require 到 foo,同样也能 require 到 bar

nodejs的寻址方式:(查看更多)

  1. 对于核心模块(core module) => 绝对路径 寻址
  2. node标准库 => 相对路径寻址
  3. 第三方库(通过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 )

这个问题其实也可以说是 提升 导致的,这个问题可能会导致有大量的依赖的被重复安装.

举个例子:项目中有packageApackageBpackageCpackageDpackageA依赖packageX 1.0和packageY 1.0packageB依赖packageX 2.0和packageY 2.0packageC依赖packageX 1.0和packageY 2.0packageD依赖packageX 2.0和packageY 1.0

npm2时,结构如下

markdown
- 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各有一个版本被提升了上来,目录结构如下

markdown
- 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,也就是软硬连接,这是操作系统提供的机制。

硬链接可以理解为同一个文件的不同引用,怎么说呢?就是我们通过文件路径a/b/c去访问存储在磁盘上的某个文件时,操作系统会通过一系列的寻址操作读取到这个文件内容,然而操作系统允许我们通过不同的路径去访问。

举个例子,我们的文件存储在磁盘的A扇区,我们可以通过建立多个硬链接的方式来访问,a/b/c这个路径能访问到,d/e/f这个路径也能访问到,这两个路径就成为文件的硬链接。

软链接则是新建一个文件,文件内容指向另一个路径,类似操作系统上的快捷方式和咱们js中对象的存储方式。

可以理解为文件中只是存储另外一个文件的路径,通过这个去访问文件内容

pnpm是如何利用软硬链接的

回到上文,为了解决依赖安装多次&文件路径过长的问题,pnpm的解决思路是不复制文件,只在全局仓库保存一份 npm 包的内容,其余的地方都 link 过去

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

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

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

img

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

我们打开 node_modules 看一下:

img

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

展开 .pnpm 看一下:

img

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

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

img

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

原理图解

如果还是看的不太清晰,下面结合官方的一张原理图,配合着看一下:

img

这就是 pnpm 的实现原理。

总结

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

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

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

这也是它所标榜的优点:

img

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

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

这就已经足够优秀了。

Released under the MIT License.