由浅入深 学习 pnpm
Why pnpm
最近公司的项目迁移使用了
pnpm这款包管理工具,正所谓知己知彼,百战不殆。就跟着这篇文章快速的学习一下pnpm吧
指令快速学习
// 安装 pnpm
npm install -g pnpm
// or
yarn global add pnpm
// 查看版本
pnpm -v
// 查看帮助
pnpm -h
// 安装所有的包
pnpm install // 别名 i
// 更新所有的包
pnpm update // 别名 up
// 安装某一个包
pnpm add react // dependencies
pnpm add eslint -D // devDependencies
// 移除某一个包
pnpm remove react // 别名 rm、un、uninstall
嗯嗯,已经学会了。这就去开发!
等等!这些和yarn,npm并没有什么区别呀,那我们为什么要使用 pnpm呢
Store
介绍
在 pnpm中有一个很重要的概念:store,这个单词我们很常见,在pnpm中,它的意思是一个全局的仓库,pnpm他会把包缓存到store中,我们要安装包的时候并不是优先联网下载的,是先读取的这个store。
我们安装包到 node_modules的过程如下

安装包的时候会同时的去看 node_modules以及store
- 两个都没有的时候会联网下载
store有,node_modules没有,会从store中复制过去store没有,node_modules有,也会补充store
store有关的指令
// 读取 store的位置
pnpm config get store-dir // 默认是当前磁盘下的.pnpm-store的文件夹中
// 设置 store的位置
pnpm config set store-dir "D:\env\.pnpm-store"
store的体现
// 创建文件夹
mkdir test-pnpm
// 进入
cd test-pnpm
// 初始化文件夹为一个npm项目
pnpm init Or npm init -y
// 安装 express
pnpm add express
执行规程如图

我们发现在 pnpm add完成的时候,他会告诉我们 store的地址,还有一个 Virtual store 的地址,这个就是当前目录下的node_modules/.pnpm,这里路径上有一个 .pnpm后面在讨论。
在最后输出了这样一句话
// 这里和截图不同是因为我多次下载了请忽略~
Progress: resolved 57, reused 0, downloaded 57, added 57, done
我们解析一下
resolved 57,在下载express这一个包的时候,解析出来这个包以及它的依赖,总共有57项。reused 0,store中的包重复使用了0次,毕竟我们这是第一次下载,重用是0没毛病。downloaded 57,联网下载了 57 个包added 57,添加到 store 中包有57项
OK,具体这些怎么体现我们可以
- 新建一个项目在安装
express可以体现出reused - 删掉
node_modules在下载可以体现reused - 不删
node_modules,删掉store文件夹可以体现added - 删掉
node_modules,删掉store文件夹可以体现downloaded
小总结
那么到这里,我们可以了解到,pnpm是使用了一个 store 机制,让我们下载包的速度极大的提升了
pnpm 的 node_modules解析
pnpm的优异之处不仅仅是使用了store 加快了下载数据,还有它的node_modules也很有意思
Tip: 我们安装包是安装到当前项目的 node_modules 下,写代码 import/require 找包是node的机制,它找依赖是一层层的向上找node_modules,这里我们只去了解node_modules 即可,与上面的 store 完全无关
node_modules历史
npm2 的node_modules
最早版本:npm2

这个时候我们安装一下 express,node_modules是只有一个 express文件夹的

但是展开后我们就很容易发现问题了。

我们安装的 node_modules 中有express
express有它自己的 node_modules
express依赖的包也有它的 node_modules
依赖的包也有它依赖包的包。。。。反正就这样一直嵌套下去了
这里会有以下几个问题
- 安装
AB两个包,这两个包都依赖了C,这个时候C就会被安装两遍,即 公共依赖会被多次安装 windows的文件路径最长是 260 多个字符,嵌套的文件结构一直嵌套下去肯定是不行的
当时 npm 还没解决,社区就出来新的解决方案了,就是 yarn
yarn 的node_modules
yarn 解决嵌套的思路是将嵌套依赖打散到同一层,这样也就没有依赖重复多次的问题了,路径过长的问题了。
使用yarn 安装express的文件夹结构如下

这样的打散做法很好,但不是绝对的
比如
我们安装 A B 包
A依赖 1.0.0 版本的C包
B依赖 2.0.0 版本的C包
这个时候项目 node_modules 目录里面会有 A B C 包,C 包的版本是取决于安装过程先解析的是A还是B
。如果先解析的是A,则项目node_modules 目录中的C包版本是1.0.0,在B包中依然会存在 node_modules文件夹 ,在B包的node_modules 中依赖 2.0.0 版本的C包
如果先解析的B,同理。
这样感觉也很合理,但是有一个致命的问题,叫幽灵依赖。
我们依赖的是 A B,但是我们在代码中是可以写 require("C")的,也就是打散的过程中,将不是我们的依赖也打散到node_modules中了,导致我们可以引入到C包
并且,并没有实质性的解决我们重复依赖导致包被多次安装,占用重复的空间。
还是上面的例子,打散的是1.0.0的C包,如果还有一个D包依赖2.0.0的C包,则 2.0.0的C包在 安装B包和D 包的过程中会被安装两次!
pnpm 的node_modules
我们来看一下 pnpm 的做法

首先 node_modules 中只有一个 express 目录,但是我们发现后面多了一个链接符号,也就是说它真正的位置是不在这里的,这里只是一个软连接,保证我们在写 require("express") 的时候不会报错,这样的作法就解决了幽灵依赖的问题。
这个时候我们发现有一个 .pnpm的文件夹,这个文件夹的秘密就太多了。

我们一点点来解密这个 .pnpm 目录
pnpm的思路是将所有包,以及包依赖的包,等嵌套依赖的包,全部打散到.pnpm目录中,为了解决重复安装相同版本的包的问题,他将包的版本也写到了文件名中。- 我们安装的
express,在项目的node_modules目录中它是一个软连接,链接到了.pnpm目录中.pnpm/express@4.18.1/node_modules/express目录- 注意一下这个文件结构,
.pnpm中 是带版本号的 文件夹,这个目录中会有一个node_modules,在这里会有一个包名的文件夹,这个文件夹中才是真正源代码所在的目录
- 注意一下这个文件结构,
- 那我们发现,
.pnpm/express@4.18.1/node_modules/这个目录下,express目录是真实源代码所在地,跟他同级还有很多的软连接。这个软连接表示的是express这个包依赖的包- 那
express这个包依赖的包,现在是一个软连接,那他真实的路径在哪呢? - 可以发现,他被打散到
.pnpm中了,会有一个包名@版本号的文件夹,比如express依赖的accepts这个包,他真实的源代码是在.pnpm/accepts@1.3.8/node_modules/accepts文件夹下
- 那
那我们就发现了
pnpm会把所有的依赖打散到.pnpm目录下,会带上版本号- 在这个带版本号的文件夹下有一个
node_modules - 这个
node_modules中只有两种情况,一个是包名文件夹,这里是真实的源代码,包依赖的包会在当前node_modules下以软连接的方式,连接到.pnpm/包名@版本/node_modules/包名这个目录中
test-pnpm\node_modules\express // 这个是软连接,实际的地址如下
test-pnpm\node_modules\.pnpm\express@4.18.1\node_modules\express
// express 依赖的包 会在 下面这个目录里面生成 软连接
test-pnpm\node_modules\.pnpm\express@4.18.1\node_modules\依赖包名
// 对应的真实地址是
test-pnpm\node_modules\.pnpm\依赖包名@版本\node_modules\依赖包名
总结
pnpm的最大特点有两个
- 使用
store全局的缓存了包,能在安装包的时候联网下载也能从 项目的node_modules进行备份 - 在
.pnpm文件夹,创建包名@版本号文件夹,以及软连接的方式,杜绝了一个版本的包被多次安装的问题,monoreport类型的项目更加能体现出它的优势
转载自:https://juejin.cn/post/7144234186646224904