美文网首页
常见包管理机制对比

常见包管理机制对比

作者: 薯条你哪里跑 | 来源:发表于2022-06-23 14:29 被阅读0次

1. 早期的npm

早期的npm使用的是嵌套结构,相关依赖会直接嵌套安装在node_modules目录中;
例:项目依赖了A、B、C,之后A依赖D@1.0,B依赖D@2.0,而C也依赖D@1.0。 这是node_modules结构如下(通过npm ls 也可查看包依赖关系)

// 项目的根node_modules
node_modules
    A@1.0.0
         node_modules
            D@1.0.0
    B@1.0.0
        node_modules
            D@2.0.0
    C@1.0.0
        node_modules
            D@1.0.0

可以看是个互相嵌套的结构,即使有公共的D@1.0.0但是还是要安装多次,可想而知当项目依赖变多时会有多复杂,安装时间会有多长;

祭出闻风丧胆的依赖地狱

2. npm v3

v3之后npm采用了扁平的node_module结构,即会将子依赖尽量拍平放置到项目根node_modules中,以尽量减少嵌套导致的深层树和冗余;
依旧是上述的例子: 项目依赖了A、B、C,之后A依赖D@1.0,B依赖D@2.0,而C也依赖D@1.0。 此时node_modules结构如下(注:这种结构是在package.json中写明依赖ABC, 并直接npm i的情况)

// 项目的根node_modules
node_modules
    A@1.0.0
    B@1.0.0
        node_modules
            D@2.0.0
    C@1.0.0
    D@1.0.0

此时当安装到A@1.0.0时发现了 D@1.0.0,于是平级进行安装。当安装到 B@1.0.0时发现依赖D@2.0.0,但此时根部已经安装了D@1.0.0,版本不兼容于是安装在了B@1.0.0内部(如果兼容则不会再次安装,会统一版本进行一次安装,可以参考文中最后最后的示例);

幽灵依赖

也因为有了提升的特性,所以项目中会有幽灵依赖的问题,上述例子中,虽然项目中没有在package.json中显性声明要安装D@1.0.0,但是npm已经将他提升到根部,此时在项目中引用D并进行使用是不会报错的,但是一旦依赖A不再依赖D或者版本有变化那么此时instal后l代码就会因为找不到依赖而报错!!!

不确定性(可解决)

还是上述的例子,当直接npm i 进行依赖安装,由于顺序原因才使得D@1.0.0进行提升,如果是手动安装或者更换package.json中的顺序就会得到不同的结果(什么都不改进行npm i 的时候也有一定几率导致依赖树的不同),例入上述的例子:项目依赖了A、B、C,之后A依赖D@1.0,B依赖D@2.0,而C也依赖D@1.0,此时将
package.json中的顺序顺序改为BAC,那么就会有如下结构:

// 项目的根node_modules
node_modules
    B@1.0.0
    A@1.0.0
    C@1.0.0
        node_modules
            D@1.0.0
    D@2.0.0

可以看到此时是D@2.0被提升,此时再加上幽灵依赖的问题,很容易想象在项目中会遇到什么问题了;

再加上依赖或者子依赖中一般不会写死版本号,当一个依赖A版本是^1.0.3时,当有A升级了版本且有人install的时候,根据package.json的semver-range version 规范,此时安装的有可能就是1.0.4,版本不同有可能会遇到问题;

针对这个问题可以通过npm shrinkwrap来解决,该命令会生成npm-shrinkwrap.json,该文件内会记录各个依赖之间的关系。只不过需要手动执行下命令;

依赖分身

还有一种情况,改一下上述的例子:项目依赖了A、B、C、E,之后A依赖D@1.0,B、E依赖D@2.0,而C也依赖D@1.0,

// 项目的根node_modules
node_modules
    A@1.0.0
    B@1.0.0
        node_modules
            D@2.0.0
    C@1.0.0
    E@1.0.0
        node_modules
            D@2.0.0
    D@1.0.0

可以看到
D@1.0.0已经被提升到外层,此时 D@2.0.0 被B、D依赖就只能在各自的node_modules中再次重复安装了;即使npm在安装依赖的时候会尽量提高复用率,将重复度最高的进行提升,但是D@1.0.0D@2.0.0次数一致时就没法进一步优化了。当D是单例模式或者其他情况下会有问题,毕竟不是一个实例;

3. yarn & npm v5

yarn于2016年问世,它也是使用npm v3扁平化结构管理依赖项。在此基础还解决的npm v3的两大痛点:安装依赖速度慢以及不确定性;

依赖安装速度慢:npm v3是串行安装,按照顺序逐个安装;但是yarn采用并行安装,并且会将包缓存在磁盘上,下次可离线从磁盘上安装;

不确定性:上面讲到根据 package.json生成的node_modules里的结构并不唯一,yarn新增yarn.lock文件;会将package.json中的依赖进行分析,记录依赖和子依赖的关系、版本号以及获取地址和验证模块完整性的hash;通过这种手段可以达到确定性;在此之后npm v5也发布了带有package-lock.json的版本(cnpm无法锁定),也是为了锁定版本,从此无需手动执行npm shrinkwrap,当npm ipackage-lock.json文件会直接生成。(多说一句如果两个文件并存在项目根节点,则会优先根据npm-shrinkwrap.json生成)

这里多说一句npm v5后npm i有了几次变化:

  1. npm 5.0.x版本,不管package.json怎么变,npm i都会根据lock文件下载。
  2. npm 5.1.0版本后,npm i会无视lock文件,直接下载新的npm包;
  3. npm 5.4.2版本后,如果package.json和lock文件不同那么,npm i时会根据package的版本进行下载并更新lock;如果两个文件相同则会根据lock文件下载,不管package有无更新;

但是和npm一样,幽灵依赖依赖分身的问题还是没有得到解决;

4. pnpm

与npm和yarn的依赖提升和扁平化不同,pnpm采取了一套新的策略:内容寻址储存;该策略安装的依赖的每个版本只会在全局中存在唯一一个;
当引用node_module中的依赖时,会通过记录在.pnpm中的信息来使用硬链接与符号链接在全局store中找到这个文件,这里的.pnpm中的数据不是扁平化的。

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

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

还是使用上面的例子: 项目依赖了A、B、C,之后A依赖D@1.0,B依赖D@2.0,而C也依赖D@1.0,使用 pnpm 安装依赖后 node_modules 结构如下

// 项目的根node_modules
node_modules
     .pnpm
           A@1.0.0
                  node_modules
                       A => <store>/A@1.0.0
                       D => ../../D@1.0.0
           D@1.0.0
                  node_modules
                        D => <store>/D@1.0.0
           B@1.0.0
                  node_modules
                       B => <store>/B@1.0.0
                       D => ../../D@2.0.0
           C@1.0.0
                node_modules
                     C => <store>/C@1.0.0
                     D => ../../D@1.0.0
      A => .pnpm/A@1.0.0/node_modules/A
      B => .pnpm/B@1.0.0/node_modules/B
      C => .pnpm/C@1.0.0/node_modules/C

<store>/xxx 开头的路径是硬链接,指向全局 store 中安装的依赖;其余的是软链接,指向依赖的快捷方式。

pnpm的速度要比yarn快很多,对比来看yarn是从缓存中复制文件,而 pnpm 只是从全局存储中链接它们。
pnpm兼容了node的依赖解析并且解决了yarn和npm无法解决的问题:

幽灵依赖问题:子依赖不会被提升,不会产生幽灵依赖。
依赖分身问题:相同的依赖只会在全局 store 中安装一次, 不存在多份相统一来的情况。

但也存在一些弊端:

  1. 在比如 Electron等不支持软链接的环境中,无法使用 pnpm。
  2. 因为依赖源文件是安装在全局 store 中的,调试依赖或 patch-package 给依赖打补丁也不太方便,可能会影响其他项目。

-----------------------以上结束啦-------------------------






-----------------------下面是npm安装兼容/不兼容多版本包实例-------------------------


1.npm安装可兼容多版本依赖的安装实例

以某一项目中的magic-string包为例,查看package-lock.json中依赖关系

// 位置一
// 这里`@rollup/plugin-commonjs`引用的是`^0.25.7`版本的`magic-string`

 "@rollup/plugin-commonjs": {
      "version": "17.0.0",
      "resolved": "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-17.0.0.tgz",
      "integrity": "sha512-/omBIJG1nHQc+bgkYDuLpb/V08QyutP9amOrJRUSlYJZP+b/68gM//D8sxJe3Yry2QnYIr3QjR3x4AlxJEN3GA==",
      "dev": true,
      "requires": {
        ...
        "magic-string": "^0.25.7",
        ...
      },
     ...
  },
...
// 位置二
// 这里`@rollup/plugin-commonjs`引用的也是`^0.25.7`版本的`magic-string`

  "@rollup/plugin-replace": {
      "version": "2.3.4",
      "resolved": "https://registry.npmjs.org/@rollup/plugin-replace/-/plugin-replace-2.3.4.tgz",
      "integrity": "sha512-waBhMzyAtjCL1GwZes2jaE9MjuQ/DQF2BatH3fRivUF3z0JBFrU0U6iBNC/4WR+2rLKhaAhPWDNPYp4mI6RqdQ==",
      "dev": true,
      "requires": {
        ...
        "magic-string": "^0.25.7"
...
      }
    },

// 位置三
// 这里`@rollup/plugin-replace`引用是`^0.25.0`版本的`magic-string`, 出现了不同的版本

 "@surma/rollup-plugin-off-main-thread": {
      "version": "2.2.2",
      "resolved": "https://registry.npmjs.org/@surma/rollup-plugin-off-main-thread/-/rollup-plugin-off-main-thread-2.2.2.tgz",
      "integrity": "sha512-dOD6nGZ79RmWKDRQuC7SOGXMvDkkLwBogu+epfVFMKiy2kOUtLZkb8wV/ettuMt37YJAJKYCKUmxSbZL2LkUQg==",
      "dev": true,
      "requires": {
        "ejs": "^3.1.6",
        "json5": "^2.2.0",
        "magic-string": "^0.25.0"
      },
       ...
    },
...
// 位置四
// `magic-string`包是`0.25.7`版本;

 "magic-string": {
      "version": "0.25.7",
      "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.7.tgz",
      "integrity": "sha512-4CrMT5DOHTDk4HYDlzmwu4FVCcIYI8gauveasrdCu2IKIFOJ3f0v/8MDGJCDL9oD2ppz/Av1b0Nj345H9M+XIA==",
      "dev": true,
      "requires": {
        "sourcemap-codec": "^1.4.4"
      }
  },
...

我们工程里 package-lock.json 中涉及magic-string的只有以上4个位置,并且通过查看requires字段可以发现,我们工程并没有直接安装magic-string,可以理解为位置四中的配置项是npm分析过依赖关系后,为了公用而提取到外层的;
那么位置三^0.25.0版本被安装到哪里了么,npm是怎么处理的呢,我们看看node_modules/@surma/rollup-plugin-off-main-thread/package.json

  "dependencies": {
    "ejs": "^3.1.6",
    "json5": "^2.2.0",
    "magic-string": "^0.25.0"
  },

ok没错使用了magic-string,但是当我查看node_module/@surma/rollup-plugin-off-main-thread/node_modules时发现:

并没有将magic-string安装至此

里面并没有magic-string,回头看下,magic-string一共有有两个版本^0.25.0"^0.25.7"
这里的^表示主本兼容,即0.X.X的版本都可以;
例:^0.25.0>=0.25.0且<1.X.X
一切都很明了了,由于外部已经安装了"^0.25.7""^0.25.0"就无需重复安装了;

2. npm安装无法兼容多版本依赖的安装实例

再拿另一个包来举例:json5,打开 package-lock.json

 // 位置一
// 这里引用是`^2.2.0`版本的`json5`

"@surma/rollup-plugin-off-main-thread": {
  "version": "2.2.2",
  "resolved": "https://registry.npmjs.org/@surma/rollup-plugin-off-main-thread/-/rollup-plugin-off-main-thread-2.2.2.tgz",
  "integrity": "sha512-dOD6nGZ79RmWKDRQuC7SOGXMvDkkLwBogu+epfVFMKiy2kOUtLZkb8wV/ettuMt37YJAJKYCKUmxSbZL2LkUQg==",
  "dev": true,
  "requires": {
    "ejs": "^3.1.6",
    "json5": "^2.2.0",
    "magic-string": "^0.25.0"
  },
  "dependencies": {
    "json5": {
      "version": "2.2.0",
      "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.0.tgz",
      "integrity": "sha512-f+8cldu7X/y7RAJurMEJmdoKXGB/X550w2Nr3tTbezL6RwEE/iMcm+tZnXeoZtKuOq6ft8+CqzEkrIgx1fPoQA==",
      "dev": true,
      "requires": {
        "minimist": "^1.2.5"
      }
    }
  }
},
...
 // 位置二
// 这里引用是`"^1.0.1`版本的`json5`

"loader-utils": {
  "version": "1.4.0",
  "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.4.0.tgz",
  "integrity": "sha512-qH0WSMBtn/oHuwjy/NucEgbx5dbxxnxup9s4PVXJUDHZBQY+s0NWA9rJf53RBnQZxfch7euUui7hpoAPvALZdA==",
  "dev": true,
  "requires": {
    "big.js": "^5.2.2",
    "emojis-list": "^3.0.0",
    "json5": "^1.0.1"
  }
},
...
 // 位置三
// 这里是"1.0.1“版本的`json5`

"json5": {
  "version": "1.0.1",
  "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz",
  "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==",
  "dev": true,
  "requires": {
    "minimist": "^1.2.0"
  }
},

同样我们项目的package.json中也并没有直接引用,可以看到工程中安装的是1.0.1"@surma/rollup-plugin-off-main-thread"中需要的是2.2.0版本无法兼容。查看"@surma/rollup-plugin-off-main-thread"中的的node_modules

安装在了里面

所有package的依赖安装时都尽量拍平之后安装到项目根目录的node_modules里,并且避免各个package重复安装第三方依赖,将有冲突的依赖,安装在自己package的node_modules里,解决依赖的版本冲突问题。

至于为什么提升的是1.0.1而不是2.2.0版本呢?这个开始以为是和依赖树层级或者包版本大小有关,但不停删除lock和node_module进行重装,实测之后发现是随机的。由于node_module是根据package.lock来安装的,所以所以所以所以!!为了保证唯一性最好将lock也一起进行git托管!!!

相关文章

  • 常见包管理机制对比

    1. 早期的npm 早期的npm使用的是嵌套结构,相关依赖会直接嵌套安装在node_modules目录中;例:项...

  • 内存优化(app专项测试)

    1.优化内存的常见操作 (一)内存管理机制 (二)常用内存监控工具 (三)内存优化案例分析 2.内存管理机制 AR...

  • 关于js闭包结合定时器的一个题目

    先看fn1()函数: 我们可以对比之前我们常见的: 而在闭包内添加了clearTimeout(tc);之后为什么3...

  • iOS面试进阶篇(一)

    目录 OC的理解与特性OC内存管理机制MVC与MVVM设计模型对比垃圾回收机制协议,分类,KVC,KVO,代理,通...

  • iOS马甲包审核以及常见审核问题

    iOS马甲包审核以及常见审核问题 iOS马甲包审核以及常见审核问题

  • OC与C的对比

    一 OC与C的对比 1、源文件对比 .c语言中常见的源文件...

  • u3d_插件DoTween:(01)DoTween动画插件介绍和

    一、插件包 二、导入插件包 三、性能对比

  • 常见并发模型对比

    并发模型【UNP】对应多进程多线程阻塞IOIO复用长连接并发性多核开销互通顺序性线程数特点accept+read/...

  • 2018-09-26

    笔试要点: ①JVM内存管理机制和垃圾回收机制 ②JVM内存调优 ③ 设计模式(熟悉常见设计模式的应用场景,会画类...

  • Pytorch常见包

    torch:包含所有的工具包以及tensor库 torch.nn:包含构造神经网络的一些基本模块 torch.nn...

网友评论

      本文标题:常见包管理机制对比

      本文链接:https://www.haomeiwen.com/subject/gskevrtx.html