包管理工具 pnpm 整理

官网:https://pnpm.io/,github:https://github.com/pnpm/pnpm

其实 pnpm 官网从使用、特性、原理社区等建设得都很细致,真正打算使用的话建议通读下官网。

特点

Fast, disk space efficient package manager

pnpm 也是一款包管理工具,这点与 npm/yarn/cnpm/tnpm 一样,目前主流版本为6.x7.0也已开始。

p-download.png

其实 pnpm 是一个挺早的项目,在 21 年才在国内火了起来。

截止目前(2022.02.19)来看,该项目已上升至 15k 的 star,并且像 vue3 等一些著名项目也开始使用了 pnpm:

p-vue.jpg

源码来看,pnpm 是一个高浓度 TypeScript 项目(目前代码中 99%+都是 ts),所以源码方面可以放心食用。

  • 从依赖来看,可以重点留意verdaccioc8,这是 npm 注册及管理的工具;并且源码中多包使用了syncpack

优点

直接拿官网的宣称:pnpm 是一款快速、节省磁盘空间的包管理器。

  • 快速:pnpm 比替代方案(yarn/npm 等)快 2 倍
  • 高效:node_modules 中的文件是从一个单一的可内容寻址的存储中链接过来的
  • 支持 monorepos:pnpm 内置支持了单仓多包
  • 严格:pnpm 默认创建了一个非平铺的 node_modules,因此代码无法访问任意包

pnpm 本质上就是一个包管理器,这一点跟 npm/yarn 没有区别,核心优势就是以及磁盘利用得好。其他优势也可以在作者 blog 中查看:https://www.kochan.io/

以下是一些比较数据,也可以在官网上看到:

p-compare-1

p-compare-2

node_modules

根据核心优势,就需要大致了解下 node_modules 的安装和依赖模式:

  • Nested installation(嵌套安装): 在 npm@3 之前,node_modules 结构是干净、可预测的,因为 node_modules 中的每个依赖项都有自己的 node_modules 文件夹,在 package.json 中指定了所有依赖项。结构就像这样:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    node_modules
    - 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
  • Flat installation(扁平安装):npm@3 / yarn,扁平化结构,多个版本的包只能有一个被提升上来(hoist 机制),优势就是避免了一些重复,其余版本的包会嵌套安装到各自的依赖当中(类似 npm2 的结构),至于哪个版本的包被提升,依赖于包的安装顺序。结构就像这样:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    node_modules
    - 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
    ```

pnpm 既然改了依赖方式,那么就可以看当前模式的主要问题:

  • 重复安装,如上所示的 packageX 2.0 和 packageY 2.0 被重复安装多次,从而造成 npm 和 yarn 的性能一些性能损失。这种场景在 monorepo 多包场景下尤其明显,另外扁平化的算法实现也相当复杂,改动成本很高。
  • Phantom dependencies,幽灵依赖或幻影依赖,即某个包没有在 package.json 被依赖,但是用户却能够引用到这个包。

pnpm 的改变:

  • Net 网状 + Flat 平铺 的 node_modules 结构

    • 虚拟存储目录:.pnpm, 以平铺的形式储存着所有的包,正常的包都可以在这种命名模式的文件夹中被找到(peerDep 例外):
    1
    2
    3
    .pnpm/<organization-name>+<package-name>@<version>/node_modules/<name>

    // 组织名(若无会省略)+包名@版本号/node_modules/名称(项目名称)

    该目录通过<package-name>@<version>来实现相同模块不同版本之间隔离和复用,由于它只会根据项目中的依赖生成,并不存在提升,所以不存在幽灵依赖。

结构就像这样:

1
2
3
4
5
6
7
8
9
10
11
12
node_modules
- .pnpm
- package A
- node_modules
- A -> <store>/A
- index.js
- package.json
- package B
- node_modules
- B -> <store>/B
- index.js
- package.json

如官网描述:
p-pnpm-nodemodules.png

那么它如何跟文件资源进行关联的呢?答案是 Store + Links,可以看官网介绍《Symlinked node_modules structure》或掘金上这篇易于理解的文章《pnpm 的破解之道:网状 + 平铺的 node_modules 结构》

根目录下的 node_modules 下面不再是眼花缭乱的依赖,而是跟 package.json 声明的依赖基本保持一致。即使 pnpm 内部会有一些包会设置依赖提升,会被提升到根目录 node_modules 当中,但整体上,根目录的 node_modules 比以前还是清晰和规范了许多。

pnpm 这种依赖管理的方式也巧妙地规避了非法访问依赖的问题,pnpm 不允许安装 package.json 中没有包含的包,也就是说只要一个包未在 package.json 中声明依赖,那么在项目中是无法访问的。

在此背景情况下,据说 yarn 也计划开始存储模式的优化。

缺点

node_modules 的依赖模式改变下,因为软链的关系必然也会导致一些问题:

  • 1.子依赖提升到同级的目录结构的兼容性问题,类似 Egg、Webpack 的插件加载逻辑,在用到相对路径的地方,需要去适配;
  • 2.软链在不同操作系统的实现不太一样,且在非 SSD 的硬盘上,还是会有一定的磁盘 io 损耗的。
  • 3.多包某种情况下,依赖出现两次:可见官网说明https://pnpm.io/how-peers-are-resolved

其余大部分问题可在官网 faq中找到,也可以看 github 上一些 issue 反馈,包括现在的问题:

p-bugs.jpg

使用

极为简单

安装

1
npm i -g pnpm

使用

与 yarn 类似:

1
2
3
4
5
6
7
8
9
10
11
pnpm add xxx  # 安装xxx到dependencies

pnpm add -D xxx # 安装xxx到devDependencies

pnpm install # 安装项目所有依赖

pnpm install -S xxx 安装xxx并添加到dependencies

pnpm remove xxx # 删除dependencies中的xxx

pnpm remove -D xxx # 删除devDependencies中的xxx

pnpm-workspace.yaml

此文件定义了工作空间的根目录,并能够使您从工作空间中包含 / 排除目录 。 默认情况下,包含所有子目录。

如:

1
2
3
4
5
6
7
# pnpm-workspace.yaml
packages:
# 所有在 packages/ 和 components/ 子目录下的 package
- 'packages/**'
- 'components/**'
# 不包括在 test 文件夹下的 package
- '!**/test/**'

其他如钩子、别名等不太常用的功能查询官网https://pnpm.io/zh/motivation

最后

新的工具必然带来性能/空间上的提升,但在业务使用中我们需要注意它的隐患,避免乐观主义陷阱。


相关链接