npm、yarn、pnpm 与幻影依赖的关系

994 阅读9分钟

大家好,我是渡一前端子辰老师,今天带来一集“幻影依赖”的话题。

幻影依赖也叫做幽灵依赖,这是每一个前端同学都会遇到的问题,但是很少有人去思考该如何来解决,甚至都发现不了这样的问题。

我们来举个例子:

我们这里有一个工程,工程里边有一些依赖,我们使用 npm 安装后,看到 node_modules 目录下出现了很多在依赖里没有声明的东西,那么这些没有声明的东西统称为幻影依赖。

可能同学们都习以为常了,你会说这有什么问题啊,出现这么多的原因就是因为像 element-plus、vue 之类的库还会依赖别的库,所以说安装一大堆。

但是这个,确实是一个问题,由于这里安装了一大堆我们并没有声明的东西,就意味着我们可以在代码里边去导入一个我们根本就没有声明过的库,比如这个 loadsh:

我们并没有声明 loadsh 这个库的依赖,但是我们任然可以导入这个库,并且可以正常的使用它里边的一些函数,控制台也正常输出了。

也就是说在这个例子里 loadsh 这个库就好像是一个幽灵一样,明明没有手动的去安装,但是莫名其妙的就可以使用了。

你可能会觉得这个玩意有啥问题,用就用呗,这个 loadsh 库一定是因为别的库依赖它,所以说才会被莫名其妙的被安装进来,那么既然被别的库依赖,该用就用呗。

幽灵依赖造成的问题

但是子辰告诉你这个问题大了去了,它至少有这几方面的问题:

问题一:版本

比如说我们这里有一个项目,安装了 A 这个库,版本是 v1,但是 A 库又依赖一个 B 库,版本也是 v1。

我们项目里明明没有手动安装这个 B 库,但是在项目里边仍然可以去导入它并且使用,这就产生了幽灵依赖。

一旦有一天因为某种原因,我们要把 A 库进行升级,升级的 v2 的版本,v2 这个版本有可能要使用 B 库的 v2 版本,于是 B 库也会跟着升级,而 B 库升级之后,它里边有些 API 可能有变动,那么就会导致我们之前用 B 库的代码出问题了。

这个时候开发者就会很疑惑,我升级个 A 库,另一个 B 库咋出问题了呢?这就是个版本问题,当然版本问题还有很多很多复杂的情况,每一种情况都很难排查和处理。

而且除了版本问题之外,还有一个依赖丢失的问题。

问题二:依赖丢失

现在的问题和版本无关了,项目使用开发依赖安装了一个 A 库,A 库又依赖 B 库,然后项目里导入了 B 库来使用。

因为我们 A 库使用的是开发依赖,而到了生产环境我们就不会安装这个 A 库了,那么 A 依赖的 B 也不会被安装,但是我们在开发的时候又去使用了这个 B,到了生产环境 B 库也没了,这就导致了依赖丢失。

这个问题同样很难排查,在本地好好的到了生产环境就出问题了。

那么同学说我以后写代码的时候小心一点,要用哪一个库的时候先去安装它,如果这样做的话肯定没问题,关键是真的能做到吗?

一些项目上了规模之后依赖了几十个第三方库,而且还是在一个团队协作的环境里,你不可能在导入每一个包的时候,都去手动的看一下依赖里有没有声明,这样太麻烦了,所有很多时候我们在导包的时候看一下有没有智能提示,有只能提示就认为是安装的。

所以我们需要一个强有力的东西来进行约束。

问题的产生

我们说回问题的根本,为什么会产生这样的问题,npm 是傻子吗?干嘛要允许这样的问题产生呢?画个图看看:

我们要用很多的包,包与包之间会形成依赖关系,我们经常说什么依赖树,其实准确来说应该叫做依赖图,那么图结构就图结构呗,但是问题就发生在 npm 的这个包管理器使用的是文件结构,而文件结构是一个树结构。

麻烦就麻烦在这了,结构不匹配。

早些的时候 npm 的处理方式就是文件树的方式,不同的包有相同的依赖就会在文件里嵌套相同的依赖包,这样一来的话就导致了有重复的包,项目如果上了规模,重复的包就多了,这些包就会大量占用磁盘空间。

所以后来出现了一个包管理器叫 yarn,它把这个问题解决了,yarn 将依赖拍扁了,不管什么依赖关系,通通作为 node_modules 的子目录。

那么拍扁了之后如何来表示依赖关系呢?咱们 nodeJs 不是有一个查询包的流程吗?如果说 A 依赖了 B 和 C,那么 A 里边的代码在 require 或者是 import 的时候,在本身的目录里找不到这两个依赖,就会向上查找,向上找就找到了。

这样子做是不会嵌套了,但是会产生幽灵依赖,本来我们手动安装了 A,但是依赖 B C 依旧会安装进来。

那么我们了解了问题产生的原因,现在我们就陷入了一个两难的情况,要么就接收重复文件,这样子就没有幽灵依赖了,要么就得接受幽灵依赖,无法两者兼得。

那到底有没有解决办法呢?

答案是肯定有的,要不然子辰也不会写这篇文章 😁

这个时候就要请我们的主角了 pnpm

pnpm

这个 pnpm 也是一个包管理器,它有很多的功能,比如说像很多的框架源码之类的,都是用这个包管理器来进行管理的。

pnpm 能脱颖而出的一点,就是它解决了幽灵依赖的问题:

pnpm 的理念是把所有的包,存到一个仓库文件夹里,然后再 node_modules 里使用正常的树形结构来表达我们的包依赖,那你说这样子不是有重复项了吗?其实并没有,因为这里它使用的是链接的方式也就是说树形结构并不占空间,而只是指向仓库里的一个个链接。

这样子就把问题解决了,我们试一下~

将原来的 node_modules 删除后,我们使用 pnpm 安装,你就会发现现在 node_modules 变成了这个样子:

并且 node_modules 里有个 .pnpm,这就是它的仓库:

我们所有的包都安装在这了。

然后这一部分:

这一份就是我们声明的依赖包了,我们只声明了四个,所以 node_modules 里就只出现了四个,然后它们的依赖就是通过链接的方式链接到仓库。

这样一来,这四个文件就不会造成磁盘空间的浪费,同时又解决了幽灵依赖的问题,再次导入一个没有声明过的依赖就会报错:

这样子就极大的降低了我们犯错的几率,其实你要完整理解这个 pnpm 原理的话,还需要花费不少的时间。

比如它的树形结构为什么能不占用磁盘空间,它是用了两种不同的方式来处理:

分别是硬连接和软连接,这是操作系统的概念,这里简单说一下。

我们的文件数据都是存在磁盘上,比如我们创建了一个文件 A,然后就给 A 分配了一个磁盘空间,然后文件 A 其实就是一个指针,指向磁盘空间。

然后我们可以通过文件 A 去创建一个硬连接,文件 B,那么通过硬连接创建的话 B 的指针和 A 的指针是一样的,同样指向磁盘空间,这就实现了两个文件,共用一块磁盘空间。

这就是硬连接,所以说在硬连接的模式下,文件 A 干掉以后,并不会影响文件 B。

软连接就类似于快捷方式,创建文件 A 时和硬连接一致,但是通过软连接创建文件 B 时,B 的指针就会指向文件 A,而不是磁盘空间,就相当于 B 是 A 的一个快捷方式。

那么这样子有一天 A 文件消失了,B 就不能用了。

这两种链接方式在 pnpm 里都用到了,具体它什么时候用什么链接,具体是如何用的,在文章里很难说清楚。

总结

幽灵依赖是一个很容易被忽视的问题,但是它会给我们的项目带来很多隐患,所以我们应该尽量避免它的出现。

pnpm 是一个很好的工具,它可以帮助我们管理依赖关系,提高代码质量和效率。

如果你对 pnpm 感兴趣,可以去它的官网了解更多的信息和使用方法。

本文来源

本文来源自渡一公众号:Duing,欢迎关注,获取超新超深入的技术讲解

感谢你阅读本文,如果你有任何疑问或建议,请在评论区留言,如果你觉得这篇文章有用,请点赞收藏或分享给你的朋友!