前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >2015年至今,包管理器与node_modules都发生了什么?

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

作者头像
HoMeTown
发布2023-08-23 10:59:04
1690
发布2023-08-23 10:59:04
举报

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

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

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

早期npm (~2015年)

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

举个🌰

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

代码语言:javascript
复制

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

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

代码语言:javascript
复制


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

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

代码语言:javascript
复制


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如下:

代码语言:javascript
复制

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

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

代码语言:javascript
复制


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

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

代码语言:javascript
复制


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之后的,那么就会出现以下的结构:

代码语言:javascript
复制

bash
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之后的,就会出现以下的结构:

代码语言:javascript
复制

bash
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

代码语言:javascript
复制

json
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文件:

代码语言:javascript
复制

json
{    "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如下(省略部分文件):

代码语言:javascript
复制

bash
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

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

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

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

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2023-06-26,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 秃头开发头秃了 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 早期npm (~2015年)
  • npm v3(2015-06)
  • yarn的出现(2016-10)
  • npm v5(2017-05)
  • pnpm的出现(2017-06)
  • yarn PnP(Plug'n'Play)(2018-09)
  • npm v7(2020-10)
  • npm v9.4.0(2023-0)
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档