常见包管理机制对比

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或者版本有变化那么此时install后代码就会因为找不到依赖而报错!!!

不确定性(可解决)

还是上述的例子,当直接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托管!!!

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 219,188评论 6 508
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,464评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 165,562评论 0 356
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,893评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,917评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,708评论 1 305
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,430评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,342评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,801评论 1 317
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,976评论 3 337
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,115评论 1 351
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,804评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,458评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,008评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,135评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,365评论 3 373
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,055评论 2 355

推荐阅读更多精彩内容