哈喽艾瑞巴蒂,我是努力写出优秀技术爽文的 HoMeTown。
node_modules
对做web领域开发的前端同学们可能都不陌生,不知道大家在平时有没有遇到过npm包的依赖地狱问题,或者是想看看node_modules中的代码时被复杂的目录结构劝退的情况。
那么本文将以时间顺序整理一下node_modules中存在的问题以及npm、yarn和pnpm的策略差异。
早期的npm其实依赖关系十分简单,可以直接体现在node_modules的目录结构中。
举个🌰
我现在有一份package.json
如下:
json
"dependencies": { "module_A": "^1.0.0", "module_B": "^1.0.0"}
其中模块之间也有与其他包的依赖,比如:
module_A 还依赖 module_C ^1.0.0module_B 还依赖 module_C ^2.0.0
这个时候使用npm v2
执行npm install
,node_modules下的目录为:
node_modules/ module_A node_modules module_C module_B node_modules module_C
其实很容易理解,只有直接依赖于项目的包才会放在node_modules
的直接目录中。
所以早期的npm依赖解析十分简单直接,但是其中存在了很大的问题,比如:
为了解决上面出现的问题,npm 从v3开始引入了Dedupe,可以简化依赖树,删除重复数据。
举个🌰
我有一份package.json
如下:
json
"dependencies": { "module_A": "^1.0.0", "module_B": "^1.0.0", "module_C": "^1.0.0"}
其中模块之间也有与其他包的依赖,比如:
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下的目录为:
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中,因为版本的不同,所以未进行数据删除。
随着 npm v3 的出现,某些问题已经得到解决。但重复数据删除又带来了一个新问题:
npmv3的新机制导致了这些新的问题的出现,特别是安装顺序的问题。
比如:
在第一个🌰中,如果npm install module_A
是在 npm install module_C
之后的,那么就会出现以下的结构:
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
之后的,就会出现以下的结构:
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相比较有两个很大创新:
我觉得使用.lock
文件是yarn的一个革命性的动作,因为从实际角度而言,package.json本身并不能确定依赖模块的版本,这也是为什么npm install不起作用的原因之一。
通过这些改进,yarn在极大程度上保证了依赖库的可重复性。
首先,yarn已经做的非常好了,但是其实也有一些问题,比如:
yarn.lock
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文件:
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此时还是不能十分准确的确定树形结构。
17年中,出现了一个新的与众不同的包管理器pnpm
。
pnpm 拒绝了使用与npmv3
一样的去重和提升机制,而是使用符号链接
。
第一个栗子执行pnpm时,node_modules如下(省略部分文件):
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在这次毫不遮掩将矛头彻底指向了万恶之源node_modules
,与其大家一起卷优化,不如我直接卷掉需要被优化的东西。
yarn认为node_modules存在几个问题:
所以,Yarn 在想能不能直接与Node进行交互,让他直接去到某个目录下面加载对应的模块,这样可以提高Node模块的搜索效率,节省性能。
索性直接就不创建node_modules了,创建一个名为.png.js
的文件,这是一个node程序,包含了项目的依赖书信息,模块查找算法,在Node环境中,直接覆盖Module._load
方法。
毋庸置疑,这种方式很大的提高了速度。
但是
这种方式会替换Node标准的require,所以有很多包不支持。
太激进了。
前面提到过,npmv3 的新机制出现了安装顺序的问题。终于在npmv7中修复了这个问题(大概率参考yarn),无论npm install的顺序如何,node_modules的树形结构都具备了准确性。
到这个时间点,npm才和yarn有了同样的功能。与此同时,package-lock.json 也升级到了 v2,提高了性能。
npm 在这个版本上添加了一个选项 --install-strategy=linked
,您猜怎么着,符号链接方法也可以在npm上使用了。
跟pnpm差不多,npm生成出来的目录叫.store
。