前端开发小伙伴在构建项目的时候肯定会接触到包管理工具,目前主流的包管理工具包括npm、yarn 和 pnpm。其中 pnpm 全名是 Performant npm,它相比于传统的 npm 在多个方面都存在优化和提升,例如空间占用、安装速度、依赖管理等,这里主要介绍一下 pnpm 对幽灵依赖问题的解决。
1. 幽灵依赖问题
当我们在一个空白项目中通过npm install express安装某一个包(例如 express)后,打开 node_modules 会发现安装的不止 express 一个包,而是存在几十个包,这是由于 express 本身也依赖了其他很多第三方包,因此在安装 express 时会同步将这些包都安装到 node_modules 中。可以看见npm将所有第三方包都平铺在了一级目录下。
在依赖没有变动的时候这并不是一个问题,但是当依赖变动时就会出现问题。假如我们在某一天不再依赖 express 包,我们删除 node_modules 并重新执行 npm install命令,之前由于安装 express 而以前装的其他第三方包都不会被安装,但是我们的代码中可能还存在着对它们的引用。因此当项目运行后就会发生报错。
问题产生的根本原因很容易理解,就是因为 npm 将所有的依赖包都进行了平铺。根据 Node.js 的模块解析规则--允许模块在其父目录以及祖先目录的 node_modules 目录中查找依赖,因此我们在文件中可以直接引用到任何一个安装的第三方包。yarn 也存在同样的问题。
- 实际上2.X及之前版本的 npm 并不存在这个问题,因为 node_modules 文件夹采用树结构,即每个安装的第三方包会将自己依赖的其它第三方包安装到自己文件夹下的 node_modules 文件夹中,而非根目录的 node_modules 文件夹下。根据node的寻包机制,我们在文件中无法直接引用到它。但是这种树型结构会导致文件夹体积过大以及嵌套层数过多导致长路径问题,因此 npm 在之后的版本中和 yarn 一样改用了平铺的方式。
2. pnpm的解决思路
我们可以尝试新建一个项目,同样使用 pnpm 来安装 express 包,然后查看 node_modules 文件夹下的内容,可以发现文件夹中的内容相比于 npm 的要简洁不少。
实际上pnpm是通过链接的方式来巧妙的解决了幽灵依赖的问题,这里的express文件夹只是一个符号链接,当 Node.js 解析依赖的时候,它会使用这些依赖的真实位置。 express 真正保存的地址在.pnpm/express@4.21.2/node_modules/express中。
3. 符号连接和硬连接
3.1 符号连接
符号链接又称为软连接,类似windows系统的快捷方式,如果为某个文件或文件夹A创建符号连接B,则B指向A,当删除A时,B不会被删除但是会失效。当node执行符号链接下的JS文件时,会使用原始路径。
3.2 硬链接
硬链接的概念是指将一个文件A指针复制到另一个文件B指针中,文件B就是文件A的硬链接。硬链接不会产生额外的磁盘占用,并且两个文件都能找到相同的磁盘内容。硬链接的数量没有限制,可以为同一个文件产生多个硬链接。硬链接是一个实实在在的文件,node不对其做任何特殊处理,也无法区别对待,实际上,node根本无从知晓该文件是不是一个硬链接。
3.3 pnpm中的应用
pnpm官网中有这么一句话:pnpm中node_modules 内每个包的每个文件都是到内容可寻址存储的硬链接。
内容可寻址存储是一种根据内容而不是位置进行检索信息的存储方式,被用于高速存储和检索的固定内容。我们可以通过执行pnpm store path来获取到全局存储的位置,以我的电脑为例是在 /Users/xxxx/Library/pnpm/store/v3下。在项目中执行pnpm install的时候,如果统一版本的依赖包已经存在于store中,会直接创建依赖包硬链接到store中,如果不存在,则从远程下载后存储在store中,并从项目的node_modules依赖包中创建硬链接到store中。
还是以安装 express 为例:
node_modules
└── .pnpm
└── express@4.21.2
└── node_modules
└── express -> <store>/express
├── index.js
└── package.json
└── body-parser -> ../../body-parser@1.20.3/node_modules/body-parser
└── body-parser@1.20.3
└── node_modules
└── body-parser -> <store>/express
├── index.js
└── package.json
└── express -> ./.pnpm/express@4.21.2/node_modules/express
express 包硬链接到 express@4.21.2/node_modules/express内的子文件夹中。而 express 所依赖的 body-parser 则软连接到 node_modules/.pnpm/body-parser@1.20.3/node_modules/body-parser。同时 express 会被符号链接至根目录的 node_modules 文件夹,因此 express 作为项目的直接依赖项能够被访问到。
4 小结
pnpm 巧妙地通过硬连接和软连接的方式解决了幽灵依赖问题,将所有真实依赖平铺在 ./node_modules/.pnpm 文件夹下, 将直接依赖软连接到根目录的 node_modules 文件夹下。同时通过硬链接的方式保证了相同的包不会被重复下载。因此相对于老版本的 npm,pnpm 在安装体积和下载速度都有了非常明显的提升,确实称得上是 Performant npm。