Hi! 这里是刚吃完鲜虾鱼板面的JustHappy,上一次我们聊了聊主流的npm软件包下载器,其中pnpm似乎是目前各方面最屌的一个,所以这次我们来深扒一下pnpm的实现原理,也就是pnpm是如何“杀死幽灵”以及“又快又省又稳的”,本文将会结合pnpm的官方文档进行解析
图片
可能pnpm的特性比较多,小弟能力暂时有限,本篇文章只会涉及部分pnpm特性,如果大家有兴趣,可以去官方文档查看pnpm中文文档 | pnpm中文网[1]
首先你得先搞明白“硬链接”和“软链接”
是吧!死去的回忆又回来了,这俩是否让你想起了逃过的计算机操作系统?没关系,我们再来回顾一下
大白话回顾基础概念
“硬链接(Hard Link)”
硬链接是咱文件系统中的一个数据实体,它是直接指向咱硬盘上的数据块的,也就是说硬链接就是目标文件的另一个名字,如果目标文件被删除,硬链接依然有效,这怎么说呢?因为咱执行删除目标文件这一操作只是删除了该目标文件在文件系统中的一个名字,只要目标文件的数据还有一个硬链接,那么这个数据就不会被真正的删除
“软链接 (Symbolic Link)”
软链接,又叫做符号链接,这个比硬链接要好理解些,windows的快捷方式大家都使用过吧,软链接就是一个指向文件或者目录(文件路径)的快捷方式,你删除快捷方式的时候原文件不会受影响
以下是我将一些特性汇总了一个表格
特性 | 硬链接 (Hard Link) | 软链接 (Symbolic Link) |
定义 | 一个指向文件inode的引用 | 一个指向文件路径的引用 |
目标 | 必须指向文件,不能指向目录 | 可以指向文件或目录 |
删除原文件 | 删除原文件不会连接硬链接 | 删除原文连接软链接变为“死链接” |
权限 | 硬链接和原文件共享相同的权限 | 软链接有自己的权限设置 |
空间占用 | 不占用额外空间(除了目录项) | 占用少量空间来存储链接目标路径 |
移动/重命名 | 移动或重命名原文件会影响硬链接 | 移动或重命名原文件不会影响软链接,但软链接会指向错误路径 |
我们先来看看使用pnpm生成的node_modules是什么样的
图片
如果你仔细比对,会发现node_modules的一级目录下所出现的包都是在项目的package.json中已经被声明的
我们还看到相比于使用npm构建的node_module,这里多了个 .pnpm文件夹,这其实是node_modules中存放包硬链接的地方
.pnpm 目录下的文件结构可以参照下图
图片
注:点我去官方文档[2]
我们来对比npm画几张图吧
我们来回顾一下npm的node_module结构吧:
图片
可以看到这是一个扁平化的结构,这直接导致了幽灵依赖的出现
那么结合上面对pnpm的分析,我们可以初步得到以下结构(本人自己画的,如果难看的话望见谅哈):
图片
好吧,可能有些复杂,但是不慌,接下来我们结合这个图讲pnpm的三层寻址
pnpm的三层寻址
pnpm的三层寻址策略是其高效存储和依赖管理的核心机制,具体包括以下三个层面:
第一层:全局的 pnpm 存储
- 目的:允许跨项目共享依赖,进一步减少存储和下载的冗余。
- 原理:pnpm维护了一个全局的存储(通常位于用户的home目录下),在其中保存了所有下载的包的版本。这些版本被硬链接到项目的.pnpm目录。
这有什么好处呢?
这其实实现了一个 跨项目共享依赖 的目的,这意味着,不同的项目可以共享全局存储中的同一个物理文件,从而不需要重复下载或存储相同的文件。(极大了节省了下载依赖的时间)
第二层:项目级的 .pnpm 目录
- 目的:.pnpm文件夹内使用的是硬链接。这意味着每个包的文件都是硬链接到全局存储中的文件。这些硬链接为单个项目提供一个集中的地方来存储其所有依赖的软链接(或符号链接),以减少重复并确保稳定的包结构。
- 原理:每个项目中的.pnpm目录链接到全局pnpm存储中的依赖版本。项目的node_modules目录中的每个依赖实际上都是指向这个.pnpm目录中的相应版本的软链接。
.pnpm的作用
这确保了项目内的node_modules可以维持一个干净和结构化的布局,而真正的包文件都存储在全局存储中,并通过项目级的.pnpm目录链接。
第三层:本地 node_modules
- 目的:维持项目结构的语义性,提供一个确定性的依赖解析方式。
- 原理:每个项目的node_modules目录中的直接依赖都被组织成与package.json中声明的结构相匹配的方式。这遵循了Node.js的模块解析逻辑,确保每个依赖都能被正确地找到。
我们得到了一个更加接近Node.js模块解析逻辑的目录结构,并解决了“幽灵依赖”
幽灵依赖(Phantom Dependency)是指在项目中使用了某些依赖包,但这些包没有显式地声明在项目的依赖列表(如 package.json 文件)中。这种依赖的存在通常是因为它被其他依赖的依赖(即间接依赖)提供,而不是项目直接安装的。
到此,我们得到了一个接近Node.js模块解析逻辑的 “非扁平的” node_modules结构
可能有些混乱,我们再来总结一遍
总结!为什么说!又快!又省!又稳!
有关于“快”
- pnpm拥有一个全局的存储空间,所有的npm包都存储在这个位置。这意味着,无论我们有多少个项目,对于同一个npm包,我们只需要下载一次并存储一次在全局pnpm存储中。这样,当我们在不同项目中需要同一个包时,pnpm可以直接从全局存储中链接,而不需要重复下载。
- 在每个项目的node_modules目录中,pnpm使用软链接(符号链接)或硬链接来引用全局存储中的包。这些链接指向全局存储中的实际文件,因此,无论在哪个项目中,我们都能快速访问到这些包,而不需要等待重复的下载过程。
- 由于pnpm避免了重复下载相同的包,它大大减少了网络请求和磁盘I/O操作,这在网络速度较慢或磁盘I/O性能有限的环境中尤其有用。
- 在安装新项目依赖时,pnpm首先检查全局存储中是否已经存在所需的包。如果存在,它将直接链接到项目中,而不是重新下载,这大大加快了安装速度。
- pnpm支持并行安装,这意味着它可以同时下载多个包,而不是一个接一个地下载,这进一步提高了安装速度。
有关于“省”
上面提到,pnpm有一个全局的pnpm存储,所有的项目中软链接、硬链接都是链接这里面的npm包这意味这在我们的电脑上对于同一个npm包我们只需要下载一次、存储一次在我们全局的pnpm存储中,从而实现了对磁盘空间的”省“,不像之前使用npm或者yarn的时候,将所有的依赖安装到项目文件夹的node_modules下,从而导致了在我们电脑上多次、重复的下载。
有关于“稳”
- 基于链接的node_modules结构:pnpm通过软链接将项目的依赖直接链接到虚拟store下对应包的版本下,然后虚拟store对应版本会直接硬链接到全局的pnpm store下。这种布局的一大好处是只有真正在依赖项中的包才能访问,避免了幽灵依赖问题。
- 严格的依赖平面:pnpm为每个包提供独立的依赖视图,减少了不必要的包冗余。这种严格的控制有助于防止版本冲突,确保每个包的依赖关系都被满足,减少了因版本不匹配导致的问题。
文章作者:JustHappy
原文地址:https://juejin.cn/post/7443866293755592742