likes
comments
collection
share

为什么说 pnpm 才是前端工程化项目的未来?

作者站长头像
站长
· 阅读数 15

相信小伙伴们都接触过npm/yarn,这两种包管理工具想必是大家工作中用的最多的包管理工具,npm 作为 node 官方的包管理工具,它是随着 node 的诞生一起出现在大家的视野中,而 yarn 的出现则是为了解决 npm 带来的诸多问题,虽然 yarn 提高了依赖包的安装速度与使用体验,但它依旧没有解决 npm 的依赖重复安装等致命问题。「pnpm」的出现完美解决了依赖包重复安装的问题,并且实现了 yarn 带来的所有优秀体验,所以说 pnpm 才是前端工程化项目的未来

一、npm 与 yarn 存在的问题

1. 早期的 npm

在 npm@3 之前,node_modules 结构可以说是整洁、可预测的,因为当时的依赖结构是这样的:

node_modules 
└─ 依赖A 
   ├─ index.js 
   ├─ package.json 
   └─ node_modules 
       └─ 依赖B 
       ├─ index.js 
       └─ package.json
 └─ 依赖C 
   ├─ index.js 
   ├─ package.json 
   └─ node_modules 
       └─ 依赖B 
       ├─ index.js 
       └─ package.json

每个依赖下面都维护着自己的 node_modules,这样看起来确实非常整洁,但同时也带来一些较为严重的问题:

  • 依赖包重复安装
  • 依赖层级过多
  • 模块实例无法共享
1.1 依赖包重复安装

从上面的依赖结构我们可以看出,依赖 A 与依赖 C 同时引用了依赖 B,此时的依赖B会被下载两次。此刻我们想想要是某一个依赖被引用了 n 次,那么它就需要被下载 n 次。

1.2 依赖层级过多

我们再来看另外一种依赖结构:

node_modules 
└─ 依赖A 
   ├─ index.js 
   ├─ package.json 
   └─ node_modules 
       └─ 依赖B 
       ├─ index.js 
       ├─ package.json
       └─ node_modules 
           └─ 依赖C 
           ├─ index.js 
           ├─ package.json 
           └─ node_modules 
               └─ 依赖D 
               ├─ index.js 
               └─ package.json

这种依赖层级少还能接受,要是依赖层级多了,这样一层一层嵌套下去,就像一个依赖地狱,不利于维护。

2. npm@3 与 yarn

为了解决上述问题,npm3yarn 都选择了扁平化结构,也就是说现在我们看到的 node_modules 里面的结构不再有依赖嵌套了,都是如下依赖结构:

node_modules 
└─ 依赖A  
    ├─ index.js 
    ├─ package.json 
    └─ node_modules 
└─ 依赖C   
    ├─ index.js 
    ├─ package.json 
    └─ node_modules 
└─ 依赖B 
    ├─ index.js 
    ├─ package.json 
    └─ node_modules 

node_modules下所有的依赖都会平铺到同一层级。由于 require 寻找包的机制,如果 A 和 C 都依赖了 B,那么 A 和 C 在自己的 node_modules 中未找到依赖 C 的时候会向上寻找,并最终在与他们同级的 node\_modules 中找到依赖包 C。这样就不会出现重复下载的情况,而且依赖层级嵌套也不会太深,因为没有重复的下载,所有的 A 和 C 都会寻找并依赖于同一个B包,自然也就解决了实例无法共享数据的问题。

由于这个扁平化结构的特点,想必大家都遇到了这样的体验,自己明明就只安装了一个依赖包,打开node_modules文件夹一看,里面却有一大堆。

这种扁平化结构虽然是解决了之前的嵌套问题,但同时也带来了另外一些问题:

  • 依赖结构的不确定性
  • 扁平化算法的复杂度增加
  • 项目中仍然可以非法访问没有声明过的依赖包(幽灵依赖)
2.1 依赖结构的不确定性

这个怎么理解,为什么会产生这种问题呢?我们来仔细想想,假如有如下一种依赖结构:

为什么说 pnpm 才是前端工程化项目的未来?

A 包与 B 包同时依赖了 C 包的不同版本,「由于同一目录下不能出现两个同名文件,所以这种情况下同一层级只能存在一个版本的包,另外一个版本还是要被嵌套依赖。」

那么问题又来了,既然是要一个扁平化一个嵌套,那么这时候是如何确定哪一个扁平化哪一个嵌套的呢?

为什么说 pnpm 才是前端工程化项目的未来?

这两种结构都有可能,准确点说哪个版本的包被提升,取决于包的安装顺序!

这就是为什么会产生依赖结构的不确定问题,也是 lock 文件诞生的原因,无论是package-lock.json(npm 5.x 才出现)还是yarn.lock,都是为了保证 install 之后都产生确定的node_modules结构。尽管如此,npm/yarn 本身还是存在扁平化算法复杂和 package 非法访问的问题,影响性能和安全。

二、pnpm

前面说了那么多的 npmyarn 的缺点,现在再来看看 pnpm 是如何解决这些问题的。

1. 什么是 pnpm

快速的,节省磁盘空间的包管理工具。其实它跟 npm 与 yarn 没有区别,都是包管理工具。但它的独特之处在于:

  1. 包安装速度极快
  2. 磁盘空间利用非常高效

2. pnpm 的特性

2.1 安装包速度快

为什么说 pnpm 才是前端工程化项目的未来?

从上图可以看出,pnpm 的包安装速度明显快于其它包管理工具,那么它为什么会比其它包管理工具快呢?我们来可以来看一下各自的安装流程:

  • npm/yarn

为什么说 pnpm 才是前端工程化项目的未来?

  1. resolving:首先他们会解析依赖树,决定要fetch哪些安装包
  2. fetching:安装去fetch依赖的tar包,这个阶段可以同时下载多个,来增加速度
  3. wrting:然后解压包,根据文件构建出真正的依赖树,这个阶段需要大量文件IO操作
  • pnpm

为什么说 pnpm 才是前端工程化项目的未来?

上图是 pnpm 的安装流程,可以看到针对每个包的三个流程都是平行的,所以速度会快很多,当然 pnpm 会多一个阶段,就是通过链接组织起真正的依赖树目录结构。

2.2 磁盘空间利用非常高效

pnpm 内部使用 基于内容寻址 的文件系统来存储磁盘上所有的文件,这个文件系统出色的地方在于:

  • 不会重复安装同一个包。用 npm/yarn 的时候,如果 100 个项目都依赖 lodash,那么 lodash 很可能就被安装了 100 次,磁盘中就有 100 个地方写入了这部分代码。但在使用 pnpm 只会安装一次,磁盘中只有一个地方写入,后面再次使用都会直接使用 hardlink
  • 即使一个包的不同版本,pnpm 也会极大程度地复用之前版本的代码。举个例子,比如 lodash 有 100 个文件,更新版本之后多了一个文件,那么磁盘当中并不会重新写入 101 个文件,而是保留原来的 100 个文件的 hardlink,仅仅写入那 一个新增的文件
2.3 支持monorepo

pnpm 与 npm/yarn 另外一个很大的不同就是支持了 monorepo,pnpm 内置了对 monorepo 的支持,只需在工作空间的根目录创建 pnpm-workspace.yaml 和 .npmrc 配置文件,同时还支持多种配置,相比较 lerna 和 yarn workspace,pnpm 解决 monorepo 的同时,也解决了传统方案引入的问题。

monorepo 的宗旨就是用一个 git 仓库来管理多个子项目,所有的子项目都存放在根目录的 packages 目录下,那么一个子项目就代表一个 package

3. 依赖管理

pnpm 使用的是 npm version 2.x 类似的嵌套结构,同时使用 .pnpm 以平铺的形式储存着所有的包,然后使用 Store + Links 和文件资源进行关联。简单地说 pnpm 把会包下载到一个公共目录,如果某个依赖在 sotre 目录中存在了话,那么就会直接从 store 目录里面去 hard-link,避免了二次安装带来的时间消耗,如果依赖在 store 目录里面不存在的话,就会去下载一次。通过 Store + hard link 的方式,使得项目中不存在 NPM 依赖地狱问题,从而完美解决了 npm3+ 和 yarn 中的包重复问题。

为什么说 pnpm 才是前端工程化项目的未来?

我们分别用npmpnpm来安装 vite 对比看一下:

npmpnpm
为什么说 pnpm 才是前端工程化项目的未来?为什么说 pnpm 才是前端工程化项目的未来?
所有依赖包平铺在node_modules目录,包括直接依赖包以及其他次级依赖包node_modules目录下只有.pnpm和直接依赖包,没有其他次级依赖包
没有符号链接(软链接)直接依赖包的后面有符号链接(软链接)的标识

pnpm 安装的vite 所有的依赖都软链至了 node_modules/.pnpm/ 中的对应目录,把 vite 的依赖放置在同一级别避免了循环的软链。

3.1 软链接 和 硬链接 机制

pnpm 是通过 hardlink 在全局里面搞个 store 目录来存储 node_modules 依赖里面的 hard link 地址,然后在引用依赖的时候则是通过 symlink 去找到对应虚拟磁盘目录下(.pnpm 目录)的依赖地址。

这两者结合在一起工作之后,假如有一个项目依赖了 A@1.0.0 和 B@1.0.0 ,那么最后的 node_modules 结构呈现出来的依赖结构可能会是这样的:

node_modules
└── A // symlink to .pnpm/A@1.0.0/node_modules/A
└── B // symlink to .pnpm/B@1.0.0/node_modules/B
└── .pnpm
    ├── A@1.0.0
    │   └── node_modules
    │       └── A -> <store>/A
    │           ├── index.js
    │           └── package.json
    └── B@1.0.0
        └── node_modules
            └── B -> <store>/B
                ├── index.js
                └── package.json

node_modules 中的 A 和 B 两个目录会软连接到 .pnpm 这个目录下的真实依赖中,而这些真实依赖则是通过 hard link 存储到全局的 store 目录中

参考文章:点这里