npm 存在的问题
我们经常使用 npm
来管理 node 项目中的包,从 package.json
中读取配置将依赖下载到本地,以保障项目的正常运行。
当项目数量多时,这样的包管理方式会非常的占用电脑内存。由于每个项目都有属于自己的依赖,每个项目都需要安装,即使 npm 会对依赖进行缓存,但是每个项目仍然需要安装到自己的 node_modules
文件夹下,此时每个项目安装的每一份依赖都会在磁盘中保存一份,即使各个项目中依赖的版本可能相同。
pnpm
就是针对以上问题出现的解决方案,它使用统一的仓库来存放项目中的包,在项目中使用硬链接+软连接的方式找到依赖所在磁盘的位置。
硬链接和软连接
想要清晰的知道 pnpm 管理依赖的原理,首先要了解硬链接和软连接、拷贝操作的区别。
拷贝操作会在磁盘中复制一份新的数据
,比如拷贝 a.js 为 a_copy.js,两个文件在拷贝后就互不关联,修改 a.js 不会影响 a_copy.js,删除 a_copy.js 也不会影响 a.js。
硬链接是通过寻址的方式
找到磁盘中的数据,比如新建 b_hard.js 与 b.js 创建硬链接,两者指向的是同一个磁盘数据,所以修改其中一个文件,另一个文件也会发生变化。
软连接就是我们平时常见的创建快捷方式
(文件后面会存在一个向右的小箭头),它只是保存着文件的路径,不可以编辑,直接双击就会找到原始的文件。如果原文件被删除,通过软连接将无法找到磁盘中的数据。
我们可以通过命令来进行连接操作,windows 是这样的
/*拷贝*/ copy a.js a_copy.js
/*硬链接*/ mklink /H b_hard.js b.js
/*软连接*/ mklink c_soft.js c.js
pnpm原理
使用 npm 或者 yarn 时,如果有100个项目,并且所有项目都有一个相同的依赖包,那么在磁盘上就需要保存100份该相同依赖的包
。
如果使用 pnpm,依赖包将被放在统一的位置,当安装包时,其包含的所有文件会硬链接到这个位置,不会另外占用磁盘空间,这样不同项目之间就可以共享相同版本的依赖。
如果对同一依赖包使用相同的版本
,那么磁盘上只有这个依赖包的一份文件,如果对同一依赖包使用不同的版本
,那么只有版本之间不同的文件
被存储起来。
比如 a/b/c 三个项目都使用 axios,axios 的所有文件都保存在 pnpm 上,axios 这些文件对应着磁盘的数据,直接 a/b/c 项目的axios 通过硬链接指向磁盘里的数据。 这样有两个好处:
(1)效率非常高,无需下载、查找缓存解压等操作
(2)节省磁盘空间,每个项目不需要再下载一份
pnpm 依赖包统一保存的位置可以使用命令 pnpm store path
来查看
非扁平的 node_modules 目录
使用 npm 或者 yarn安装的依赖包会将所有的子级依赖全部平铺
到 node_modules 文件夹中,即扁平化的目录结构,这样会导致源码可以访问本来不属于当前项目所设定的依赖包。
比如安装 axios ,同时会安装非常多的其它的库如 form-data,虽然在 package.json 中是没有配置的,但在源代码中可以直接通过require('form-data') 引用
,这样就会有隐患,如果项目某天删除了 axios,form-data 就不存在了。
使用 npm 和 pnpm 分别只安装 axios,npm 会将 axios 所需的其它依赖平铺,而 pnpm 的 node_modules 根目录下只有 axios 和 .pnpm 文件夹,这样就可以避免非主动下载的其它依赖包可随意访问的情况。
如果直接按照这样的层级下载包,可能会带来新的问题,如多个包依赖同一个包时,就会被重复安装。
▾ node_modules▾ axios▾ node_modules▸ form-data▾ xxx▾ node_modules▸ form-data
那 pnpm 是如何做到非扁平化并且不重复安装的呢?答案就是它使用硬链接与软连接结合的方式来与依赖包关联。
在 node_modules 根目录有一个文件夹 .pnpm,这里包含了项目所有依赖。
根目录下 axios 软连接
到 .pnpm 目录下的 axios 文件夹中,展开 .pnpm/axios@16.1 的node_modules 文件夹,其中有 axios 所需的依赖,包含 axios、follow-redirects、form-data、proxy-from-env,其中 axios 硬链接
到磁盘中(即与 pnpm 仓库保存的地址一致),其它文件软连接到 .pnpm 的自身位置。
node_modules 根目录下的依赖
,软连接到 .pnpm 文件夹中,如果有相互依赖的关系,仍然通过软连接,只有找到依赖自身,才会通过硬链接找到磁盘中的位置,这样可以保证同一个项目里不同依赖也不会重复安装,同时不同项目之间的相同依赖也无需在磁盘中存储多份。