pnpm 简介
什么是 pnpm
pnpm(performance npm) 官方文档是这样定义的:
Fast, disk space efficient package manager
pnpm 是新一代包管理工具,它的优势在于:
1. Fast
pnpm is up to 2x faster than the alternatives
以 React 包为例来对比一下,可以看到,pnpm 在绝大多数场景下,包安装的速度都是明显优于 npm/yarn,速度会比 npm/yarn 快 2-3 倍。
补充
yarn 还有 PnP 安装模式,直接去掉 node_modules,将依赖包内容写在磁盘,节省了 node 文件 I/O 的开销,这样也能提升安装速度。(具体原理可参考这篇文章)
2. Efficient
Files inside node_modules are cloned or hard linked from a single content-addressable storage
pnpm 内部使用基于内容寻址的文件系统来存储磁盘上所有的文件,这个文件系统出色的地方在于:
- 不会重复安装同一个包。用 npm/yarn 的时候,如果 100 个项目都依赖 lodash,那么 lodash 很可能就被安装了 100 次,磁盘中就有 100 个地方写入了这部分代码。但在使用 pnpm 只会安装一次,磁盘中只有一个地方写入,后面再次使用都会直接使用 hardlink(参考文章)。
- 即使一个包的不同版本,pnpm 也会极大程度地复用之前版本的代码。举个例子,比如 lodash 有 100 个文件,更新版本之后多了一个文件,那么磁盘当中并不会重新写入 101 个文件,而是保留原来的 100 个文件的 hardlink,仅仅写入那一个新增的文件。
3. Supports monorepos
pnpm has built-in support for multiple packages in a repository
随着前端工程的日益复杂,越来越多的项目开始使用 monorepo。之前对于多个项目的管理,我们一般都是使用多个 git 仓库,但 monorepo 的宗旨就是用一个 git 仓库来管理多个子项目,所有的子项目都存放在根目录的packages目录下,那么一个子项目就代表一个package。如果你之前没接触过 monorepo 的概念,建议仔细看看这篇文章以及开源的 monorepo 管理工具lerna,项目目录结构可以参考一下 babel 仓库。
pnpm 与 npm/yarn 另外一个很大的不同就是支持了 monorepo,体现在各个子命令的功能上,比如在根目录下pnpm add A -r, 那么所有的 package 中都会被添加 A 这个依赖,当然也支持 --filter 字段来对 package 进行过滤。
4. Strict
pnpm creates a non-flat node_modules by default, so code has no access to arbitrary packages
之前在使用 npm/yarn 的时候,由于 node_module 的扁平结构,如果 A 依赖 B, B 依赖 C,那么 A 当中是可以直接使用 C 的,但问题是 A 当中并没有声明 C 这个依赖。因此会出现这种非法访问的情况。但 pnpm 脑洞特别大,自创了一套依赖管理方式,很好地解决了这个问题,保证了安全性。
依赖管理方式
npm/yarn 的依赖管理方式
npm install 安装是在确定了包的版本后,获取包信息,构建依赖树然后进行扁平化处理,将所有依赖包安装在同一层级。
在使用依赖包的过程中,所有依赖都在同一层级去找,解决了 npm@3 之前的版本以下问题:
- 将所有依赖包安装成树结构之后,大量重复的安装了依赖,造成安装速度非常慢,磁盘空间管理非常差
- 依赖的层级太深导致文件路径过长
但是依赖扁平化后,就会出现下列问题:
- 依赖结构的不确定性
- 模块可以访问没有声明依赖的包
- 扁平化一个依赖树的算法非常复杂
第一点中的「不确定性」是如何体现的呢?假如现在项目依赖两个包 foo 和 bar,这两个包的依赖又是这样的:
那么
npm/yarn install 的时候,通过扁平化处理之后,究竟是这样
还是这样?
答案是: 都有可能。取决于 foo 和 bar 在
package.json中的位置,如果 foo 声明在前面,那么就是前面的结构,否则是后面的结构。
这就是为什么会产生依赖结构的不确定问题,也是 lock 文件诞生的原因,无论是package-lock.json还是yarn.lock,都是为了保证 install 之后都产生确定的 node_modules 结构。
尽管如此,npm/yarn 本身还是存在扁平化算法复杂和 package 非法访问的问题,影响性能和安全。
pnpm 的依赖管理方式
树结构会出现问题,扁平化后也会出现问题,那么 pnpm 是怎么解决的呢?
例子
以安装 express 为例,通过pnpm install express安装后查看 node_modules:
▾ node_modules
▸ .pnpm
▸ express
.modules.yaml
我们直接就看到了 express,但值得注意的是,这里仅仅只是一个软链接,里面并没有 node_modules 目录,如果是真正的文件位置,那么根据 node 的包加载机制,它是找不到依赖的。那么它真正的位置在哪呢?
我们继续在 .pnpm 当中寻找,最后在 .pnpm/express@4.17.1/node_modules/express下面找到了!
▾ node_modules
▾ .pnpm
▸ accepts@1.3.7
▸ array-flatten@1.1.1
...
▾ express@4.17.1
▾ node_modules
▸ accepts
▸ array-flatten
▸ body-parser
▸ content-disposition
...
▸ etag
▾ express
▸ lib
History.md
index.js
LICENSE
package.json
Readme.md
并且 express 的依赖都在.pnpm/express@4.17.1/node_modules下面,这些依赖也全都是软链接,例如.pnpm/express@4.17.1/node_modules/etag链接到的真实地址是.pnpm/etag@1.8.1/node_modules/etag
解释
.pnpm 目录下虽然呈现的是扁平的目录结构,但仔细想想,顺着软链接慢慢展开,其实就是嵌套的结构!将包本身和依赖放在同一个 node_module 下面,与原生 Node 完全兼容,又能将 package 与相关的依赖很好地组织到一起,设计十分精妙。
根目录下的 node_modules 下面不再是眼花缭乱的依赖,而是跟 package.json 声明的依赖基本保持一致。即使 pnpm 内部会有一些包会设置依赖提升,会被提升到根目录 node_modules 当中,但整体上,根目录的 node_modules 比以前还是清晰和规范了许多,所以它不会存在之前提到的能访问没有声明依赖的包这个问题。
然后使用 Store + Links 和文件资源进行关联。简单说pnpm把会包下载到一个公共目录,如果某个依赖在 sotre 目录中存在了话,那么就会直接从 store 目录里面去 hard-link,避免了二次安装带来的时间消耗,如果依赖在 store 目录里面不存在的话,就会去下载一次。
pnpm 日常使用及遇到的问题
日常使用
// 安装 axios
pnpm i
// 安装 axios
pnpm add axios
// 安装 axios 并将 axios 添加至 devDependencies
pnpm add axios -D
// 安装 axios 并将 axios 添加至 dependencies
pnpm add axios -S
// 卸载 axios
pnpm remove axios
// monorepo 项目的 package-a 项目中移除 axios
pnpm remove axios --filter package-a
一些问题
使用 pnpm install --shamefully-hoist
如果依赖一直有问题,可以使用pnpm install --shamefully-hoist创建一个扁平node_modules目录结构, 类似于 npm 或 yarn
解决幽灵依赖时,安装默认的包导致报错
先使用 npm 安装,生成package-lock.json, 安装缺少的包时,使用 lock 里面的版本
即使删除了 node_modules 和 lock 文件,安装时,特定的包还是报错
比如我们在升级时,一个包把最新的版本删除了。导致安装时一直失败。可以尝试使用pnpm store prune来删除