pnpm 模块原理
原理
显著的优点?
- 省磁盘空间,使用软硬链接和符号链接节约空间。
- 安装依赖快,因为处理链接要比处理文件快,并且每个包的解析、下载和写入磁盘这三个阶段是分离的。
- 内建支持 monorepo,所有命令支持
--filter(缩写-F)过滤包。 - 依赖严格,避免幽灵依赖和依赖分身问题。
依赖的层次结构是怎样的?
当项目文件读取 bar 时,直接读取 node_modules/bar,但它其实是 .pnpm 文件夹下 bar/node_modules/bar 的一个符号链接。也就是说,他的真实的地址在 .pnpm 文件夹下。
这样一来,当 bar 读取它的依赖的时候(项目的依赖的依赖),会在 bar 的上层目录即 bar/node_modules 找到 foo 这个依赖。
项目文件没法读取 bar/node_modules/foo 所以避免了幽灵依赖问题。
bar/node_modules/foo 要怎么找到它的依赖呢?它的真实地址在项目 ·(和官网的图有出入),所以 foo 也只能读取它自己的子依赖,即 node_modules/.pnpm/foo/node_modules。
如果有依赖依赖了同一份(同一版本)的 foo,那么它们的真实地址都是一样的 node_modules/.pnpm/foo/node_modules/foo,这样就避免依赖分身问题。

见:Flat node_modules is not the only way | @pnpm、Symlinked node_modules structure | @pnpm
如何处理同级依赖?
当项目的两个包 A、B 依赖依赖了同一版本的 C,但是却安装了不同版本的 C 的同级依赖 D@1.0 和 D@1.1,这时,A 依赖的 C 的真实地址是 .pnpm/C_D@1.0/node_modules/C,
B 依赖的 C 的真实地址是 .pnpm/C_D@1.1/node_modules/C。这样一来,两个 C 在读取其同级依赖时,
分别读取了 .pnpm/C_D@1.0/node_modules/D 和 .pnpm/C_D@1.1/node_modules/D@1.0,
分别对应真实地址 .pnpm/D@1.0/node_modules/D 和 .pnpm/D@1.1/node_modules/D。
当同级依赖的深度增加时,层级结构也随之变得复杂。如果某个依赖 A 依赖了同一个 B,但是 B 子依赖 C 的版本不同(C@1.0 和 C@1.1),这样就创造出了指数级增长的软链数量。为了保证 B 能分别访问两个 C,
B 的符号链接的被链接地址会有 .pnpm/B_C@1.0/node_modules/B 和 .pnpm/B_C@1.1/node_modules/B,而为此,A 的被链接地址也会有两个,
.pnpm/A_C@1.0/node_modules/A 和 .pnpm/A_C@1.0/node_modules/A。从不同的 A 的被链接地址的上级目录 node_modules 可以读取到不同的 B 的被链目录,
从不同的 B 可以读取到不同的 C。
node_modules
└── .pnpm
├── a@1.0.0_c@1.0.0
│ └── node_modules
│ ├── a
│ └── b -> ../../b@1.0.0_c@1.0.0/node_modules/b
├── a@1.0.0_c@1.1.0
│ └── node_modules
│ ├── a
│ └── b -> ../../b@1.0.0_c@1.1.0/node_modules/b
├── b@1.0.0_c@1.0.0
│ └── node_modules
│ ├── b
│ └── c -> ../../c@1.0.0/node_modules/c
├── b@1.0.0_c@1.1.0
│ └── node_modules
│ ├── b
│ └── c -> ../../c@1.1.0/node_modules/c
├── c@1.0.0
├── c@1.1.0
见:How peers are resolved | @pnpm
怎样兼容 NodeJS 模块加载顺序?
不论系统自带的模块,NodeJS 加载模块的顺序是这样的:
- 读取本文件夹中 node_modules 中对应名字的文件夹的 package.json,并寻找 main 字段对应的路径
- 读取本文件夹中 node_modules 中对应名字的 js 文件
- 读取本文件夹中 node_modules 中对应名字文件夹中的 index.js 文件
- 跳出本文件夹,继续前三个步骤
因为项目 node_modules/bar 是 .pnpm/bar/node_modules/bar 的符号链接,所以项目文件可以直接读取 node_modules/bar/x.js。同时,
如果 node_modules/bar/x.js 需要加载 foo/x.js,那么在上述步骤的第 4 步,就能找到 .pnpm/bar/node_modules/foo/x.js 文件。
为什么其模块层次只是“比较严格的”(semistrict)?
由 NodeJS 模块加载规则可知,在 pnpm 创造的依赖层次下,一个依赖的子依赖间仍有相互访问的能力。
为什么能节约磁盘空间?
由 pnpm 创造的 node_modules 层级结构可以知道,所有依赖的符号链接的真实地址都在 .pnpm/package-name/node_modules/package-name 这个文件夹中。
这种文件夹会通过硬链接的形式链接到 user-document/.pnpm-store 中,所以相同的包只会存一份,也就是 .pnpm-store/package-name。
软硬链接有什么问题?
- 特定的软链结构会导致某些应用出现死循环
- 硬链接是同一份文件,不便调试
- 软链接在非 SSD 上的读写会有性能损耗
- pnpm 创造的 node_modules 层级结构会破坏某些依赖依赖了相对路径的依赖的逻辑
见:精读 pnpm