npm与pnpm的拓扑结构
# 0.前言
本篇博客主要讲的是:不同包管理工具形成node_modules拓扑结构的变化。
在这一节就可以明显的发现最近大火的pnpm相比于前代包管理(npm/yarn)工具上在资源空间上的优势,创造性的使用hardlink和softlink来链接module与module之间的引用。
此外,讲解pnpm的时候,需要有软链接和硬链接的前提知识,这个可以看我的另一篇文章《软链接与硬链接》。
# 1.module 的查找方式:向上查找
当 require('package-hello') 时,假设 package-hello 是一个 npm 库,我们是如何找到该 package 的?
- 寻找当前目录的
node_modules/package-hello目录 - 如果未找到,寻找上一级的
../node_modules/package-hello目录,以此递归查找。
# 2.npm2 的依赖结构
在 npmv2 时,node_modules 对于各个 package 的拓扑为嵌套结构。
假设:
- 项目依赖
package-a与package-b两个 package package-a与package-b均依赖lodash@4.17.4
依赖关系以 Markdown 列表表示:
+ package-a
+ `lodash@4.17.4`
+ package-b
+ `lodash@4.17.4`
2
3
4
此时 node_modules 目录结构如下:
此时最大的问题:
- 嵌套过深。
- 占用空间过大。
# 3.npm3 的依赖结构
目前在 npm/yarn 中仍然为平铺结构,但 pnpm 使用了更省空间的方法,以后将会提到。
扁平/平铺的含义:第三方依赖和第三方依赖的依赖会被安装在同级。
在 npmv3 之后 node_modules 为平铺结构,拓扑结构如下:
问题:以下依赖最终node_modules的结果是什么?
依赖关系以 Markdown 列表表示:
+ package-a
+ `lodash@^4.17.4`
+ package-b
+ `lodash@^4.16.1`
2
3
4
答: 与上图所示的拓扑结构一致,因为二者为 ^ 版本号,他们均会下载匹配该版本号范围的最新版本,即 @4.17.4,因此二者依赖一致。
如果考虑 package.lock.json文件的话,会严格按照package.json中指定的版本规则,此时拓扑结构会变成:
此时,npm3拓扑结构的问题就差不多出现了,即当存在两个相同module,而不同version时。
即,当第一个同名的module会被扁平的安装在与dependency同级的目录,其余version的module则会和npm2一样嵌套的安装在依赖下方。
如,假设存在依赖:
+ package-a
+ `lodash@4.0.0`
+ package-b
+ `lodash@4.0.0`
+ package-c
+ `lodash@3.0.0`
+ package-d
+ `lodash@3.0.0`
2
3
4
5
6
7
8
答:package-d 只能从自身的 node_modules 下寻找 lodash@3.0.0,而无法从 package-c 下寻找,此时 lodash@3.0.0 不可避免地会被安装两次
node_modules 目录结构如下图:
# 4.pnpm的依赖结构
那么不可避免地在 npm 或者 yarn 中,lodash@3.0.0 会被多次安装,无疑造成了空间的浪费与诸多问题。
这是一个较为常见的场景,在平时项目中有些库相同版本甚至会安装七八次,如 postcss、ansi-styles、ansi-regex、braces 等。
而在 pnpm 中,它改变了 npm/yarn 的目录结构,采用软链接+硬链接的方式,避免了 doppelgangers 问题,更加节省空间。
假设在package.json中存在以下依赖:
+ bar@1.0.0
+ bar
+ `foo@1.0.0` #(且foo之前已经被别的依赖在同级安装过了)
2
3
pnpm的目录拓扑结构如下所示:

pnpm在安装bar依赖时,会在node_modules目录下会生成两个文件:
# 1. node.js正常寻找的目录
|-- node_modules/bar # 全部存放着软链接
# 2. modulde真正存放的位置
|-- node_modules/.pnpm/bar@1.0.0/node_modules/A # 硬链接
2
3
4
5
从图中可以发现,当 foo 遇到其余同名其他 version 的依赖时,会把多个version 版本的module安装在.pnpm这一层级上,并以hardlink的方式与真实硬件存放的.pnpm store链接在一起,如图中的foo@1.0.0、foo@2.0.0。
当发现重复的依赖时,如foo@1.0.0,则会以软链接的方式与第一层的foo@1.0.0链接起来。
此时,会有同学存在一个问题,为啥嵌套的依赖不以
hardlink方式与.pnpm store链接起来呢?这部分我也不是很清楚,详见:https://pnpm.io/blog/2020/05/27/flat-node-modules (opens new window)-is-not-the-only-way (opens new window)