pnpm 解决我哪些痛点?

lxf2023-05-04 00:53:20

前言

本文主要目的是帮助大家理解现代包管理工具——pnpm,同时会对 npm/yarn 总结一下潜在缺陷的结论,帮助大家了解到 pnpm 是如何解决 npm/yarn 的设计缺陷的,以及 pnpm 是如何改进的。

npm/yarn 依赖管理

开开胃,简单介绍一下 npm/yarn 在依赖管理方面存在的缺陷问题,方便后续了解 pnpm 特性。

早期的

使用早期的 npm1/2 安装依赖,node_modules 文件夹会以递归的形式呈现,严格按照 package.json 结构以及次级依赖的 package.json 结构将依赖安装到它们各自的 node_modules 中,直到次级依赖不再依赖其它模块。

就像下面这样,foo 依赖 bar 作为次级依赖,bar 会安装到 foo 的 node_modules 里面:

node_modules
└─ foo
   ├─ index.js
   ├─ package.json
   └─ node_modules
      └─ bar
         ├─ index.js
         └─ package.json

假设项目的中的两个依赖同时依赖了相同的次级依赖,那么它们二者的次级依赖将会被重复安装。

node_modules
├─ foo1
│  ├─ index.js
│  ├─ package.json
│  └─ node_modules
│      └─ bar
│          ├─ index.js
│          └─ package.json
└─ foo2
   ├─ index.js
   ├─ package.json
   └─ node_modules
       └─ bar
           ├─ index.js
           └─ package.json

这只是简单的例子,在真实的开发场景中其问题还会更加恶劣:

  • 依赖层级太深,会导致文件路径过长(在 Windows 系统下会出现一些问题

  • 重复的包被安装,导致 node_modules 文件体积巨大,占用过多的磁盘空间

转折点

自 npm3/yarn 开始,相比 npm1/2 项目依赖管理的方式有了很大的改变,不再是以往的“嵌套式”而是采用了“扁平化”方式去管理项目依赖。

这里继续拿上面的例子,foo1 和 foo2 都依赖了 bar,依赖安装后呈现的是下面的这种扁平化目录:

node_modules
├─ bar
│  ├─ index.js
│  └─ package.json
├─ foo1
│  ├─ index.js
│  └─ package.json
└─ foo2
   ├─ index.js
   └─ package.json

扁平化的目录的确解决了上一小节暴露的一些问题,同时也暴露了新的问题:

  • Phantom dependencies

称为“幽灵依赖”,指的是在项目内引用未在 package.json 中定义的包。这个问题在 npm3 展现,因为早期的树形结构导致了依赖冗余和路径过深的问题,npm3 之后采用扁平化的结构,一些第三方包的次级依赖提升到了与第三方包同级。

一旦出现幽灵依赖的问题,可能会导致意想不到的错误,所以一定要正视:

  • 不兼容的版本(例如某一个 api 进行了重大更新

  • 有可能会丢失依赖(某依赖不再依赖呈现在我们项目中的幽灵依赖

// bar 就属于是幽灵依赖,因为它是属于 foo1、foo2 的次级依赖。
import bar from 'bar';
  • NPM doppelgangers

称为“分身依赖”,在 monorepo 项目中非常常见,项目中依赖的第三方包以及第三方包所依赖的同名包都会被重复安装。

常见的问题:

  • 项目打包会将这些“重身”的依赖都进行打包,增加产物体积

  • 无法共享库实例,引用的得到的是两个独立的实例

  • 重复 TypeScript 类型,可能会造成类型冲突

在实际开发中也会出现这样的情景,假设 foo1、foo2、依赖 lodash@3.6.0,bar 依赖 lodash@4.5.0,这时候会造成依赖冲突,解决冲突的方式会将对应的冲突包放到对应依赖目录的 node_mudules 中,类似下面结构:

node_modules
├─ lodash@4.5.0
│  ├─ index.js
│  └─ package.json
├─ foo1
│  ├─ index.js
│  ├─ package.json
│  └─ node_modules
│      └─ lodash@3.6.0
│          ├─ index.js
│          └─ package.json
├─ foo2
│  ├─ index.js
│  ├─ package.json
│  └─ node_modules
│      └─ lodash@3.6.0
│          ├─ index.js
│          └─ package.json
└─ bar
   ├─ index.js
   └─ package.json

这时候你可能会发现一个问题,foo1、boo2 node_modules 下都有重复且版本相同的 lodash@3.6.0,这个问题就是我们正在所说的“分身依赖”的问题。

可能你还会有另外一个疑惑,什么不扁平 lodash@3.6.0,这样能减少一份所占用的空间,还能够解决“分身依赖”的问题。这是因为具体是扁平谁这是根据依赖的顺序决定的。因为开发者不关注依赖的顺序,所以存在很大的不确定性。

结论

  • 扁平化的 node_modules 结构允许访问没有在 package.json 中声明的依赖。
  • 安装效率低,大量依赖被重复安装,磁盘空间占用高。
  • 多个 FE 项目之间已经安装过的的包不能共享,每次都是重新安装。

主角登场

pnpm 解决我哪些痛点?

它是什么?

pnpm 是一个兼容 npm 的 JavaScript 包管理工具,它在依赖安装速度和磁盘空间利用方面都有显着的改进。它与 npm/yarn 非常相似,它们都是使用相同的 package.json 文件管理依赖项,同时也会像 npm/yarn 利用锁文件去确保跨多台机器时保证依赖版本的一致性。

性能基准

先来看一下官方 benchmarks 对 npm、pnpm、yarn、yarnPnP 对多个情景下的性能基准测试,涵盖了很多使用场景:

  • clean install:全新安装,不包含锁文件,没有缓存,没有安装依赖。
  • with cachewith lockfilewith node_modules:第一次安装完成后,再次运行安装命令。
  • with cachewith lockfile:当开发人员获取 repo 并首次运行安装时。
  • with cache:与上面的相同,但包管理器没有可用的锁文件。
  • with lockfile:当安装在 CI 服务器上运行时。
  • with cachewith node_modules:锁文件被删除,再次运行安装命令。
  • with node_moduleswith lockfile:包缓存被删除,再次运行安装命令。
  • with node_modules:包缓存和锁文件被删除,再次运行安装命令。
  • update:通过更改版本 package.json 并再次运行安装命令来更新您的依赖项。

pnpm 解决我哪些痛点?\

结合多个使用场景与官方产出的性能基准报告可以看到 pnpm 的效率要远远高于 npm/yarn。

这是我使用 pnpm 安装的,如下表格展示 pnpm 安装依赖后 node_modules 的目录大小,这效果是非常显著的。不仅仅是在依赖安装的效率方面要优于,在磁盘占用空间方面也大大的节省了,据官方数据统计 pnpm 相比 npm/yarn 效率要高于 2 倍。

yarnpnpm
EV 后台2.6GB1.6GB
运营后台1.4GB524MB

依赖安装

使用 pnpm 安装,pnpm 会将依赖存储在位于 ~/.pnpm-store 目录下。只要你在同一机器下,下次安装依赖的时候 pnpm 会先检查 store 目录,如果有你需要的依赖则会通过一个硬链接丢到到你的项目中去,而不是重新安装依赖。

也可以通过命令直接获取 store 目录所在的位置:pnpm`` store path

下面是包管理重复安装的输入结果:

pnpm 在输出易懂方面也略胜一筹,可以看到你复用了多少包和需要重新下载了多少包。反之 yarn 会是把所有关联的包全部陈列出来,但是这些很大概率我们都是不关心的。

$ pnpm i express

Packages: +52
++++++++++++++++++++++++++++++++++++++++++++++++++++
Progress: resolved 52, reused 52, downloaded 0, added 0, done

dependencies:
+ express 4.17.1
$ yarn add express
yarn add v1.22.11
[1/4]