pnpm为什么是新一代包管理利器?

1,244 阅读8分钟

前言

都2202了,不会还有人没听过pnpm吧(手动狗头🐶)。如果真没听过,看看这篇文章定会让你有所收获。

首先放张pnpm的截图,从截图中我们可以看到pnpm的周下载量已经达到90w+了,足以证明他的强大了。接下来我们将通过what、why、how三个方面展开介绍。

image.png

什么是pnpm(what)

pnpm 的官方文档是这样说的:

快速的,节省磁盘空间的包管理工具

因此,pnpm 本质上就是一个包管理器,这一点跟 npm/yarn 没有区别,但它作为杀手锏的两个优势在于:

  • 节约磁盘空间并提升安装速度
  • 创建非扁平化的 node_modules 文件夹

节约磁盘空间并提升安装速度

当使用 npm 或 Yarn 时,如果你有 100 个项目使用了某个依赖(dependency),就会有 100 份该依赖的副本保存在硬盘上。  而在使用 pnpm 时,依赖会被存储在内容可寻址的存储中,所以:

  1. 如果你用到了某依赖项的不同版本,只会将不同版本间有差异的文件添加到仓库。 例如,如果某个包有100个文件,而它的新版本只改变了其中1个文件。那么 pnpm update 时只会向存储中心额外添加1个新文件,而不会因为仅仅一个文件的改变复制整新版本包的内容。
  2. 所有文件都会存储在硬盘上的某一位置。 当软件包被被安装时,包里的文件会硬链接到这一位置,而不会占用额外的磁盘空间。 这允许你跨项目地共享同一版本的依赖。

因此,您在磁盘上节省了大量空间,这与项目和依赖项的数量成正比,并且安装速度要快得多!

创建非扁平化的 node_modules 文件夹

使用 npm 或 Yarn Classic 安装依赖项时,所有包都被提升到模块目录的根目录。 因此,项目可以访问到未被添加进当前项目的依赖。

默认情况下,pnpm 使用软链的方式将项目的直接依赖添加进模块文件夹的根目录。 如果你想了解 pnpm 关于 node_modules 结构设计的更多细节,以及为什么它在 Node.js 生态成为了后起之秀,请参考:

它的安装也非常简单。(不过你得保证你的node主版本大于10)

npm i -g pnpm

为什么要用pnpm(why)

1.安装速度更快

这里附上一个非常直观的安装速度对比(详细对比了npm、pnpm、yarn、yarn pnp在有无缓存,lock文件,node_modules文件情况下的详细时间)。该应用程序在package.json 这里

actioncachelockfilenode_modulesnpmpnpmYarnYarn PnP
install48.4s18s21.7s28s
install2s1.4s2.8sn/a
install10.4s4.7s8.6s1.8s
install15.6s8.1s14.5s7.6s
install27.8s16.4s14.9s21s
install2.5s2.8s8.8sn/a
install2s1.4s9.3sn/a
install2.5s9.5s15.1sn/a
updaten/an/an/a2s11.9s20.4s34.9s

image.png

可以看到,作为黄色部分的 pnpm,在绝多大数场景下,包安装的速度都是明显优于 npm/yarn,速度会比 npm/yarn 快 2-3 倍。

安装快速原因

看到了效果之后,让我们深入了解一下为啥与其他“传统”包管理器相比,为什么 pnpm 如此疯狂?

pnpm 没有安装的阻塞阶段。每个依赖都有自己的阶段,下一个阶段会尽快开始。

首先看传统的包管理器(npm、yarn)安装过程,他们会统一去执行解析,解析完成后执行请求,统一请求完成后再一并写入到node_modules,这当中如果某个过程很慢,会导致后续过程一直处于等待中。即安装时间 = resolve时间(all package) + fetch时间(all package) + white时间(all package)

执行npm i 或者 yarn 命令后,首先会构建依赖树,然后针对每个节点下的包,会经历下面三个步骤:

    1. 将依赖包的版本区间解析为具体的版本号
    1. 下载对应版本依赖的 tar 包到本地离线镜像
    1. 将依赖从离线镜像解压到本地缓存,将依赖从缓存拷贝到当前目录的 node_modules 目录

image.png

而pnpm则是为每个依赖去独立执行安装过程,安装时间 = resolve时间(最耗时包) + fetch时间(最耗时包) + white时间(最耗时包)

image.png

2.更高效的利用磁盘空间

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

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

3.可管理node环境

安装并使用指定版本的 Node.js

安装 LTS 版本的 Node.js:

pnpm env use --global lts
pnpm env use --global argon

安装 v16 的Node.js

pnpm env use --global 16

安装 Node.js 的预发行版本

pnpm env use --global nightly
pnpm env use --global rc
pnpm env use --global 16.0.0-rc.0
pnpm env use --global rc/14

安装最新版本的 Node.js:

pnpm env use --global latest

配置项

--global, -g

此更改将全局生效。

4. 安全性高

之前在使用 npm/yarn 的时候,由于 node_module 的扁平结构,如果 A 依赖 B, B 依赖 C,那么 A 当中是可以直接使用 C 的,但问题是 A 当中并没有声明 C 这个依赖。因此会出现这种非法访问的情况。但 pnpm 脑洞特别大,自创了一套依赖管理方式,很好地解决了这个问题,保证了安全性。

如何使用pnpm(how)

以下是简便的 npm 命令等效列表,可帮助您入门:

npm 命令pnpm 等效
npm installpnpm install
npm i <pkg>[pnpm add <pkg>]
npm run <cmd>[pnpm <cmd>]

当你使用一个未知命令时, pnpm 会查找一个具有指定名称的脚本, 所以 pnpm run lint 和 pnpm lint相同. 如果没有指定名称的脚本,那么pnpm将以shell脚本的形式执行该命令,所以你可以做类似pnpm eslint的事情(查阅 pnpm exec).

拓展

配置package.json中依赖版本的^和~区别

1.标准版本

SemVer规范的标准版本号采用 X.Y.Z 的格式,其中 X、Y 和 Z 为非负的整数,且禁止在数字前方补零。X 是主版本号、Y 是次版本号、而 Z 为修订号。每个元素必须以数值来递增。

  • 主版本号(major):当你做了不兼容的API 修改
  • 次版本号(minor):当你做了向下兼容的功能性新增
  • 修订号(patch):当你做了向下兼容的问题修正。

2.先行版本

当某个版本改动比较大、并非稳定而且可能无法满足预期的兼容性需求时,你可能要先发布一个先行版本。我们可以给这个不稳定的版本贴上标签beta,因为一般 npm install 的版本都是@latest

  • 内部版本(alpha):如1.0.0-alpha.1
  • 公测版本(beta):如1.0.0-beta.1
  • 正式版本的候选版本rc: 即 Release candiate,如1.0.0-rc.1

3.依赖版本管理

我们经常看到,在 package.json 中各种依赖的不同写法:

"dependencies": {
    "signale": "1.4.0",
    "figlet": "*",
    "react": "16.x",
    "table": "~5.4.6",
    "yargs": "^14.0.0"
}

前面三个很容易理解:

"signale": "1.4.0",// 固定版本号
"figlet": "*",//任意版本(>=0.0.0)
"react": "16.x",//匹配主要版本(>=16.0.0 <17.0.0)
"react": "16.3.x",//匹配主要版本和次要版本(>=16.3.0 <16.4.0)

再来看看后面两个,版本号中引用了 ~ 和 ^ 符号:

~: 当安装依赖时获取到有新版本时,安装到 x.y.z 中 z 的最新的版本。即保持主版本号、次版本号不变的情况下,保持修订号的最新版本。

^: 当安装依赖时获取到有新版本时,安装到 x.y.z 中 y 和 z 都为最新版本。即保持主版本号不变的情况下,保持次版本号、修订版本号为最新版本。

参考:

npm版本控制遵循SemVer规范

node_modules困境

.lock文件

npm在npm3版本中曾推出shrinkwrap,用来解决项目依赖版本号的问题,不过并没有取的理想的效果,但是通过后面npm4和npm5不断迭代,后来产生了package.lock(pm v5 中只需要 package-lock.json 就可以保正确的 node_modules 目录结构)。

使用yarn作为包管理工具的项目会生成yarn.lock(yarn 需要同时拥有 yarn.lock 文件和 package.json文件)。

使用pnpm则为生成pnpm-lock.yaml

他们存在的目的都是一样的,都是为了解决 ‘在我的电脑上明明是好的啊,怎么到你那里就不行了’ 的问题。

总结

如果你正苦于安装依赖等待时间太久,并且烦难于在多个项目间每次都需要重复安装依赖。那么pnpm会非常适合你。现在pnpm经过几年的迭代已经到pnpm7版本了,可以放心使用噢。