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

常见包管理机制对比

作者: 薯条你哪里跑 | 来源:发表于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托管!!!

    相关文章

      网友评论

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

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