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

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

1. npm(v1和v2)

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

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

这种嵌套结构会导致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的结构如下:

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

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

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

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

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

2.1 幽灵依赖

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

如上图所示,项目的直接依赖只包含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被安装

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文件

perl 复制代码
// 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结构如下:

首先,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和硬链接解决了依赖冗余的问题,同时也提升了依赖安装的速度

参考资料

相关推荐
恋猫de小郭3 小时前
Flutter Zero 是什么?它的出现有什么意义?为什么你需要了解下?
android·前端·flutter
崔庆才丨静觅10 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby606111 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了11 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅11 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅11 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅12 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment12 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅12 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊12 小时前
jwt介绍
前端