详情见:
https://www.yuque.com/docs/share/d64606fd-0e7b-4423-b200-93bdbbb1c4e9?# 《npm 如何处理依赖与依赖冲突》
图片懒得复制了
npm2是如何处理依赖的?
假设有三个模块:A、B、C。A依赖B@1.0,C依赖B@2.0。
现在,我们创建一个应用,它同时以来A、C。
依赖地狱
npm2会这么处理:
此时的目录结构是这样的:
此时执行npm ls查看包的依赖关系是这样的:
问题
这么做带来很多问题
- 依赖树的层级非常深。如果需要定位某依赖的依赖,很难找到该依赖的文件所在(例如,如果想定位模块 E,就不得不先知道他在依赖树中的位置);
- 不同的依赖树分支里,可能有大量实际上是同样版本的依赖(例如,A 目录下的 C 和 B 目录下面的 C 如果版本一致,实际上完全一样);
- 安装时额外下载或拷贝了大量重复的资源,并且实际上也占用了大量的硬盘空间资源等(例如,C 模块在依赖目录中出现了两次);
- 安装速度慢,甚至因为目录层级太深导致文件路径太长的缘故,在 windows 系统下删除 node_modules 文件夹也可能失败!
npm3是如何处理依赖的?
npm3 解决依赖关系的方式与 npm2 不同。
npm2是以嵌套方式安装所有依赖项,但 npm3 试图减轻这种嵌套导致的深度和冗余。 npm3 通过把一些secondary依赖项和primary依赖项平铺在同一层级的方式来实现这一点。
主要的区别是:
● 目录结构中的位置不再代表着依赖项的类型(primary、secondary等)
● node_modules的目录结构取决于安装顺序
●
假设我们有一个模块 A,A 依赖 B。
现在,我们创建一个应用,它依赖A。
执行npm install, npm v3会把A、B以及他们的依赖都下载到/node_modules下平铺。在npm v2中,他们会被嵌套下载。
现在如果我们还需要依赖C,C依赖于B,但是依赖的是B@2.0。
因为B@1.0已经存在于顶层目录中了,所以我们不能把B@2.0也下载到顶层目录。npm v3此时会和npm v2一样的嵌套方式去处理新的依赖B@2.0,把它放在依赖它的目录下,也就是C@1.0下,如下图:
此时的目录结构是这样的:
此时执行npm ls查看包的依赖关系是这样的:
如果你只想看primary依赖,执行npm ls --depth=0
npm3是如何处理重复的?
接着上面,我们继续。如果我们现在要再下一个模块D,同时D依赖于B@2.0呢?
因为B@1.0已经存在于顶层目录中了,所以我们不能把B@2.0也下载到顶层目录。此时B@2.0会以嵌套的方式下载到模块D下面,尽管在C的目录下已经有了一个B@2.0了。
可以看出,如果一个secondary的依赖同时被两个primary依赖所依赖,但是这个secondary依赖并没有被下载到顶层目录中,那么它将会被分别下载到这两个primary依赖的目录下,这就出现了重复!
但是如果说这个secondary的依赖已经被下载到顶层目录中,那它将会在这两个primary依赖中共享,并不会出现重复下载!
比如说,我们现在还需要下载一个模块E,模块E和A一样,依赖于B@1.0。
因为B@1.0已经在顶层目录了,所以我们不需要重复下载它,只需要单纯的下载E,它会和A共享B。
现在的目录结构是这样的:
现在,如果我们A升级到了2.0,而A@2.0依赖的不再是B@1.0,而是B@2.0呢?会发生什么?
要解决这个问题,最关键的是要记住,下载顺序很重要!
即使说A@1.0是第一个被package,.json下载的(因为它是按字母顺序排序的),但当时你执行npm install后,就意味着A@2.0是最新下载的包!
当我们执行npm install mod-a@2 --save后,npm3会做一下的事情:
● 移除A@1.0
● 下载A@2.0
● 它会保留顶层的B@1.0,因为E@2.0还需要它
● 它会把B@2.0下载到A@2.0的目录下去,因为此时顶层有B@1.0在
最后,如果我们把E更新到v2.0,E@2.0依赖B@2.0,会发生什么?
npm3会做以下事情:
● 移除E@1.0
● 下载E@2.0
● 移除掉B@1.0,因为没有模块还需要依赖它
● 把B@2.0下载到顶层目录,因为顶层目录已经没有任何版本的B了
现在这样显然还不够理想,太多重复的B@2.0了。为了去掉重复,我们可以执行npm dedupe,这个命令会将所有依赖B@2.0的包的依赖重定向到顶层的B@2.0上,并且把它们下面的B@2.0移除掉。
npm3的不确定性
现在让我们跳回到之前一个例子中
此时你的package.json 是这样的
{
“name”: “example3”,
“version”: “1.0.0”,
“description”: “”,
“main”: “index.js”,
“scripts”: {
“test”: “echo “Error: no test specified” && exit 1”
},
“keywords”: [],
“author”: “”,
“license”: “ISC”,
“dependencies”: {
“mod-a”: “^1.0.0”,
“mod-c”: “^1.0.0”,
“mod-d”: “^1.0.0”,
“mod-e”: “^1.0.0”
}
}
现在如果你执行npm install mod-a@2 --save,我们的目录结构将会变成这样:
而你的package.json也更新了,此时是这样的:
{
“name”: “example3”,
“version”: “1.0.0”,
“description”: “”,
“main”: “index.js”,
“scripts”: {
“test”: “echo “Error: no test specified” && exit 1”
},
“keywords”: [],
“author”: “”,
“license”: “ISC”,
“dependencies”: {
“mod-a”: “^2.0.0”,
“mod-c”: “^1.0.0”,
“mod-d”: “^1.0.0”,
“mod-e”: “^1.0.0”
}
}
而如果此时你的同事小高拿到最新的项目,执行npm install,得到的目录将会是这样的
what?竟然和你刚才执行的结果完全不同?此时只要记住,下载顺序很重要!
因为小高拿到新的项目,执行npm install,此时的下载顺序是由package.json中的字母顺序决定的,第一个下载的就是A@2.0,随即把B2.0也到了顶层。
所以npm3并不能总是保证每个人的目录结构是一样,但是这并不影响你的应用!一切都可以正常进行!
如果你实在是想让node_modules的目录结构保持一致的话,你可以先把node_modules都删掉,然后再执行npm install,因为此时的顺序永远是package.json中的字母顺序!
参考链接
- https://npm.github.io/how-npm-works-docs/npm2/how-npm2-works.html
- http://aprilandjan.github.io/npm/2019/08/02/how-npm-handles-dependency-version-conflict/