一个大型项目经常要依赖不少第三方的模块,而第三方的模块又有本身的依赖,假如其中有两个模块依赖了同一个模块的不一样版本,这个时候该模块就要存在两个不一样版本,那么它们在 node_modules 中是如何存在的呢? npm 的大量工做都是在处理这样的版本依赖问题。node
好比你的项目 yxxx,有以下依赖:npm
"dependencies": { A: "1.0.0", C: "1.0.0" }
而 A 和 C 两个模块有以下的依赖关系。json
A@1.0.0 -> B@1.0.0 C@1.0.1 -> B@2.0.0
在 npm v2 时代,执行 npm install
后 node_modules 会是这样的:ui
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 有以下依赖:code
D@1.0.0 -> B@1.0.0
安装完成以后,node_modules 会是这样的:ci
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 存在了两份,这显然是浪费的,这也是被吐槽最多的点,一个项目中存在太多相同版本的模块的副本。class
想一想 require 在寻找模块时候的机制,它会向上级目录去寻找,所以 npm 3 作了改变。require
安装 A@1.0.0 模块,如今的目录结构变为:module
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
好了,这个时候忽然 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 是安装字母顺序排列的。