关于npm,yarn,pnpm的依赖管理

455 阅读6分钟

npm/yarn 依赖管理

npm1/2

早期npm安装依赖,node_modules文件以递归方式呈现,并且严格按照package.json结构以及次级依赖package.json结构安装

node_modules
└─ A
   ├─ index.js
   ├─ package.json
   └─ node_modules
      └─ B
         ├─ index.js
         └─ package.json
         └─ node_modules
            └─ C
             ├─ index.js
             └─ package.json
             └─ node_modules
             ....

假如项目中依赖有相同的次级依赖,还会被重复安装

node_modules
├─ A
│  ├─ index.js
│  ├─ package.json
│  └─ node_modules
│      └─ C
│          ├─ index.js
│          └─ package.json
└─ B
   ├─ index.js
   ├─ package.json
   └─ node_modules
       └─ C
           ├─ index.js
           └─ package.json

真实的开发场景中问题会更多

  • 依赖层级过深,导致文件路径过长
  • 重复包被安装,导致node_modules文件体积巨大,占用过多磁盘
  • 模块实例不能共享。(例如,两个不同包引入的React不是一个模块实例,会导致内部变量无法共享,导致一些不可预知的bug)

npm >3.x & yarn

npm3版本以及yarn开始采用扁平化方式代替嵌套式方式管理项目依赖。

例如:A和B都依赖C依赖安装后

node_modules
├─ A
│  ├─ index.js
│  └─ package.json
├─ B
│  ├─ index.js
│  └─ package.json
└─ C
   ├─ index.js
   └─ package.json

由于node的require机制(非node核心模块会递归上层node_modules目录查找)实现了依赖打平,解决了嵌套地狱的目录,但是确无法完成解决重复安装依赖的问题。

例如 B,C 同时依赖了A的2.0.0版本,还是会重复安装A@2.0.0两次

node_modules
├─ A@1.00
│  ├─ index.js
│  └─ package.json
├─ B
│  ├─ index.js
│  └─ package.json
|  └─ node_modules
|     └─ A@2.0.0
|        ├─ index.js
|        └─ package.json
└─ C
   ├─ index.js
   └─ package.json
   └─ node_modules
      └─ A@2.0.0
         ├─ index.js
         └─ package.json

那么npm/yarn会提升A@1.0.0还是A@2.0.0?事实上,这取决于package.json的顺序,这个就是为什么产生依赖树不确定的问题。

拍平依赖还很可能造成幽灵依赖的问题,比如,package.json里声明了A依赖,A依赖了B依赖,这种情况下,我们在代码中依然可以引用到B,如果A升级了版本,并且提升对于B的版本依赖,或者删除了B的版本依赖,这个时候我们代码就会出现不可预期的bug

这种扁平化的处理方式,依旧存在着问题

  • 允许访问package.json中没有声明的依赖,造成幽灵依赖的问题
  • 扁平化算法复杂性高,安装效率低。
  • 依赖树不确定性。
  • 无法完全解决依赖重复安装问题,导致分身依赖问题。

为了解决依赖结构的不确定性问题以及锁定唯一版本,yarn.lock,package-lock.json(npm > 5.x) 出现。 用来确保install之后都产生确定的node_modules结构。yarn.lock和package-lock.json不同的是一个是用yaml格式,一个用json格式,锁版本规则都是大版本根据package.json,小版本根据lock文件

pnp

yarn2.0 -pnp, 重写了node require机制,直接在node解析的时候把相应模板返回给node,抛去生成一个node_modules结构去给require查找。

好处:

  • 节省了生成node_modules文件结构树的io操作时间,节省了node递归查找node_modules的时间
  • 防止了幽灵依赖
  • 防止了重复依赖的安装

问题

  • 重写了node解析规则,需要兼容webpack,ts, vscode之类有自己模块解析的工具

pnpm

简介

从官方文档上的介绍来看,它是一个快速的,节省磁盘空间的包管理工具,号称比其他包管理器快俩倍,搬运下官网上的图。

可以看出从各个情况的速度对比,pnpm占很大优势。官方也给出了 The reason pnpm is fast。

上图是 npm & yarn 的安装流程。可以看出整个过程是统一解析,统一下载,统一解压。

上图是pnpm的安装流程。可以看出pnpm 没有安装的阻塞阶段。每个依赖都有自己的阶段,下一个阶段会尽快开始。

依赖管理

对于npm和yarn的幽灵依赖以及依赖重复问题,在pnpm里得到了很好的解决,pnpm的解决思路就是Store + Links。

Store

关于pnpm的store我们可以从安装中看出。

Content-addressable store 和 Virtual store 有分别是什么呢。

Content-addressable store:CAS内容寻址存储,不同于位置寻址,其可以根据内容来检索信息。

Virtual store: 虚拟存储目录 node_modules/.pnpm。

我理解的是如果某个依赖在 Content-addressable store目录中存在了话,那么就会直接从 Content-addressable store目录里面去 hard-link,在.pnpm下生成依赖的硬链接文件。

硬链接

硬链接本质是文件的同步副本,Windows系统的硬链接是一种针对文件的特殊快捷方式,只不过这种快捷方式的实现和一般的快捷方式不一样,是NTFS文件系统特有的属性之一。linux下硬链接引用的是文件系统种的物理索引(也称为inode),简单理解就是硬链接文件与源文件指向同一个磁盘空间,硬链接文件不占空间。

  • 硬连接适用于在同一个卷的文件级别,不允许给目录创建硬链接;
  • 硬连接是不能跨卷的,只有在同一文件系统中的文件之间才能创建链接。

软链接

linux下软链拥有自己的inode,文件本身存的是源文件的文件名和inode号,window下的软链类似快捷方式,包括Junction、symbolic-link。

node_modules结构

下面大致看下pnpm的node_modules的包结构。

node_modules
|__.pnpm
|    |__D@1.0.0
|	|__D@1.0.1
|	|__A@1.0.0
|   	|  |__node_modules
|       |	|__D
|	|	|__A
|       |__B@1.0.0
|	    |__node_modules
|		|__D
|		|__B
|
|      
|__A
|__B

可以看出外层严格按照package.json来,真正的依赖在相应的.pnpm下平铺,对应依赖的次级依赖全部到打平到相应的node_modules里,这样避免了能够引入到幽灵依赖

结合上面的结构目录图已经前置的软硬链接知识。来看下pnpm的依赖链接情况。

可以看出 package1 会通过JUNCTION软链接到.pnpm\package-test1-tang@1.0.0\node_modules\package-test1-tang。 其最深路径为

.pnpm/<organization-name>+<package-name>@<version>/node_modules/<name>

组织名(若无会省略)+包名@版本号/node_modules/名称(项目名称)

这样可以解决掉路径过深的问题。那如果所有次级依赖也平铺到相应包的node_modules里会不会和外部被打平的依赖重复安装呢,可以看下。

真实情况其实是.pnpm下相关依赖下的重复次级依赖也会通过软链到外层拍平的依赖上,这样保证了依赖的次级依赖的完整和可看性,也不会重复安装,

整体的node_modules链接情况大致如下。

总结

yarn的出现解决了早期npm的不支持离线模式、树形结构的依赖、依赖安装不确定性等问题,但是npm的后期更新也解决了这些问题,pnpm的出现继承npm,yarn的优点,同时使用link+store的模式节省了安装时间和磁盘空间,解决了依赖重复安装以及幻影依赖的问题,由此看来pnpm目前的依赖管理更加的安全,高效,但是对于npm或者yarn,相信包的管理机制在后期的更新中都会得到更好的完善。