前言
pnpm(performant npm,意思是高性能的 npm)是 Node.js 的包管理器。它是 npm 的直接替代品,相对于npm和yarn它的优点就在于速度快和高效节省磁盘空间。
本文主要讲解pnpm相比于npm/yarn如何利用软硬链接来节省磁盘空间,以及如何实现依赖包共享和依赖包项目隔离的。
硬链接
只能引用同一文件系统中的文件。硬链接引用的是文件在文件系统中的 物理索引(inode) 而inode存储了文件的元数据和指向文件数据的指针。当移动或者删除原始文件时,硬链接不会被破坏,因为它所引用的是文件的inode而不是文件在文件结构中的位置,删除或移动原始文件只是改变了文件的结构。硬链接记录的是目标的inode。同一文件的不同硬链接文件相当于该文件的多个不同文件名,即多个不同访问路径,他们的inode都是一样的。删除一个硬链接并不会删除文件的数据,除非这是指向该文件的最后一个硬链接。只有当所有指向文件的硬链接都被删除后,该文件才会被标记为可以删除,并在适当的时候被文件系统清理。
不可以为目录创建硬链接。因为这可能会造成循环引用的问题,假如目录A硬链接到目录B,而目录B内又包含了对目录A的引用(可能是直接引用或者间接引用),那么就会形成一个循环。在遍历目录树时,系统可能会陷入这个无限循环中,导致无法正确定位到要访问的目录。
如何创建硬链接呢?
// 初始文件为file ln file hardfile // linux环境下创建file文件的硬链接 ln -ls // 查看文件信息
可以看到硬链接的文件和原始文件的inode值相同。
删除原始文件后还可以访问硬链接的文件:
rm file cat hardfile // 查看文件内容 ls -li 查看该文件夹下所有文件的inode值
软链接(符号链接)
和原文件不是同一个文件,符号链接会有自己的inode,它所引用的是原文件的path,当原文件被移动或删除的时候,符号链接的文件也会失效。例如windows中的快捷方式就是一种软链接。也可以为目录创建软连接。
ln -s file sysfile // linux环境下创建file文件的软连接 ls -li
可以看到软连接文件跟原始文件的inode值不同。
删除原始文件后不能访问软链接的文件:
rm file cat sysfile
高效的节省磁盘空间
npm和yarn存在的问题
用 npm/yarn 的时候,如果 100 个项目都依赖 lodash,那么 lodash 很可能就被安装了 100 次,磁盘中就有 100 个地方写入了这部分代码,这就会浪费大量的磁盘空间。比较好的办法就是每个包只在全局存储一份,其它项目在使用时都通过引用去链接到这个包的地址,pnpm就是通过 内部使用基于内容寻址的文件系统来存储磁盘上所有的文件来实现节省空间的。
pnpm安装依赖结构
我们借助pnpm官方给的例子进行讲解。假如我们的项目依赖了foo包,而foo包又依赖了bar包,那使用pnpm i的整个安装流程是怎样的,node_modules目录的结构又是怎样的?
当执行pnpm i后首先会将依赖包下载到pnpm配置的本地仓库里边,可以使用pnpm config get store-dir命令查看,默认是在用户主文件夹中,可以通过pnpm config set store-dir ×××来手动设置包下载后存放的目录。下载到本地仓库后,会在项目中的node_modules文件夹中创建一个 .pnpm 文件夹,这个文件夹会将所有的依赖以及依赖的子依赖铺平通过硬链接的方式引入进来。
-> 表示硬链接 --> 表示符号链接(软链接)
node_modules └── .pnpm ├── bar@1.0.0 │ └── node_modules │ └── bar -> <store-dir>/bar // 硬链接到本地store-dir仓库中的bar文件 │ ├── index.js │ └── package.json └── foo@1.0.0 └── node_modules └── foo -> <store-dir>/foo // 硬链接到本地store-dir仓库中的foo文件 ├── index.js └── package.json
它们是node_modules中唯一真实的文件。当所有的依赖(包括依赖的子依赖)都硬链接到node_modules文件夹中的.pnpm目录后就开始创建符号链接去构建嵌套的依赖结构。也就是在本例中的foo会依赖bar,那么就会在foo+@+版本号目录下的node_modules目录下创建bar依赖的符号链接去构建嵌套结构。
node_modules └── .pnpm ├── bar@1.0.0 │ └── node_modules │ └── bar -> <store-dir>/bar └── foo@1.0.0 └── node_modules ├── foo -> <store-dir>/foo └── bar --> ../../bar@1.0.0/node_modules/bar // 将嵌套依赖通过符号链接连接到真实的文件中
接下来就是去处理项目的直接依赖关系了,比如本例项目直接依赖了foo包
node_modules ├── foo --> ./.pnpm/foo@1.0.0/node_modules/foo 软链接到.pnpm目录下的真实文件 └── .pnpm ├── bar@1.0.0 │ └── node_modules │ └── bar -> <store-dir>/bar └── foo@1.0.0 └── node_modules ├── foo -> <store-dir>/foo └── bar --> ../../bar@1.0.0/node_modules/bar
可以看到.pnpm目录中每个依赖包的目录结构都是固定的,每个依赖真实的文件(也就是该依赖的硬链接)都会放在一个包名+@+版本号的目录的node_modules下,而该依赖的所有子依赖又通过符号链接放在同一层级下,这样做的一个好处就是依赖包可以在代码中引用自己。比如foo依赖包中的代码中可以使用 const foo = require('foo') 这是没问题的,因为node查找依赖时就是通过node_modules一层一层去找到。
这种目录结构的另一个好处就是可以避免npm/yarn存在的幽灵依赖问题,在根目录的node_modules文件夹中只存在项目的直接依赖包的符号链接,当项目中的代码使用这个包时会根据符号链接引用的地址找到.pnpm目录中的真实文件。
验证一下,根目录的node_modules文件夹中项目的直接依赖包的符号链接是指向.pnpm中的真实文件的
拿express包举例,我们先在.pnpm目录中找到express包的真实文件,然后再添加一行打印语句,那么当根目录的node_modules文件夹中express的符号链接中的文件也相应改变就可以验证上面的结论。
express文件夹后边的一个箭头符号就表示该文件夹是一个符号链接,其实就是真实文件的一个快捷方式,当真实文件里边的内容改变了,快捷方式内的文件内容肯定也跟着改变了。
因为pnpm是通过硬链接和符号链接去管理node_modules文件夹的,而不是像npm和yarn那样直接将依赖包复制到node_modules中,所以可以高效的节省磁盘空间。
依赖共享
因为pnpm再安装依赖包时是全局仓库store-dir内依赖包一个硬链接,那么其它的项目也都是对全局依赖中原始依赖包的一个硬链接,真实的文件只在磁盘中保留了一份,避免了多个项目带来多份相同文件引起的空间浪费问题。这就是多个项目中的依赖共享。
依赖包的项目隔离
但是说到硬链接,又有一个问题,这相当于所有项目都依赖了同一个原始文件,那么在一个项目中修改了某个npm包的文件,就会影响到其他项目。但是我自己试了一下,开了两个项目都依赖了express包,再其中一个项目修改后并不会影响到另外一个项目。经过查阅,pnpm是使用了copy-on-write策略来解决这个问题的,实现依赖包的项目隔离。
copy-on-write(写时复制)是一种优化策略。这种策略的核心思想是在首次安装或更新包时,尽量使用硬链接和符号链接来引用全局存储库(store-dir)中的文件,以节省磁盘空间和提高安装速度。但是,当需要修改某个文件时(例如,需要打log去调试依赖包中的文件),
pnpm
不会直接修改硬链接或符号链接引用的原始文件,而是会创建一个该文件的副本,并将这个副本放入项目的node_modules
目录中,相当于修改的是原始文件的副本。这种做法的好处是避免了多个项目之间因为共享文件而产生的潜在冲突。每个项目都有其自己的node_modules
目录,其中包含它需要的所有依赖项和可能的文件副本,这些副本是独立于其他项目的。这样,即使一个项目更新了某个包,也不会影响到其他项目。
以上就是pnpm实现依赖包共享和依赖包项目隔离的方法详解的详细内容,更多关于pnpm依赖包隔离和共享的资料请关注其它相关文章!