除了npm install,你对包管理工具还有其他了解吗?

823 阅读10分钟

前言

今天要拉一个新项目做新的需求,运行项目的过程中出现了各种问题,发现依赖装不上,一开始我以为是node版本不兼容,然后用nvm修改了node版本之后还是无法安装依赖,最后只能用最笨的办法让同事把整个node_modules打包发给我,按理说到这里问题都解决了,只要run dev一下就能运行了,结果运行的时候还是出现了问题,显示依赖不存在,这就给我看的一脸懵了,明明依赖都在,为什么还显示不存在,这终端咋睁着眼睛说瞎话,后面查了资料发现,这涉及到pnpm依赖的特性。

包管理工具的发展史

包管理工具大家肯定不陌生,平常开发的时候天天用,虽然现在基本上已经很少用npm了,基本上都是以yarn和pnpm为主,但是了解一下它的发展史,对后续的使用以及为什么要用这个而不用那个,也会有一个更深的理解。 总的来说包管理工具的发展史可以分为以下几个节点:
npm1、npm2 -> npm3 -> yarn -> pnpm

npm的诞生

随着 Node.js 的推出,开发者需要一种管理众多 JavaScript 库和模块的方法,npm(Node Package Manager)由此诞生。

npm 于2010年被引入,很快成为 Node.js 生态系统中分享和管理模块的标准方式。其中 node_modules 目录就是npm用来局部安装依赖的地方,使得不同的项目可以使用不同版本的包,而不会互相干扰。随着时间的推移,npm 还成为了一个庞大的开源库生态系统,目前已是世界上最大的软件注册机构。 然而,随着 npm 的快速成长,一些问题也随之而来,比如 node_modules 随着依赖的嵌套,体积越来越大。npm1和npm2一直采用的都是嵌套式的树状依赖结构,这样的依赖结构会导致依赖无法共享,很多包被重复安装。

image.png

于是为了解决这个问题,就对它的依赖结构进行了修改,于是就有了npm3。

npm3

npm3采用的是扁平式的依赖结构,也就是所有的依赖及其子依赖等全部放置在node_modules目录的顶层,这样就避免了相同依赖被重复的问题。

image.png

但是npm3同样存在很多问题:

  • npm install时会拉取当前大版本下的最新依赖包,当依赖包有小版本更新时,导致协同开发者依赖树不一致
  • 扁平化的依赖结构带来了幻影依赖和依赖分身的问题

yarn

yarn 的出现是为了解决 npm 当时存在的一些问题,它由 Facebook、Google、Exponent 和 Tilde 共同开发,于2016年发布,旨在提供一个更快、更安全的 JavaScript 包管理工具。

yarn 的特点:

  • 性能提升:yarn 在发布之初就强调了性能优势,特别是在安装依赖时。它通过并行安装依赖和缓存已下载的包来加速这一过程,减少了安装时间。
  • 更好的依赖管理:yarn 引入了 yarn.lock 文件,这个锁文件确保了依赖的一致性。无论是在哪个环境下运行yarn install,都能确保安装相同版本的依赖,解决了因版本不匹配导致的问题。
  • 更好的安全性:yarn 通过检查安装的每个包的许可证,并提供了一种机制来限制或拒绝具有不安全许可证的包的安装,增强了项目的安全性。

yarn存在的问题:

  • 扁平化算法本身的复杂性
  • 没有解决幻影依赖和依赖分身的问题

依赖分身

依赖分身的根本原因是在项目中存在多个相同包的不同版本。npm3和yarn又是扁平的依赖结构,不可能在相同的根node_modules下存在多个相同名字的包,所以只能把不同版本的包存放在自目录下,从而导致分身。

在小项目中很少遇见,因为一个项目不会使用两个版本不同的相同包,但是在monorepo中尤为常见。

假设有packages/library(A~E),B和C依赖于F1,D和E依赖于F2,就会出现以下两种情况:

image.png

或者:

image.png

依赖分身会产生依赖结构的不确定问题。这也是 lock 文件诞生的原因,无论是package-lock.json(npm 5.x才出现)还是yarn.lock,都是为了保证 install 之后才产生确定的node_modules结构。尽管如此,npm/yarn 本身还是存在扁平化算法复杂package 非法访问的问题,影响性能和安全。

幻影依赖

幻影依赖(Phantom Dependencies)是指在项目的依赖树中出现了一些不存在的依赖项。这种情况通常发生在包管理器(如 npm、Yarn 等)安装依赖时,由于依赖版本冲突或其他原因导致的。简单来说就是项目使用了package.json中没有声明的依赖,因为npm3和yarn这种扁平化的依赖结构,即使package.json中没有声明,也还是可以使用其他依赖。

具体来说,幻影依赖的原因可能包括:

  1. 版本冲突:当项目中依赖了同一个库的不同版本时,可能会出现幻影依赖。比如项目 A 依赖 library@1.0.0,项目 B 依赖 library@2.0.0,那么在项目 A 的依赖树中可能会出现一个指向 library@2.0.0 的幻影依赖。
  2. 依赖传递:当一个依赖引入了另一个依赖,但该依赖在项目的依赖树中不存在时,也会产生幻影依赖。比如 library-A 依赖 library-B,但 library-B 并未被项目直接依赖。
  3. 安装错误:在某些情况下,包管理器的安装过程可能出现问题,导致项目的依赖树中出现不存在的依赖项。

pnpm

然后就到了我们今天的重头戏了,pnpm英文里面的意思叫做 performant npm ,意味“高性能的 npm”,pnpm 相比较于 yarn/npm 这两个常用的包管理工具在性能上也有了极大的提升。

安装依赖的速度比npm和yarn更快

  • npm&yarn安装依赖的过程:
    • 将依赖包的版本区间解析为某个具体的版本号
    • 下载对应版本依赖的 tar 包到本地离线镜像
    • 将依赖从离线镜像解压到本地缓存
    • 将依赖从缓存拷贝到当前目录的 node_modules 目录

pnpm内部使用基于内容寻址的文件系统来存储磁盘上的所有文件,不会重复安装同一个包。用 npm/yarn 的时候,如果 100 个项目都依赖 lodash,那么 lodash 很可能就被安装了 100 次,磁盘中就有 100 个地方写入了这部分代码。但在使用 pnpm 只会安装一次,磁盘中只有一个地方写入,后面再次使用都会直接使用 hardlink

即使一个包的不同版本,pnpm 也会极大程度地复用之前版本的代码。举个例子,比如 lodash 有 100 个文件,更新版本之后多了一个文件,那么磁盘当中并不会重新写入 101 个文件,而是保留原来的 100 个文件的 hardlink,仅仅写入那一个新增的文件。

除此之外pnpm解决了幻影依赖的问题。

pnpm是怎么解决幻影依赖的

这里先等等,这里还涉及到一个知识点,软链接硬链接

  • 硬链接(Hard Link)

    • 概念:硬链接是文件系统中的一个链接,它指向磁盘上的数据。当创建一个硬链接时,实际上是在创建一个和原始文件相同的入口点,但是不占用额外的磁盘空间。这个新的链接和原始文件共享相同的数据块,任何一个文件的修改都会反映在另一个上。
    • 特点:硬链接不能跨文件系统创建,也不能用于链接目录,但如果原始文件被删除,硬链接依然可以访问数据。
    • 使用场景:当你想要在不同位置访问同一个文件内容,而又不想占用额外磁盘空间时,可以使用硬链接。比如,在多个项目中共享相同的库文件,但不需要复制这个文件多份。
  • 软链接(符号链接,Symbolic Link)

    • 概念:软链接是一个特殊类型的文件,它包含了另一个文件的路径。类似于 Windows 系统中的快捷方式。与硬链接不同,软链接可以指向目录,也可以跨文件系统。
    • 特点:软链接指向文件或目录的路径,如果原始文件被删除,软链接就会失效,因为它的指向已经不存在了。
    • 使用场景:软链接适用于需要引用特定位置的文件或目录时,特别是当这些文件或目录可能会移动或变化时。它允许链接到另一个文件系统中的文件或目录。

pnpm通过使用全局的 .pnpm-store 来存储下载的包,使用硬链接来重用存储在全局存储中的包文件,这样不同项目中相同的包无需重复下载,节约磁盘空间。并且pnpm 将各类包的不同版本平铺在 node_modules/.pnpm 下,对于那些需要构建的包,它使用符号链接连接到存储在项目中的实际位置。这种方式使得包的安装非常快速,并且节约磁盘空间。

其实从这里也可以看出,pnpm解决幻影依赖的核心就是在node_modules目录下只存在package.json中声明的依赖项,并且每个依赖项都是个软链接,连接到.pnpm目录下,所以幻影依赖不存在对应的软链接,使用的时候就会报错。

并且pnpm的既不是树状结构,也不是npm和yarn这样的完全平铺,而是采用了网状 + 平铺的目录结构,软件包按照其依赖项进行分组,它保持了包的隔离,并且与node.js兼容
网状性:在网状结构中,依赖可以共享同一版本的包,这样可以避免重复安装相同的依赖。
平铺性:包的依赖项与依赖包的实际位置位于同一目录级别。避免了循环符号链接

image.png

回归正题callback

然后就可以去想想开头那个地方为什么同事直接把这个node_modules复制下来给我依然还是没用的,因为pnpm里的node_modules里全是软链接,他们指向依赖实际的安装位置,但是我的电脑上并没有安装这些依赖,所以这些依赖在做链接的时候当然链接不到啊。所以就狠狠的爆红。

不过后面的解决方法是在安装的时候加上--ignore-script,就是在安装依赖的时候忽略脚本的执行,因为有些依赖可能会自带执行脚本,你在安装它的时候这些脚本会在安装依赖的某些生命周期中自动执行,就会导致依赖安装出现问题,所以直接忽略脚本就可以判断是依赖本身的问题还是脚本执行带来的问题。

结尾

这篇文章是重新写过的,之前写的实在有点太水,现在稍微好点了,精力有限,争取再接再厉。