npm & yarn & pnpm包管理器机制

164 阅读6分钟

前言

在做Node Library依赖关系分析工具时,起初只考虑使用npm管理包,没有考虑yarn&pnpm的情况,故去看了下这几个管理依赖工具之间的区别。

1️⃣ npm

npm早期采用的是嵌套的node_modules结构,直接依赖会平铺在node_modeuls下,子依赖嵌套在直接依赖的node_modules中。实际上,真实场景下,依赖增多,冗余的包也会变多,会把磁盘占满,以来嵌套的深度非常可怕。

// 比如项目依赖了A 和 C,而 A 和 C 依赖了不同版本的 B@1.0 和 B@2.0,node_modules 结构如下:
node_modules
├── A@1.0.0
│   └── node_modules
│       └── B@1.0.0
└── C@1.0.0
    └── node_modules
        └── B@2.0.0
// 如果 D 也依赖 B@1.0,会生成如下的嵌套结构:
node_modules
├── A@1.0.0
│   └── node_modules
│       └── B@1.0.0
├── C@1.0.0
│   └── node_modules
│       └── B@2.0.0
└── D@1.0.0
    └── node_modules
        └── B@1.0.0
// 真实场景下,依赖增多,冗余的包也会变多,会把磁盘占满,以来嵌套的深度非常可怕。

考虑上述问题,npm v3将子依赖提升(hoist),采用了扁平的node_modules结构,不会造成大量包的重复安装,依赖的层级也不会太深,解决了依赖地狱的问题。

但是同时出现了三个问题,幽灵依赖 Phantom dependencies、不确定性、依赖分身 Doppelgangers

❣ 幽灵依赖是指:在 package.json 中未定义的依赖,但项目中依然可以正确地被引用到。

❣ 不确定性是指:同样的 package.json 文件,install 依赖后可能不会得到同样的 node_modules 目录结构。

❣ 依赖分身 Doppelgangers:

假设继续再安装依赖 B@1.0 的 D 模块和依赖 @B2.0 的 E 模块,此时:

以下是提升 B@1.0 的 node_modules 结构:

node_modules
├── A@1.0.0
├── B@1.0.0
├── D@1.0.0
├── C@1.0.0
│   └── node_modules
│       └── B@2.0.0
└── E@1.0.0
    └── node_modules
        └── B@2.0.0

可以看到 B@2.0 会被安装两次,实际上无论提升 B@1.0 还是 B@2.0,都会存在重复版本的 B 被安装,这两个重复安装的 B 就叫 doppelgangers。而且虽然看起来模块 C 和 E 都依赖 B@2.0,但其实引用的不是同一个 B,假设 B 在导出之前做了一些缓存或者副作用,那么使用者的项目就会因此而出错。

npm v5 发布了 package-lock.json,解决了不确定的问题

2️⃣ yarn

yarn 也采用扁平化 node_modules 结构。它的出现是为了解决 npm v3 几个最为迫在眉睫的问题:依赖安装速度慢,不确定性。

❣ 提升安装速度

在 npm 中安装依赖时,安装任务是串行的,会按包顺序逐个执行安装,这意味着它会等待一个包完全安装,然后再继续下一个。

为了加快包安装速度,yarn 采用了并行操作,在性能上有显著的提高。而且在缓存机制上,yarn 会将每个包缓存在磁盘上,在下一次安装这个包时,可以脱离网络实现从磁盘离线安装。

❣ lockfile 解决不确定性

yarn 更大的贡献是发明了 yarn.lock。

在依赖安装时,会根据 package.josn 生成一份 yarn.lock 文件。

lockfile 里记录了依赖,以及依赖的子依赖,依赖的版本,获取地址与验证模块完整性的 hash。

即使是不同的安装顺序,相同的依赖关系在任何的环境和容器中,都能得到稳定的 node_modules 目录结构,保证了依赖安装的确定性。

yarn 依然和 npm 一样是扁平化的 node_modules 结构,没有解决幽灵依赖依赖分身问题。

3️⃣ pnpm

快速的,节省磁盘空间的包管理工具,开创了一套新的依赖管理机制

  • ❣ 内容寻址存储 CAS

与依赖提升和扁平化的 node_modules 不同,pnpm 引入了另一套依赖管理策略:内容寻址存储。

该策略会将包安装在系统的全局 store 中,依赖的每个版本只会在系统中安装一次。

在引用项目 node_modules 的依赖时,会通过硬链接与符号链接在全局 store 中找到这个文件。为了实现此过程,node_modules 下会多出 .pnpm 目录,而且是非扁平化结构。

  • ❣ 硬链接 ****Hard link:硬链接可以理解为源文件的副本,项目里安装的其实是副本,它使得用户可以通过路径引用查找到全局 store 中的源文件,而且这个副本根本不占任何空间。同时,pnpm 会在全局 store 里存储硬链接,不同的项目可以从全局 store 寻找到同一个依赖,大大地节省了磁盘空间。

  • ❣ 符号链接 Symbolic link:也叫软连接,可以理解为快捷方式,pnpm 可以通过它找到对应磁盘目录下的依赖地址。

  • 实际上,存储的目录结构如下所示,仔细解释一下的话,pnmp在安装依赖包时后,会在"node_modules"目录中,会给直接依赖创建指向".pnpm"目录的符号链接,将依赖项连接到正确的位置。在.pnmp目录下,通过符号链接指向直接依赖目录下的node_module里面存放着直接依赖以及其子依赖。

      如下图所示:

node_modules
├─ .pnmp 
  ├─ registry.npmmirror.com+pretty-format@29.6.2/node_modules
      ├─ @jest // pretty-format 子依赖符号链接
      ├─ ansi-styles // pretty-format 子依赖 符号链接
      ├─ pretty-format // pretty-format 本身
         ├─ package.json 
      ├─ react-is // pretty-format 子依赖 符号链接
├─ pretty-format-> ../node_modules/.pnpm/registry.npmmirror.com+pretty-format@29.6.2/

1692602984524.png

image.png

Pnpm addpnpm install

*pnpm add*当我们想要向项目添加新的依赖项时需要使用

*pnpm install*当我们有一个带有锁定文件的现有项目并且我们想要安装锁定文件中的所有依赖项时,我们将需要使用

附言:

以@开头的依赖:在package.json文件中的依赖中,以@开头的通常是指向特定npm命名空间(namespace)的包。在npm中,包名称可以包含命名空间,命名空间是包的一种逻辑组织方式,用于将相关的包归类在一起。

例如,@babel/core中的@babel是一个命名空间,core是这个命名空间下的具体包名称。这个包属于Babel工具链的核心模块,用于将源代码转换为目标代码。

使用命名空间可以帮助npm中的包更好地组织和管理,尤其是当有很多相关包时,它们可以被分组在一起,方便用户查找和使用。

手写扁平化代码

1、写一个JS函数,实现数组扁平化,只减少一次嵌套,如 输入[1,[2,[3]],4] 输出[1,2,[3],4]

思路

  • 定义空数组arr=[] 遍历当前数组
  • 如果item非数组,则累加到arr
  • 如果item是数组,则遍历之后累加到arr
/**
 * 数组扁平化,使用 push
 * @param arr arr
 */
function flatten1(arr) {
  const res = []

  arr.forEach(item => {
    if (Array.isArray(item)) {
      item.forEach(n => res.push(n))
    } else {
      res.push(item)
    }
  })

  return res
}
/**
 * 数组扁平化,使用 concat
 * @param arr arr
 */
function flatten2(arr) {
  let res = []

  arr.forEach(item => {
    res = res.concat(item)
  })

  return res
}

2、手写一个JS函数,实现数组深度扁平化

  思路

  • 先实现一级扁平化,然后递归调用,直到全部扁平化
/**
 * 数组深度扁平化,使用 push
 * @param arr arr
 */
function flattenDeep1(arr) {
  const res = []

  arr.forEach(item => {
    if (Array.isArray(item)) {
      const flatItem = flattenDeep1(item) // 递归
      flatItem.forEach(n => res.push(n))
    } else {
      res.push(item)
    }
  })

  return res
}
/**
 * 数组深度扁平化,使用 concat
 * @param arr arr
 */
function flattenDeep2(arr) {
  let res = []

  arr.forEach(item => {
    if (Array.isArray(item)) {
      const flatItem = flattenDeep2(item) // 递归
      res = res.concat(flatItem)
    } else {
      res = res.concat(item)
    }
  })

  return res
}