PNPM的使用

一 、NPMP包管理工具

pnpm 介绍

pnpmperformant npm(高性能的 npm),如 pnpm 官网所言,它是快速的,节省磁盘空间的包管理工具,同时,它也较好地支持了 workspacemonorepos

​ 举例来说,如果项目中,你使用了某个依赖项的多个版本,那么 pnpm 只会将有差异的文件添加到仓库。如果某个依赖包有 100 个文件,而它的新版本只改变了其中 1 个文件。那么 pnpm update 时只会添加 1 个新文件,而不会复制整个新版本的所有包。此外。所有文件都会存储在硬盘上的某一位置。 当依赖包被被安装时,其中的文件会硬链接到这一位置,而不会占用额外的磁盘空间。同时,项目中允许共享同一版本的依赖。接下来我们先了解下 pnpm 的使用效果

安装配置

使用 PowerShell安装:

iwr https://get.pnpm.io/install.ps1 -useb | iex

使用npm安装(Node.js版本需v14及以上)

npm install -g pnpm

设置仓库地址

pnpm config set store-dir path-to/your-dir

以下是各版本 pnpm 与各版本 Node.js 之间的支持表格。

Node.js pnpm 4 pnpm 5 pnpm 6 pnpm 7
Node.js 10 ✔️ ✔️
Node.js 12 ✔️ ✔️ ✔️
Node.js 14 ✔️ ✔️ ✔️ ✔️
Node.js 16 未知 未知 ✔️ ✔️
Node.js 18 未知 未知 ✔️ ✔️

pnpm 效果

npmyarnyarn pnp 工具链效果对比,来自 pnpm benchmarks

action cache lockfile node_modules npm pnpm Yarn Yarn PnP
install 1m 9.5s 15.3s 16.6s 23.6s
install 2.4s 1.3s 2.3s n/a
install 14.8s 4s 6.8s 1.5s
install 21.8s 8.9s 11.2s 6.2s
install 35.4s 13.4s 12s 17.9s
install 3.1s 1.9s 7s n/a
install 2.4s 1.3s 7.6s n/a
install 3s 6.1s 11.8s n/a
update n/a n/a n/a 2.3s 11.8s 15.5s 28.3s

从上表数据我们可以看出,pnpm 的各项性能均比其它包管理工具有优势,为什么 pnpm 有如此优越的表现?

pnpm 的原理

pnpm 主要有两个不同与其包管理工具的特性:

基于硬链接的 node_modules

pnpm 创建从全局存储到项目中 node_modules 文件夹的硬链接,而硬链接指向磁盘上原始文件所在的同一位置,具体来说就是 node_modules 中每个包的每个文件都是来自内容可寻址存储的硬链接,简言之,就是特定版本和名称的包全局只有一份。举例来看:

node_modules
└── .pnpm
    ├── [email protected]
    │   └── node_modules
    │       └── bar -> <store>/bar
    │           ├── index.js
    │           └── package.json
    └── [email protected]
        └── node_modules
            └── foo -> <store>/foo
                ├── index.js
                └── package.json

node_modules 下面的唯一文件夹叫做 .pnpm, .pnpm 下面是一个 <PACKAGE_NAME@VERSION> 文件夹,而在其下面 <PACKAGE_NAME> 的文件夹是一个基于内容可寻址存储的硬链接。同时,我们也可以通过 pnpm root 命令来打印当前项目中存放模块(modules)的有效目录

基于依赖解析的软链接 symlinks

观察以下依赖包结构

node_modules
├── foo -> ./.pnpm/[email protected]/node_modules/foo
└── .pnpm
    ├── [email protected]
    │   └── node_modules
    │       └── bar -> <store>/bar
    └── [email protected]
        └── node_modules
            ├── foo -> <store>/foo
            └── bar -> ../../[email protected]/node_modules/bar

我们可以看到在 [email protected]/node_modules/bar 内引用了 bar 的软链接 ../../[email protected]/node_modules/bar,而在项目里引用 foo 的软链接 ./.pnpm/[email protected]/node_modules/foo,如果项目内新增一个依赖包 [email protected],则其引用结构如下:

node_modules
├── foo -> ./.pnpm/[email protected]/node_modules/foo
└── .pnpm
    ├── [email protected]
    │   └── node_modules
    │       ├── bar -> <store>/bar
    │       └── qar -> ../../[email protected]/node_modules/qar
    ├── [email protected]
    │   └── node_modules
    │       ├── foo -> <store>/foo
    │       ├── bar -> ../../[email protected]/node_modules/bar
    │       └── qar -> ../../[email protected]/node_modules/qar
    └── [email protected]
        └── node_modules
            └── qar -> <store>/qar

根据前文我们介绍到的Nodejs的依赖解析规则[email protected]/node_modules/foo/index.js 中所需的依赖包 bar,实际上使用的是[email protected]/node_modules/bar中的内容,因此,只有真正在依赖项中的包才能被访问到。而对于不同的 peer dependencies 的依赖解析原理,可以参考这里 How peers are resolved

通过**基于硬链接的node_modules**和**基于依赖解析的软链接**原理,我们了解到,当我们在相同操作系统下第二次安装同一个依赖包时,我们需要做的仅仅是创建一个该依赖包对应的硬链接,对于同一个依赖包的不同版本,也只有不同的部分会被重新保存起来,而具体有没有 pnpm 是在哪里判断的呢?全局的 pnpm 索引文件在 ~/.pnpm-store/v3/files。基于此,使用硬链接让依赖包的安装速度非常快,同时也去除了冗余,节省较大磁盘空间。

symlinks 符号连接

实际应用

  1. 执行pnpm i
  2. 执行pnpm run uno package.json文件修改后取消安装,
  3. 执行pnpm i命令后等待依赖导入完成。
  4. 执行npm run start
此时你会发现... 项目报错找不到依赖文件image.png image.png

查找资料发现,yarnnpm 会将所有依赖平铺在 node_modules,在项目中可以访问任何依赖的包。默认配置的 pnpm 创建 node_modules 时不会平铺依赖,我们需要将依赖提升至node_modules根目录。

在安装依赖之前我们需要先配置项目

  1. 在项目根目录创建 .npmrc文件,添加如下配置,此配置会将所有内容提升到node_modules的根目录。
  2. 执行pnpm i
  3. 执行pnpm run uno ,在package.json文件修改后取消安装,
  4. 执行pnpm i命令后等待依赖导入完成。
  5. 执行npm run start
auto-install-peers=true # 是否自动下载peer依赖项
shamefully-hoist=true # 是否将所有依赖包平铺在node_modules

peers依赖项文档
shamefully-hoist配置文档

如果明确知道需要提升的包名,可以按如下设置:

public-hoist-pattern[]=moment
public-hoist-pattern[]=@umijs/*
public-hoist-pattern[]=antd
public-hoist-pattern[]=umi-request
...

缺陷

Pnpm 不能跨多个驱动器或文件系统工作

包存储应与安装的位置处于同一驱动器和文件系统上,否则,包将被复制,而不是被链接。 这是由于硬链接的工作方式带来的一个限制,因为一个文件系统上的文件无法寻址另一个文件系统中的位置。 有关更多详细信息,请参阅 issue #712