当前位置: 首页 > 科技观察

pnpm是前端工程项目的未来

时间:2023-03-17 19:26:29 科技观察

前言相信小伙伴们都接触过npm/yarn。这两个包管理工具一定是你工作中使用最多的包管理工具了。npm作为node官方的包管理工具,随着node的诞生出现在大家的视野中,而yarn的出现就是为了解决npm带来的诸多问题,虽然yarn提升了依赖包的安装速度和使用体验,但还是没有解决重复安装npm依赖等致命问题。“pnpm”的出现完美解决了重复安装依赖包的问题,??实现了yarn带来的所有优秀体验,所以说“pnpm是前端工程项目的未来”。npm和yarn的问题早期的npm在npm@3之前,node_modules的结构可以说是工整可期,因为那时候的依赖结构是这样的:node_modules└─依赖A├─index.js├─package.json└─node_modules└─依赖B├─index.js└─package.json└─依赖C├─index.js├─package.json└─node_modules└─依赖B├─index.js└─包。json的每个依赖都维护着自己的node_modules,看起来确实很整洁,但是也带来了一些严重的问题:重复安装依赖包,依赖层级太多。模块实例不能共享依赖包。从上面重复安装依赖从结构上我们可以看出,依赖A和依赖C同时引用了依赖B,此时依赖B会被下载两次。此时我们认为如果某个依赖被引用了n次,那么就需要下载n次。(此时此刻你是不是很纳闷,怎么会有这么糟糕的设计)我们再来看一个依赖过多的依赖结构:node_modules└─DependencyA├─index.js├─package.json└─node_modules└─依赖B├─index.js├─package.json└─node_modules└─依赖C├─index.js├─package.json└─node_modules└─依赖D├─index.js└─package.json这种依赖hierarchy少是可以接受的,但是如果依赖的层数多了,一层层嵌套就像依赖地狱,不利于维护。为了解决以上问题,npm@3和yarn选择了扁平结构,也就是我们现在看到的node_modules中的结构不再有依赖嵌套,都是依赖结构如下:node_modules└─DependencyA├─index.js├─package.json└─node_modules└─依赖C├─index.js├─package.json└─node_modules└─依赖B├─index.js├─package.json└─node_modulesnode_modules下面的所有依赖将被压平到同一水平。由于require找包的机制,如果A和C都依赖B,那么A和C在自己的node_modules中都没有找到依赖的C时会向上查找,最后同时在自己的node_modules中找到依赖的包C等级。这样就不会出现重复下载的情况。而且依赖层级嵌套不会太深。因为没有重复下载,所以A和C都会去寻找和依赖同一个B包。自然也就解决了实例无法共享数据的问题。由于这种扁平结构的特点,想必大家也有过这样的经历。我明明只安装了一个依赖包。当我打开node_modules文件夹时,里面有很多。这种扁平化结构虽然解决了之前的嵌套问题,但同时也带来了一些其他问题:依赖结构的不确定性增加了扁平化算法的复杂度,未声明的依赖包在项目中仍然可以被非法访问(幽灵依赖)如何理解依赖结构的不确定性,为什么会出现这种问题呢?我们仔细想想,添加如下依赖结构:A包和B包同时依赖C包的不同版本,由于同一个目录下不能出现两个同名文件,所以本例中只有一个文件可以存在同级版本的包中,另一个版本仍然需要嵌套。那么问题又来了,既然一个要展平,一个要嵌套,那么这时候怎么判断哪个展平,哪个嵌套呢?两种配置都是可能的,具体升级哪个版本的包取决于包的安装顺序!这就是为什么存在依赖结构不确定的问题,也是锁文件诞生的原因,无论是package-lock.json(npm5.x才出现)还是yarn.lock,都是为了确保在安装后生成定义的node_modules结构。尽管如此,npm/yarn本身仍然存在扁平化算法复杂、非法包访问等问题,影响性能和安全性。pnpm说了这么多npm和yarn的缺点,现在来看看pnpm是如何解决这些尴尬问题的。什么是pnpm?一个快速、节省磁盘空间的包管理工具就是这么简单。说白了,它和npm、yarn没什么区别,都是包管理工具。但它的独特之处在于:安装包速度极快,磁盘空间利用率非常高,安装包速度快。从上图可以看出,pnpm的包安装速度明显快于其他包管理工具。那么为什么它比其他包管理工具更快呢?我们先看看他们各自的安装过程npm/yarnresolving:首先他们会解析依赖树,决定去抓取哪些安装包。fetching:安装fetch依赖的tar包。这个阶段可以同时下载多个,以提高速度。Wrting:然后解压包并根据文件构建一个真正的依赖树。这个阶段需要大量的文件IO操作。pnpm的上图就是pnpm的安装过程。可以看到每个包的三个进程是并行的,所以速度会快很多。当然pnpm还会多一个阶段,就是通过链接来组织真正的依赖树目录结构。磁盘空间利用率非常高。pnpm内部使用基于内容的寻址文件系统将所有文件存储在磁盘上。该文件系统的突出特点是不会重复安装同一个包。在使用npm/yarn的时候,如果有100个项目依赖lodash,那么lodash大概安装了100次,这部分代码写在了磁盘的100个地方。但是使用pnpm的时候只会安装一次,而且磁盘里面只有一个地方可以写,以后再用的时候直接hardlink。即使一个包有不同的版本,pnpm也会极大的复用之前版本的代码。比如lodash有100个文件,更新版本后又增加了一个文件,那么磁盘就不会重写101个文件,而是保留原来100个文件的hardlink,只写入新的一个文件。支持monorepopnpm和npm/yarn的另一个大区别是它支持monorepo。pnpm内置了对monorepo的支持。只需要在workspace的根目录下创建pnpm-workspace.yaml和.npmrc配置文件,同时还支持多个配置,相比lerna和yarnworkspace,pnpm不仅解决了monorepo,还解决了引入的问题通过传统的解决方案。monorepo的目的是用一个git仓库来管理多个子项目。所有的子项目都存放在根目录的packages目录下,所以一个子项目代表一个包。依赖管理pnpm使用类似于npm2.x版本的嵌套结构,并使用.pnpm以平面形式存储所有包。然后使用Store+Links关联文件资源。简单地说,pnpm将包下载到公共目录。如果sotre目录下存在某个依赖,会直接从store目录跳转到hard-link,避免二次安装带来的时间消耗。如果dependency在store目录下不存在,则会下载一次。通过Store+硬链接,项目中不存在npm依赖地狱问题,完美解决了npm3+和yarn中的包重复问题。下面分别使用npm和pnpm安装vite,对比一下,npmpnpm的所有依赖包都平铺在node_modules目录下,包括直接依赖包和其他二级依赖包。node_modules目录下只有.pnpm和直接依赖包,没有其他二次依赖包。没有直接依赖包的符号链接(软链接),后面有一个符号链接(软链接)。pnpm安装的vite的所有依赖都软链接到node_modules/.pnpm/中对应的目录下。将vite的依赖放在同一层级可以避免循环软链接。软链接和硬链接机制pnpm使用hardlink在全局设置一个store目录来存放node_modules依赖中的硬链接地址,然后在引用依赖时使用symlink找到对应的虚拟磁盘目录(.pnpm目录)依赖在地址上。两者协同工作后,如果一个项目依赖A@1.0.0和B@1.0.0,那么最终的node_modules结构体呈现的依赖结构可能是这样的:node_modules└──A└───B└──.pnpm├──A@1.0.0│└──node_modules│└──A->/A│├──index.js│└──package.json└──B@1.0.0└──node_modules└──B->/B├──index.js└──package.jsonnode_modules中的A、B两个目录会软链接到.pnpm目录下真正的Dependencies,而这些真正的依赖通过硬链接存储在全局存储目录中。Storepnpm下载的依赖都存放在store中,store是pnpm在硬盘上的公共存储空间。pnpm的store在Mac/linux下默认设置为{homedir}>/.pnpm-store/v3;它将被设置为windows当前盘符的根目录。使用名为.pnpm-store的文件夹名称。项目中.pnpm/dependencyname@versionnumber/node_modules/下的所有软链接都会连接到pnpmstore。