一个大型项目常常要依赖很多第三方的模块,而第三方的模块又有自己的依赖,假如其中有两个模块依赖了同一个模块的不同版本,这个时候该模块就要存在两个不同版本,那么它们在 node_modules 中是如何存在的呢? npm 的大量工作都是在处理这样的版本依赖问题。
比如你的项目 yxxx,有如下依赖:
"dependencies": {
A: "1.0.0",
C: "1.0.0"
}
而 A 和 C 两个模块有如下的依赖关系。
A@1.0.0 -> B@1.0.0
C@1.0.1 -> B@2.0.0
npm v2 时代
在 npm v2 时代,执行 npm install
后 node_modules 会是这样的:
node_modules
├── A@1.0.0
│ └── node_modules
│ └── B@1.0.0
└── C@1.0.0
└── node_modules
└── B@2.0.0
这个时候如果再安装一个模块 D@1.0.0,D 有如下依赖:
D@1.0.0 -> B@1.0.0
安装完成之后,node_modules 会是这样的:
node_modules
├── A@1.0.0
│ └── node_modules
│ └── B@1.0.0
├── C@1.0.0
│ └── node_modules
│ └── B@2.0.0
└── D@1.0.0
└── node_modules
└── B@1.0.0
B@1.0.0 存在了两份,这显然是浪费的,这也是被吐槽最多的点,一个项目中存在太多相同版本的模块的副本。
想想 require 在寻找模块时候的机制,它会向上级目录去寻找,因此 npm 3 做了改变。
npm v3 时代
安装 A@1.0.0 模块,现在的目录结构变为:
node_modules
├── A@1.0.0
└── B@1.0.0
可以看到他们存在于同一级目录,这个时候 A 中的 js 脚本在 A 中找不到 node_modules 后会在父级目录中找到 B 模块。
继续安装 C@1.0.0 模块,因为 C@1.0.0 依赖的是 B@2.0.0 模块,而此时在 node_modules 中已经存在了 B@1.0.0,因此安装后的目录结构是这样的:
node_modules
├── A@1.0.0
├── B@1.0.0
└── C@1.0.0
└── node_modules
└── B@2.0.0
现在继续安装一个模块 E@1.0.0,它有如下依赖:
E@1.0.0 -> B@2.0.0
其实 B@2.0.0 已经存在了,只是它位于 C@1.0.0 模块下,
安装完成后目录结构变为了:
node_modules
├── A@1.0.0
├── B@1.0.0
├── C@1.0.0
│ └── node_modules
│ └── B@2.0.0
└── E@1.0.0
└── node_modules
└── B@2.0.0
这个时候 B@2.0.0 又存在了两份。Ok,继续安装一个模块 F@1.0.0,它的依赖关系如下:
F@1.0.0 -> B@1.0.0
这个时候因为 B@1.0.0 已经存在于项目根目录下的 node_modules 中了,因此目录结构是这样的:
├── A@1.0.0
├── B@1.0.0
├── C@1.0.0
│ └── node_modules
│ └── B@2.0.0
├── D@1.0.0
│ └── node_modules
│ └── B@2.0.0
├── E@1.0.0
│ └── node_modules
│ └── B@2.0.0
└── F@1.0.0
npm v3 去重
好了,这个时候突然 A@1.0.0 需要升级到 2.0.0 版本,依赖关系也变为了:
A@2.0.0 -> B@2.0.0
安装后目录结构变为了:
node_modules
├── A@2.0.0
│ └── node_modules
│ └── B@2.0.0
├── B@1.0.0
├── C@1.0.0
│ └── node_modules
│ └── B@2.0.0
├── D@1.0.0
│ └── node_modules
│ └── B@2.0.0
├── E@1.0.0
│ └── node_modules
│ └── B@2.0.0
└── F@1.0.0
随后 F 模块也升级至 2.0.0 版本了,依赖关系也变为了:
F@2.0.0 -> B@2.0.0
执行安装,在这个过程中首先会移除掉,F@1.0.0 然后发现,B@1.0.0 已经没有模块依赖它了,因此也移除了 B@1.0.0,然后安装 F@2.0.0,并安装其依赖 B@2.0.0,发现项目根目录的 node_modules
中并没有 B 模块的任何版本,于是就安装在了根目录的 node_modules
中。
得到目录结构为:
node_modules
├── A@2.0.0
│ └── node_modules
│ └── B@2.0.0
├── B@2.0.0
├── C@1.0.0
│ └── node_modules
│ └── B@2.0.0
├── D@1.0.0
│ └── node_modules
│ └── B@2.0.0
├── E@1.0.0
│ └── node_modules
│ └── B@2.0.0
└── F@2.0.0
坑爹呢,B@2.0.0 存在了很多个副本了。但也不要紧张,通常 npm 会利用链接来将多个副本指向同一个模块。这样的目录结构虽然觉得有些浪费,但是对代码运行没有丝毫影响。也许你想让他好看一点,没有问题,执行命令:
npm dedupe
该命令会遍历模块依赖树,根据模块之间的依赖关系,移动模块的位置,去除重复,让整个 node_modules 的目录结构更加扁平一些。
node_modules
├── A@2.0.0
├── B@2.0.0
├── C@1.0.0
├── D@1.0.0
├── E@1.0.0
└── F@2.0.0
node_modules
目录结构的不确定性
模块的安装次序决定了 node_modules
中的目录结构,这也是为什么明明 dependencies 中依赖的模块但得到的目录结构不同,假如有如下两个模块需要安装:
A@1.0.0 -> B@1.0.0
C@1.0.0 -> B@2.0.0
安装 A 和 C 的次序不同得到的 node_modules
也就不同,因为 npm 会优先将模块放置在根目录下的 node_modules
中,所以先安装 A 和 C 中的哪一个决定了在 根目录下的 node_modules
中存在的是 B 的 2.0.0 版本还是 1.0.0 版本。
只有在手动使用 npm i <package> --save
的时候才会出现这种情况,使用 npm i
,npm 会去读取 package.json
中的 dependencies,而 dependencies 是安装字母顺序排列的。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。