掘金
随着 pnpm 的逐渐崭露头角,越来越多的开发者和项目开始倾向于采用 pnpm 作为其包管理工具,以期利用 pnpm 的独特优势来解决 npm 在处理大型、复杂项目时可能遇到的一些问题。特别是在 Vue 3 及其生态中的许多包迁移使用pnpm做完包管理器后,这个趋势变得更为明显。pnpm 通过其独特的包管理策略和硬链接、软链接技术,以及三层寻址策略,显著减少了冗余数据,并提升了依赖解析的效率。同时,在 Node.js 环境中,理解如何处理依赖引用的逻辑是至关重要的。随着时间的推移,npm 也从简单的依赖解析演变为现今更为成熟和复杂的依赖管理系统。下面,我们将探讨 pnpm 的包管理策略、Node.js 在处理依赖引用时的逻辑、npm 的依赖历史、硬链接与软链接的原理,以及全局 pnpm-store 的组织方式。
一、pnpm的包管理策略带来的优点
- 磁盘空间效率:
pnpm
使用一个公共内容地址able的存储区域来存放所有的包。每个版本的每个包在磁盘上只存储一次。如果多个项目使用相同的包和版本,pnpm
只需使用符号链接(或硬链接,取决于系统和配置)来链接到公共存储位置。这种方法节省了大量磁盘空间,而npm 7
会在每个项目的node_modules
文件夹中独立存储每个包的副本。 - 安装速度:
由于pnpm
使用符号链接,所以它的安装速度通常更快,尤其是在多个项目使用相同的依赖时。 - 更强的隔离性:
pnpm
的node_modules
结构确保了包只能访问其声明的依赖,这提供了更强的隔离性,能更早地捕获到未声明的依赖问题,而npm
的扁平结构可能会隐藏这类问题,造成幻影依赖。
二、Node.js 在处理依赖引用时的逻辑
这里先简要描述下Node.js 在处理依赖引用时的逻辑,当使用 require()
函数导入模块时,Node.js 的解析方式如下:
-
如果参数指向的是核心模块(如“fs”或“path”)或是一个绝对或相对的文件路径(例如
./packageA.js
或/myLib/packageB.js
),Node.js 将直接加载相应的文件或模块。 -
对于其他情况,Node.js 会启动对
node_modules
目录的搜索流程:- 首先,Node.js 会在当前的工作目录下查找
node_modules
。 - 如果在当前目录未找到,它会逐级向上移动到父目录进行查找。
- 这一过程会持续进行,直到达到系统的根目录。
- 首先,Node.js 会在当前的工作目录下查找
-
一旦找到
node_modules
目录,Node.js 会在目录中查找具有指定模块名的.js
文件,或者一个与模块名同名的子目录。
三、 npm的依赖历史
从npm的诞生开始,其嵌套的node_modules
目录结构旨在解决版本冲突,确保每个项目都有特定版本的依赖。但这也导致了大量的磁盘空间浪费。在 npm1、npm2 中呈现出的是嵌套结构,类似下面这样:
(packageA 依赖 packageB 依赖 packageN…)
```
node_modules
└─ packageA
├─ index.js
├─ package.json
└─ node_modules
└─ packageB
├─ index.js
└─ package.json
└─ node_modules
└─ ...
```
是以依赖包之间互相嵌套的方式存储的,这样最明显的一个问题就是依赖的层级有可能很深,包A
依赖 包B
依赖 包C
…,这会造成文件路径长度过长的问题,另外还会磁盘空间的浪费:相同的包可能被多次安装在不同的嵌套级别上,导致了不必要的重复和空间的浪费。结合上面说的node.js的解析依赖的方式在层级过深的时候要进行大量文件的 I/O 操作,效率不高。
当npm进入其第三个主要版本(npm@3)时,它引入了一个显著的变化,即扁平化的依赖管理。包括 yarn,都着手来通过扁平化依赖的方式来避免前两个版本中常见的嵌套node_modules结构。
上面图片里只安装了一个 axios 作为项目的依赖,但是node_modules里却多出了很多其他的包。这就是扁平化依赖管理的结果。相比之前的嵌套结构,现在的目录结构类似下面这样: