该从什么角度思考npm、yarn与pnpm的区别

随着 pnpm 越来越火爆,相信大家听到的次数也越来越多,今天我们也来一起学习一下大力推崇的 pnpm 到底有什么神奇之处?


目录

一、npm/yarn install 原理

二、npm

三、yarn

四、pnpm

五、更好的实用性

总结


一、npm/yarn install 原理

  npm/yarn install 时发生了什么?主要分为两个部分,首先是:包如何到达 node_modules 当中,其次是:node_modules 内部是如何管理依赖的;

  执行命令以后,首先会构建依赖树,然后针对每个节点下的包,会经历四个步骤:

  1. 将依赖包的版本区间解析为某个具体的版本号
  2. 下载对应依赖的 tar 包到本地离线镜像
  3. 将依赖从离线镜像解压到本地缓存
  4. 将依赖从缓存拷贝到当前目录的 node_modules 目录

然后,对应的包就会到达项目的 node_modules 中。 

那么,这些依赖在 node_modules 当中是什么样的目录结构呢?也就是我们上面输的 依赖树是什么样子呢?这个要看下面详细讲解,不同的版本是不一样的!

二、npm

我们按照 npm 包管理工具的发展历史,从 npm2 开始讲起:

   用 node 版本管理工具把 node 版本降到 4,那 npm 版本 就是 2.x

 然后我们新建一个Demo,执行下 npm init -y,快速创建一个 package.json

 然后执行 npm install express ,那么 express 包和它的依赖都会被下载下来:

展开 express,它也有 node_modules 

 再展开几层,我们会发现每个依赖都有自己的 node_modules

 

 也就是 npm2 的 node_modules 是嵌套的

node_modules
└─ express
   ├─ index.js
   ├─ package.json
   └─ node_modules
      └─ accepts
         ├─ index.js
         └─ package.json

这种结构就是我们上面所说的 依赖树

现在的 accepts 当中又有依赖,然后就会继续嵌套下去。试想一下这样的设计存在什么问题呢?

  1. 依赖层级太深,导致致命的问题是 Windows 的文件路径最长是 260 多个字符,这样嵌套是会超过 Windows 路径的长度限制的。
  2. 大量重复的包被安装,文件体积超大。比如跟 express 统计目录下有一个 foo,两者都依赖于同一个版本的 Lodash,那么 Lodash 会分别在两者的 node_modules 中被安装,也就是重复安装,会占据比较大的磁盘空间。
  3. 模块实例不能共享。比如 React 有一些内部变量,在两个不同包引入的 React 不是同一个模块实例,因此无法共享内部变量,导致一些不可预知的 bug。

当时 npm 还没解决,社区就出来新的解决方案了,就是 yarn:↓ 

三、yarn

  yarn 是怎么解决依赖重复很多次,嵌套路径过长的问题呢?

  铺平,也就是说所有的依赖不再一层一层嵌套了,而是全部在同一层,这样也就没有依赖重复多次的问题了,也不会存在路径过长的问题了;

我们把 node_modules 删了,用 yarn 再重新安装下,执行 yarn add express

这时候 node_modules 就是这样了:

全部铺平在了一层,展开下面的包大部分是没有二层 node_modules 的:

所有的依赖都被拍平到 node_modules目录下,不再有很深层次的嵌套关系。这样在安装新的包时,根据 node require 机制,会不停往上级的 node_modules当中去找,如果找到相同版本的包就不会重新安装,解决了大量包重复安装的问题,而且依赖层级也不会太深。


但是多展开几个依赖包,大家会发现,为什么还会有嵌套呢?

        因为一个包是可能有多个版本的,提升只能提升一个,所以后面再遇到相同包的不同版本,依然还是用嵌套的方式。 


 npm 后来升级到 3 之后,也是采用这种铺平的方案了,和 yarn 很类似。当然,yarn 还实现了 yarn.lock 来锁定版本,这个功能 npm 也实现了。

但是 yarn 和 npm 都采用铺平了的方案,这种方案就没有问题了吗?

并不是,它照样还是存在诸多问题的,梳理一下:

  1. 依赖结构的不确定性
  2. 扁平化算法本身的复杂性很高,耗时较长。
  3. 幽灵依赖( 通俗一点的说:可以非法访问没有声明过依赖的包 )

后来两个都好理解,但是第一点的 不确定性 该如何理解呢? 

假如现在项目依赖两个包 Barry 和 Lishen,这两个包的依赖又是这样的:

 那么 npm / yarn install 的时候,通过扁平化处理之后究竟是怎么样子?

 究竟是这样?

 还是这样呢?

 答案是:都有可能,取决于 Barry Lishen package.json 中的位置,如果 Barry 声明在前则是前面的结构,否则就是后面的结构。这就是为什么依赖会产生依赖结构的 不确定 问题,也就是前面说的 lock 文件诞生的原因,无论是 package-lock.json (npm 5.X 以后才有的,也就是npm3)还是 yarn.lock ,都是为了保证 install 之后都产生确定的 node_modules 结构。


 那 pnpm 是怎么解决这俩问题的呢?

四、pnpm

回想下 npm3 yarn 为什么要做 node_modules 扁平化?不就是因为同样的依赖会复制多次,并且路径过长在 windows 下有问题么?

那如果不复制呢,比如通过 link

首先介绍下 link,也就是软硬连接,这是操作系统提供的机制,硬连接就是同一个文件的不同引用,而软链接是新建一个文件,文件内容指向另一个路径。当然,这俩链接使用起来是差不多的。

如果不复制文件,只在全局仓库保存一份 npm 包的内容,其余的地方都 link 过去呢?

这样不会有复制多次的磁盘空间浪费,而且也不会有路径过长的问题。因为路径过长的限制本质上是不能有太深的目录层级,现在都是各个位置的目录的 link,并不是同一个目录,所以也不会有长度限制。

没错,pnpm 就是通过这种思路来实现的。

再把 node_modules 删掉,然后用 pnpm 重新装一遍,执行 pnpm install。

你会发现它打印了这样一句话:

 包是从全局 store 硬连接到虚拟 store 的,这里的虚拟 store 就是 node_modules/.pnpm

我们打开 node_modules 看一下:

确实不是扁平化的了,依赖了 express,那 node_modules 下就只有 express,没有幽灵依赖。

展开 .pnpm 看一下:

所有的依赖都在这里铺平了,都是从全局 store 硬连接过来的,然后包和包之前的依赖关系是通过软链接组织的。 .pnpm/node_modules 下的 expresss,这些都是软链接。

也就是说,所有的依赖都是从全局 store 硬连接到了 .pnpm/node_modules 下,然后之间通过软链接来相互依赖。

 官方给了一张原理图,配合着看一下就明白了:pnpm官方地址

 这就是 pnpm 的实现原理

那么回过头来看一下,pnpm 为什么优秀呢?

首先,最大的优点是节省磁盘空间呀,一个包全局只保存一份,剩下的都是软硬连接,这得节省多少磁盘空间呀。

其次就是,因为通过链接的方式而不是复制,自然会

五、更好的实用性

  说了这么多,估计你会觉得 pnpm 挺复杂的,是不是用起来成本很高呢?

  恰好相反,pnpm 使用起来十分简单,如果你之前有 npm/yarn 的使用经验,甚至可以无缝迁移到 pnpm 上来。

  仅仅是这些吗?下面还有更重要的就是项目管理方式的支持

个人感觉:pnpm 可以更好的支持 Multirepo 项目过渡到  Monorepo 项目,原因如下:

如果 A 依赖 X,B 依赖 X,还有一个 C,它不依赖 X,但它代码里面用到了 X。由于依赖提升的存在,npm/yarn 会把 X 放到根目录的 node_modules 中,这样 C 在本地是能够跑起来的,因为根据 node 的包加载机制,它能够加载到 monorepo 项目根目录下的 node_modules 中的 X。但试想一下,一旦 C 单独发包出去,用户单独安装 C,那么就找不到 X 了,执行到引用 X 的代码时就直接报错了。

总结


当然,今天只是深入的学习,那种包管理方式更适合大家具体情况还是要看我们自己的项目,可能因人而异,因项目而异,因业务而异~~~

欢迎大家一起讨论学习~~~

猜你喜欢

转载自blog.csdn.net/weixin_56650035/article/details/126842623