欢迎您访问 最编程 本站为您分享编程语言代码,编程技术文章!
您现在的位置是: 首页

npm/yarn/pnpm 如何进行依赖关系管理

最编程 2024-03-12 21:11:44
...

本文介绍了业界最主流的三个包管理器(npm/yarn/pnpm)的依赖管理策略,并对各种策略的优缺点进行了对比分析

1. npm(v1和v2)

npm(v1和v2)采用嵌套结构的node_modules,间接依赖直接嵌套在直接依赖的node_modules中

若项目包含A和C两个依赖,A依赖B、C也依赖B,那么node_modules的结构如下:

image.png

这种嵌套结构会导致node_modules文件极大地占用了磁盘空间,降低依赖安装的速度,原因如下:

1.1 嵌套层次深

在直接依赖越来越多的情况下,间接依赖也会越来越多,node_modules目录的嵌套层次就会越来越深

1.2 依赖冗余

如上图所示,如果A和C都依赖了B,那么B就会被重复安装2次,即使B有可能是同一版本的

2. npm(v3+)

从v3开始,npm采用了扁平化结构的node_modules,间接依赖会尽量平铺在node_modules的一级目录中

若项目包含A和C两个依赖,A依赖B、C也依赖B

安装依赖时就会对B进行提升,node_modules的结构如下:

image.png

之所以能够进行「依赖提升」,是因为:当我们使用require引入模块时,NodeJs会按照如下优先级去寻找对应的模块:

  • 核心模块:NodeJs提供的系统核心模块,例如fs、querystring等
  • 文件模块:以./或../开头的参数,会被当做文件模块进行处理
  • 第三方模块:在当前目录下的node_modules目录下查找,如果找不到就一直向上(父目录)递归

详见面试官:你真的了解CommonJs和EsModule吗?- 掘金

所以当依赖A中的代码使用require('B')时,会往上层父目录的node_modules中继续查找

「依赖提升」虽然在一定程度上减少了node_modules文件对磁盘空间的占用,但是也带了新的问题

2.1 幽灵依赖

幽灵依赖指的是在package.json中未声明的依赖,但项目中依然可以正确地引用

image.png

如上图所示,项目的直接依赖只包含A和C两个,但由于依赖提升,在项目的代码中引入B还是能够正常编译构建的

如果在项目后续的迭代中,B不再是间接依赖,那么项目中引入B的代码就会报错

2.2 不确定性

不确定性指的是:同样的项目,在不同开发者的本地安装依赖后可能会得到不同的node_modules目录

若A依赖B@1.0.0,C依赖B@2.0.0,那么提升哪个版本的B,可能取决于依赖的安装顺序

  • 若执行npm install,则基于package.json中、依赖名称的字母顺序进行安装,过程如下:

    • 安装A
    • A依赖了B,因此需要提升B
    • 安装C
    • C依赖了B,但B已经被提升
    • 最终被提升的就是1.0.0版本的B
  • 若先后执行npm install Cnpm install A,则安装过程如下:

    • 安装C
    • C依赖了B,因此需要提升B
    • 安装A
    • A依赖了B,但B已经被提升
    • 最终被提升的就是2.0.0版本的B

2.3 依赖冗余

若在后续的迭代中,项目又安装了依赖D和E

  • D依赖B@1.0.0
  • E依赖B@2.0.0

那么无论提升哪个版本的B,都会存在重复版本的B被安装

image.png

3. yarn

yarn同样采用「依赖提升」的方式形成扁平化结构的node_modules,同时也带来了一些新的变化

3.1 提升安装速度

  1. 并行安装

使用npm安装依赖时,安装任务是串行执行的,必须等到一个包安装完成、再安装下一个

而yarn采用了并行的方式来安装依赖,提升了安装速度

  1. 本地缓存

最早被yarn提出,npm也在后续的版本中支持了这个特性

在安装依赖时,yarn/npm会在本地磁盘中进行缓存

npm还提供了几个命令行参数来控制依赖的安装策略

  • --prefer-offline:本地找不到缓存才会进行网络请求
  • --prefer-online:网络请求失败才会去本地缓存取
  • --offline:强制使用本地缓存

3.2 解决不确定性

yarn会在项目第一次安装依赖时生成yarn.lock文件,用于确定依赖结构

它记录了所有依赖的版本(包括直接依赖和间接依赖),以及每个依赖的下载源地址

npm从v5版本开始,也会在第一次安装时生成package-lock.json文件

// yarn.lock
"lodash@^4.17.0", "lodash@^4.17.15", "lodash@^4.17.21":
  version: "4.17.21"
  resolved: "xxx"
  integrity: "xxx"
  • version:实际安装的版本,通常是满足版本区间里的一个版本
  • resolved:该依赖的下载源地址
  • integrity:hash值,用于对下载的文件进行完整性校验

基于lockfile,yarn会遍历所有直接依赖、并递归遍历它们的依赖,计算出各个依赖的各个版本被引用的次数。一般来说,会对 被引用次数最多的版本 进行提升,从而生成一份确定的node_modules结构

详见yarn/src/package-hoister.js at master · yarnpkg/yarn

但依赖提升也受其他因素的影响,例如:

  • 不同版本间是否兼容
  • 是否声明了resolution
  • 是否声明了peerDependency

4. yarn - PnP

PnP(Plug an Play)是通过重写依赖解析机制来实现的

  • 它在项目中维护了一份静态映射(.pnp.cjs),记录依赖在缓存中的具体位置
  • 不再生成node_modules目录,而是自建解析器、通过上述的静态映射去找到项目的依赖文件

PnP解决了依赖冗余的问题,也提升了依赖安装的速度,但同时它也脱离了NodeJs的生态、兼容性不够好

详见Plug'n'Play | Yarn

5. pnpm

先介绍几个相关的符号/术语:

  • store:表示全局store,pnpm会将依赖下载到系统的全局store中
  • Symbolic link:表示软链接,它指明了另一个文件的路径名,pnpm通过它找到另一个文件
  • Hard link:表示硬链接,它指向文件实际存储的磁盘地址,但它本身并不占用实际的存储空间

详见Linux 硬链接与软链接

接下来用同样的例子进行说明:

若项目包含A@1.0.0和C@1.0.0两个依赖,A依赖B@1.0.0、C依赖B@2.0.0,那么pnpm生成的node_modules结构如下:

image.png

首先,node_modules下的一级目录非常简洁,仅包含项目的直接依赖和一个.pnpm目录

其次,一级目录下的直接依赖目录只是一个Symbolic link(软链接),它指向.pnpm目录中对应依赖的目录

最后,我们再来看.pnpm目录:

  • 它平铺了项目的所有直接依赖和间接依赖(A、B、C)

  • 在每个依赖的node_modules目录中

    • 自身:使用Hard link指向全局store中对应的地址

    • 其他依赖:使用Symbolic link指向.pnpm目录中对应的目录

5.1 解决幽灵依赖

在pnpm生成的node_modules目录中,仅包含项目的直接依赖和一个.pnpm目录

因此,在项目的代码中引入间接依赖是不可行的,会导致编译报错,幽灵依赖的问题得以解决

5.2 解决依赖冗余&提升安装速度

pnpm会将依赖下载到全局的store中,确保每个版本的依赖只会被下载一次

这个特性使得不同的项目可以从全局store中寻找到同一个依赖,极大程度节省了磁盘空间,同时也提升了依赖安装的速度

6. 总结

  1. npm(v1和v2)

采用嵌套结构的node_modules,占用较大磁盘空间,依赖安装速度慢,原因在于:

  • 嵌套层次深
  • 依赖冗余
  1. npm(v3+)

通过「依赖提升」形成扁平化结构的node_modules

  • 一定程度上减少了node_modules文件对磁盘空间的占用
  • 依然没有解决依赖冗余的问题
  • 带来了新的问题(幽灵依赖 & 不确定性)
  1. yarn

通过「依赖提升」形成扁平化结构的node_modules

  • 提出并行安装和本地缓存的方案以提升依赖安装速度
  • 提出lockfile以解决node_modules结构的不确定性
  • 依然没有解决幽灵依赖和依赖冗余的问题
  1. yarn-PnP

废弃node_modules,通过自建依赖解析器、根据静态映射文件在全局缓存中找到依赖

  • 解决了依赖冗余的问题
  • 提升了依赖安装的速度
  • 但脱离了NodeJs的生态,导致兼容性不够好
  1. pnpm 采用了一套全新的依赖管理策略:内容寻址存储
  • 通过非扁平的node_modules目录结构解决了幽灵依赖的问题
  • 通过全局store和硬链接解决了依赖冗余的问题,同时也提升了依赖安装的速度

参考资料

  • 深入浅出 npm & yarn & pnpm 包管理机制 - 掘金
  • 关于现代包管理器的深度思考——为什么现在我更推荐 pnpm 而不是 npm/yarn? - 掘金
  • 从npm 到 yarn 再到 pnpm —— 为什么要使用pnpm? - 掘金
  • 面试官:你真的了解CommonJs和EsModule吗?- 掘金
  • Flat node_modules is not the only way | pnpm中文文档 | pnpm中文网
  • Yarn Plug'n'Play可否助你脱离node_modules苦海?