pnpm包管理机制理解
上图是pnpm官网的🌰,在项目根目录安装依赖包bar,而bar依赖了foo。点开项目根目录的node_modules,可以看到我们在项目中安装的bar,以及.pnpm文件夹。 那么pnpm是如何维护依赖的嵌套关系的呢?
- 首先在.pnpm文件夹以平铺的方式存储了项目中所有的依赖包,建立硬链接。命名的方式如下:
.pnpm/<organization-name>+<package-name>@<version>/node_modules/<name>
// 组织名(若无会省略)+包名@版本号/node_modules/名称(项目名称)
其中.pnpm中的包中的node_modules下面的包中的每个文件都是内容可寻址存储的硬链接。将foo和bar都放到node_modules文件夹,可以保证包能自行导入自己。
node_modules
└── .pnpm
├── foo@1.0.0
│ └── node_modules
│ └── foo -> 硬链接 到<store>/bar
│ ├── index.js
│ └── package.json
└── bar@1.0.0
└── node_modules
└── bar -> 硬链接 到<store>/bar
├── index.js
└── package.json
- 建立符号链接,由于bar依赖了foo,因此foo被符号链接到bar@1.0.0/node_modules文件夹下,
node_modules
└── .pnpm
├── foo@1.0.0
│ └── node_modules
│ └── foo -> 硬链接 到 <store>/bar
│ ├── index.js
│ └── package.json
└── bar@1.0.0
└── node_modules
└── bar -> 硬链接 到 <store>/bar
│ ├── index.js
│ └── package.json
└── foo -> 符号链接 到 .pnpm/foo@1.0.0
- 处理直接依赖,bar被符号链接到根目录的node_modules文件夹
node_modules
├── bar -> 符号链接 到 ./.pnpm/bar@1.0.0/node_modules/bar
└── .pnpm
├── foo@1.0.0
│ └── node_modules
│ └── foo -> 硬链接 到 <store>/bar
│ ├── index.js
│ └── package.json
└── bar@1.0.0
└── node_modules
└── bar -> 硬链接 到 <store>/bar
│ ├── index.js
│ └── package.json
└── foo -> 符号链接 到 .pnpm/foo@1.0.0
- pnpm的包管理机制符合node寻址机制
nodejs的寻址方式:
对于核心模块(core module) => 绝对路径 寻址
node标准库 => 相对路径寻址
第三方库(通过npm安装)到node_modules下的库(可以在node环境中输入module.paths查看):
3.1. 先在当前路径下,寻找 currentProject/node_modules/xxx
3.2 递归从下往上,到上级路径寻找,例如 ../node_modules/xxx
3.3 循环步骤3.2
3.4 在全局环境路径下寻找,例如 .node_modules/xxx
3.5 在用户目录下寻找,例如 users/Daming/.node_modules/xxx 或者 users/Daming/node_libraries/xxx
3.6 node安装目录下查找,例如 nodejs/lib/node/.node_modules/xxx
在项目中我们引入bar的时候,会在当前项目根目录的node_modules找bar,从projectRoot/node_modules/bar这个符号链接找到bar的真实文件projectRoot/node_modules/.pnpm/bar@1.0.0/node_modules/bar(硬链接即可看做真实存在的文件)。
接着bar依赖了foo,根据node的寻址机制,会从里到外找foo,最后在这个路径projectRoot/node_modules/.pnpm/bar@1.0.0/node_modules/foo找到了,这个是一个符号链接,会定位到projectRoot/node_modules/.pnpm/foo@1.0.0
pnpm优势
解决了幽灵依赖的问题
yarn和npm中的幽灵依赖:由于在yarn或npm中采用扁平化的安装包的方式,即所有的包(包括依赖的依赖)都被提升到了项目根目录中的node_modules中,那么项目中可以直接引用到不在package.json中声明的包,那么在包更新的时候,可能会导致问题。
而pnmp中的项目根目录的node_modules中只有项目直接依赖的包,所以不会有这个问题。
路径过长的问题
在 npm@3 之前,npm采用的嵌套结构管理包,node_modules结构是干净、可预测的,因为node_modules 中的每个依赖项都有自己的node_modules文件夹。但是这样的管理方式会导致路径过程超出window的限制。 在npm@3和yarn中,采用了扁平化结构管理包(解决了路径过长的问题,导致了幽灵依赖的问题)。
pnpm利用符号链接解决了这个问题。
继续上面的🌰
添加qar@2.0.0作为foo和bar的依赖项
node_modules
├── bar -> ./.pnpm/bar@1.0.0/node_modules/bar 符号链接
└── .pnpm
├── foo@1.0.0
│ └── node_modules
│ └── foo -> 硬链接 到 <store>/bar
│ ├── index.js
│ └── package.json
│ └── qar -> 符号链接 到 ../../qar@2.0.0/node_modules/qar
└── bar@1.0.0
│ └── node_modules
│ └── bar -> 硬链接 到 <store>/bar
│ │ ├── index.js
│ │ └── package.json
│ └── foo -> 符号链接 到 .pnpm/foo@1.0.0
│ └── qar -> 符号链接 到 ../../qar@2.0.0/node_modules/qar
└── qar@2.0.0
└── node_modules
└── qar -> <store>/qar
可以看到我们目录深度没有改变,但却表达了嵌套的包依赖关系。
占用更小的内存,下载速度更快
使用 npm 时,依赖每次被不同的项目使用,都会重复安装一次。 而在使用 pnpm 时,依赖会被存储在内容可寻址的存储中,所以:
如果你用到了某依赖项的不同版本,只会将不同版本间有差异的文件添加到仓库。 例如,如果某个包有100个文件,而它的新版本只改变了其中1个文件。那么 pnpm update 时只会向存储中心额外添加1个新文件,而不会因为仅仅一个文件的改变复制整新版本包的内容。
所有文件都会存储在硬盘上的某一位置。 当软件包被被安装时,包里的文件会硬链接到这一位置,而不会占用额外的磁盘空间。 这允许你跨项目地共享同一版本的依赖。
因此,您在磁盘上节省了大量空间,这与项目和依赖项的数量成正比,并且安装速度要快得多!
参考
关于现代包管理器的深度思考——为什么现在我更推荐 pnpm 而不是 npm/yarn?