likes
comments
collection
share

2015年至今,包管理器与node_modules都发生了什么?

作者站长头像
站长
· 阅读数 37

哈喽艾瑞巴蒂,我是努力写出优秀技术爽文的 HoMeTown

node_modules对做web领域开发的前端同学们可能都不陌生,不知道大家在平时有没有遇到过npm包的依赖地狱问题,或者是想看看node_modules中的代码时被复杂的目录结构劝退的情况。

那么本文将以时间顺序整理一下node_modules中存在的问题以及npm、yarn和pnpm的策略差异。

2015年至今,包管理器与node_modules都发生了什么?

早期npm (~2015年)

早期的npm其实依赖关系十分简单,可以直接体现在node_modules的目录结构中。

举个🌰

我现在有一份package.json如下:

"dependencies": {
    "module_A": "^1.0.0",
    "module_B": "^1.0.0"
}

其中模块之间也有与其他包的依赖,比如:

2015年至今,包管理器与node_modules都发生了什么?

module_A 还依赖 module_C ^1.0.0
module_B 还依赖 module_C ^2.0.0

这个时候使用npm v2执行npm install,node_modules下的目录为:

2015年至今,包管理器与node_modules都发生了什么?

node_modules/
    module_A
        node_modules
            module_C
    module_B
        node_modules
            module_C

其实很容易理解,只有直接依赖于项目的包才会放在node_modules的直接目录中。

npm v3(2015-06)

所以早期的npm依赖解析十分简单直接,但是其中存在了很大的问题,比如:

  • 依赖关系越深,目录结构就越深。
  • 同一个包会出现多次,造成磁盘空间压力变大,安装速度变慢。

为了解决上面出现的问题,npm 从v3开始引入了Dedupe,可以简化依赖树,删除重复数据。

举个🌰

我有一份package.json如下:

"dependencies": {
    "module_A": "^1.0.0",
    "module_B": "^1.0.0",
    "module_C": "^1.0.0"
}

其中模块之间也有与其他包的依赖,比如:

2015年至今,包管理器与node_modules都发生了什么?

module_A 还依赖 module_D ^1.0.0 
module_B 还依赖 module_D ^2.0.0
module_C 还依赖 module_D ^2.0.0

这个时候使用npm v2执行npm install,node_modules下的目录为:

2015年至今,包管理器与node_modules都发生了什么?

node_modules/
    module_A
        node_modules
            module_D
    module_B
    module_C
    module_D

此时在npmv3的版本中,module_D ^2.0.0被安装在了父级目录中,因为它在依赖项中是重复的,在npm中叫做提升

与此同时 module_D ^1.0.0 原封不动的还在 module_A目录下的node_module中,因为版本的不同,所以未进行数据删除。

yarn的出现(2016-10)

随着 npm v3 的出现,某些问题已经得到解决。但重复数据删除又带来了一个新问题:

  • 可以引入不直接依赖的模块(比如上面的栗子中,root引入module_D的问题),因为通过npm提升,node_modules中工会出现一些没有直接写在 root dependency中的模块,换句话说没有写在dependency中的模块也可以被引入。
  • 删除重复数据 npm install的性能较差。
  • node_modules下的目录结构根据npm install的安装顺序而变化。

npmv3的新机制导致了这些新的问题的出现,特别是安装顺序的问题。

比如:

在第一个🌰中,如果npm install module_A 是在 npm install module_C之后的,那么就会出现以下的结构:

node_modules/
    module_A
    module_C # 1.0.0
    module_B
        node_modules 
            module_C # 2.0.0

如果npm install module_C 是在 npm install module_A之后的,就会出现以下的结构:

node_modules/
    module_A
        node_modules 
            module_C # 1.0.0
    module_C # 2.0.0
    module_B

这是一个非常严重的问题,而且存在多人协同开发下,node_modules的结构不同的问题。

针对这个问题,fb 推出了yarn,yarn与npmv3相比较有两个很大创新:

  • 算法不再修改树结构
  • 使用锁定文件 (yarn.lock) 进行版本控制

我觉得使用.lock文件是yarn的一个革命性的动作,因为从实际角度而言,package.json本身并不能确定依赖模块的版本,这也是为什么npm install不起作用的原因之一。

通过这些改进,yarn在极大程度上保证了依赖库的可重复性。

npm v5(2017-05)

首先,yarn已经做的非常好了,但是其实也有一些问题,比如:

yarn.lock

module_A@^1.0.0:
  version "1.0.0"
  resolved "https://registry.yarnpkg.com/module_A/-/module_A-1.0.0.tgz#d27217e16777d7c0c14b2d49e365119de3bf4da0"
  integrity sha512-LHSY3BAvHk8CV3O2J2zraDq10+VI1QT1yCTildRW12JSWwFvsnzwLhdOdrJG2gaHHIya7N4GndK+ZFh1bTBjFw==
  dependencies:
    module_C "^1.0.0"

module_C@^1.0.0:
  version "1.0.0"
  resolved "https://registry.yarnpkg.com/module_C/-/module_C-1.0.0.tgz#0d6e560f07d533708a39693b5de7188db74b66b8"
  integrity sha512-w3+jMEBzh6ap32RoJkmkFSIi6EmBYArDviaA9mAri/zfhu5pKcIFhyiGdtt9Ce9Wz6aF7wkkL9hMd3F4XWgjsA==

module_C@^2.0.0:
  version "2.0.0"
  resolved "https://registry.yarnpkg.com/module_C/-/module_C-2.0.0.tgz#d3c10b5815b31689a51b7c7d84341825353a2382"
  integrity sha512-F1mbrVGqDeid+VoEdswLYsznXnTG/k8xf5aYRTX7ifhzWk9yzwQJPq5wHikqx+/eLzwEaj9tjVQSLO2prdRZew==

module_B@^1.0.0:
  version "1.0.0"
  resolved "https://registry.yarnpkg.com/module_B/-/module_B-1.0.0.tgz#849adb050fcb7f5dd463b105dbf23771a3bd9df0"
  integrity sha512-aUhu8lL4T+UYGNi9qd+DqBfCuDaZxkBJ0gDC5lS9WhQmLusTncROjXL0W8JvVe3mvwrbJCTTbyJ8SJpm1pd9Og==
  dependencies:
    module_C "^2.0.0"

可以看到,在yarn.lock中仅仅只包含了包的相关信息,不包含node_modules的树形结构信息。

node_modules下的树形结构根据yarn的树形算法而改变,即yarn的版本!那么,我们又需要控制yarn的版本(好像俄罗斯套娃🪆)。

因此,从npm v5开始,引入了一个大家现在都能看到的package-lock.json锁文件。下面是用npm v5给第一个例子生成一个lock文件:

{
    "name": "node",
    "version": "1.0.0",
    "lockfileVersion": 1,
    "requires": true,
    "dependencies": {
      "module_A": {
        "version": "1.0.0",
        "resolved": "https://registry.npmjs.org/module_A/-/module_A-1.0.0.tgz",
        "integrity": "sha512-LHSY3BAvHk8CV3O2J2zraDq10+VI1QT1yCTildRW12JSWwFvsnzwLhdOdrJG2gaHHIya7N4GndK+ZFh1bTBjFw==",
        "requires": {
          "module_C": "1.0.0"
        },
        "dependencies": {
          "module_C": {
            "version": "1.0.0",
            "resolved": "https://registry.npmjs.org/module_C/-/module_C-1.0.0.tgz",
            "integrity": "sha512-w3+jMEBzh6ap32RoJkmkFSIi6EmBYArDviaA9mAri/zfhu5pKcIFhyiGdtt9Ce9Wz6aF7wkkL9hMd3F4XWgjsA=="
          }
        }
      },
      "module_B": {
        "version": "1.0.0",
        "resolved": "https://registry.npmjs.org/module_B/-/module_B-1.0.0.tgz",
        "integrity": "sha512-aUhu8lL4T+UYGNi9qd+DqBfCuDaZxkBJ0gDC5lS9WhQmLusTncROjXL0W8JvVe3mvwrbJCTTbyJ8SJpm1pd9Og==",
        "requires": {
          "module_C": "2.0.0"
        }
      }
      "module_C": {
        "version": "2.0.0",
        "resolved": "https://registry.npmjs.org/module_C/-/module_C-2.0.0.tgz",
        "integrity": "sha512-F1mbrVGqDeid+VoEdswLYsznXnTG/k8xf5aYRTX7ifhzWk9yzwQJPq5wHikqx+/eLzwEaj9tjVQSLO2prdRZew=="
      }
    }
  }

可以看到 package-lock.json 中包含树形结构!

npm 又在这一方面力压 yarn一次。

但是!

npm此时还是不能十分准确的确定树形结构。

pnpm的出现(2017-06)

17年中,出现了一个新的与众不同的包管理器pnpm

pnpm 拒绝了使用与npmv3一样的去重和提升机制,而是使用符号链接

第一个栗子执行pnpm时,node_modules如下(省略部分文件):

node_modules
    .pnpm
        module_A@1.0.0
            node_modules
                module_A
                    package.json
                module_C -> ../../module_C@1.0.0/node_modules/module_C
        module_B@1.0.0
            node_modules
                module_B
                    package.json
                module_C -> ../../module_C@2.0.0/node_modules/module_C
        module_C@1.0.0
            node_modules
                module_C
        module_C@2.0.0
            node_modules
                module_C
    module_A -> .pnpm/module_A@1.0.0/node_modules/module_A
    module_B -> .pnpm/module_B@1.0.0/node_modules/module_B

其实这个结构与早期node_modules的结构十分相似,只是多了很多符号链接,而且我个人认为,这个结构也非常简单易懂,而且通过符号链接解决了模块重复的问题。

所以从目前来看,pnpm的符号链接我认为似乎是最合理的方式,通过一个引用符号,指向具体的依赖包,那么为什么npm v3或者yarn当时没有选择采用这样的方式呢?

难道因为windows的路径字符限制?不想嵌套太多的目录层级?那pnpm没有遇到这个问题吗?

然后我去看了 pnpm关于windows上node_modules的处理方式,官方有个qa

2015年至今,包管理器与node_modules都发生了什么?

大概是说他们说可以在windows上运行,在windows上使用符号链接多多少少有点问题,但是pnpm用 junctions 的方式解决了这个问题。

关于硬链接,微软有关于这个的解释,先贴张图,我没来得及仔细看,大概就是一种映射关系吧,感兴趣的朋友可以详细了解一下,结论可以在评论区交流一下

2015年至今,包管理器与node_modules都发生了什么?

yarn PnP(Plug'n'Play)(2018-09)

yarn在这次毫不遮掩将矛头彻底指向了万恶之源node_modules,与其大家一起卷优化,不如我直接卷掉需要被优化的东西。

yarn认为node_modules存在几个问题:

  • 当Node调用require的时候,它其实只是搜索文件系统,一直搜索到找到匹配项之后使用,出现了大量文件系统I/O,太浪费性能,也是Node启动慢的主要原因。
  • 当包管理器创建node_modules时,会从缓存中复制一大堆文件,这是安装慢的主要原因。

所以,Yarn 在想能不能直接与Node进行交互,让他直接去到某个目录下面加载对应的模块,这样可以提高Node模块的搜索效率,节省性能。

索性直接就不创建node_modules了,创建一个名为.png.js的文件,这是一个node程序,包含了项目的依赖书信息,模块查找算法,在Node环境中,直接覆盖Module._load方法。

毋庸置疑,这种方式很大的提高了速度。

但是

这种方式会替换Node标准的require,所以有很多包不支持。

太激进了。

npm v7(2020-10)

前面提到过,npmv3 的新机制出现了安装顺序的问题。终于在npmv7中修复了这个问题(大概率参考yarn),无论npm install的顺序如何,node_modules的树形结构都具备了准确性。

到这个时间点,npm才和yarn有了同样的功能。与此同时,package-lock.json 也升级到了 v2,提高了性能。

npm v9.4.0(2023-0)

npm 在这个版本上添加了一个选项 --install-strategy=linked,您猜怎么着,符号链接方法也可以在npm上使用了。

跟pnpm差不多,npm生成出来的目录叫.store

end

基本就是这些,我按照时间顺序大概解释了node_modules的问题以及每个包管理这些年来的努力。

从技术的角度上可以了解一下整个发展史。

从别的角度出发,着实有点相互较量的意思。

但是我个人觉得,从npm目前的更新进度与内容来看,基本也尘埃落定了。

尤其是npm v7之后,yarn感觉从市场上已经慢慢消失了,即使覆盖 require 的方法没有被大家认可,但是确实从性能的角度来看是十分有效的。

pnpm 依靠符号链接和monorepo架构还能与npm 再比比划划。

但是从npm v9的情况来看,留给pnpm的时间恐怕也不多了。

毕竟npm才是node的亲儿子。

但是

还是要尊重 yarn、pnpm这么多年做出的努力,瑞思拜!

2015年至今,包管理器与node_modules都发生了什么?

下次见~ 我的朋友,我是HoMeTown👨‍💻‍,➕我VX,💊你进群,这是一个大家共同成长、共同学习的社群!群内十分活跃,至于有多活跃,你来了就知道了!在这里你可以:讨论技术问题、了解前端资讯、打听应聘公司、获得内推机会、聊点有的没的。

👉 vx: hometown-468

👨‍👩‍👧 公众号:秃头开发头秃了 【关注回复“进群”】

🤖 Github:HoMeTownSoCool

高赞好文

转载自:https://juejin.cn/post/7248896036709630009
评论
请登录