1. 早期的npm
早期的npm使用的是嵌套结构,相关依赖会直接嵌套安装在node_modules目录中;
例:项目依赖了A、B、C,之后A依赖D@1.0,B依赖D@2.0,而C也依赖D@1.0。 这是node_modules结构如下(通过npm ls
也可查看包依赖关系)
// 项目的根node_modules
node_modules
A@1.0.0
node_modules
D@1.0.0
B@1.0.0
node_modules
D@2.0.0
C@1.0.0
node_modules
D@1.0.0
可以看是个互相嵌套的结构,即使有公共的D@1.0.0
但是还是要安装多次,可想而知当项目依赖变多时会有多复杂,安装时间会有多长;
![](https://img.haomeiwen.com/i4727382/166c28ab6a312f25.png)
2. npm v3
v3之后npm采用了扁平的node_module结构,即会将子依赖尽量拍平放置到项目根node_modules中,以尽量减少嵌套导致的深层树和冗余;
依旧是上述的例子: 项目依赖了A、B、C,之后A依赖D@1.0,B依赖D@2.0,而C也依赖D@1.0。 此时node_modules结构如下(注:这种结构是在package.json中写明依赖ABC, 并直接npm i的情况)
// 项目的根node_modules
node_modules
A@1.0.0
B@1.0.0
node_modules
D@2.0.0
C@1.0.0
D@1.0.0
此时当安装到A@1.0.0时发现了 D@1.0.0,于是平级进行安装。当安装到 B@1.0.0时发现依赖D@2.0.0,但此时根部已经安装了D@1.0.0,版本不兼容于是安装在了B@1.0.0内部(如果兼容则不会再次安装,会统一版本进行一次安装,可以参考文中最后最后的示例);
幽灵依赖
也因为有了提升的特性,所以项目中会有幽灵依赖
的问题,上述例子中,虽然项目中没有在package.json
中显性声明要安装D@1.0.0,但是npm已经将他提升到根部,此时在项目中引用D并进行使用是不会报错的,但是一旦依赖A不再依赖D或者版本有变化那么此时instal后l代码就会因为找不到依赖而报错!!!
不确定性(可解决)
还是上述的例子,当直接npm i 进行依赖安装,由于顺序原因才使得D@1.0.0
进行提升,如果是手动安装或者更换package.json中的顺序就会得到不同的结果(什么都不改进行npm i 的时候也有一定几率导致依赖树的不同),例入上述的例子:项目依赖了A、B、C,之后A依赖D@1.0,B依赖D@2.0,而C也依赖D@1.0,此时将
package.json中的顺序顺序改为BAC,那么就会有如下结构:
// 项目的根node_modules
node_modules
B@1.0.0
A@1.0.0
C@1.0.0
node_modules
D@1.0.0
D@2.0.0
可以看到此时是D@2.0
被提升,此时再加上幽灵依赖
的问题,很容易想象在项目中会遇到什么问题了;
再加上依赖或者子依赖中一般不会写死版本号,当一个依赖A版本是^1.0.3
时,当有A升级了版本且有人install的时候,根据package.json的semver-range version 规范,此时安装的有可能就是1.0.4
,版本不同有可能会遇到问题;
针对这个问题可以通过npm shrinkwrap
来解决,该命令会生成npm-shrinkwrap.json
,该文件内会记录各个依赖之间的关系。只不过需要手动执行下命令;
依赖分身
还有一种情况,改一下上述的例子:项目依赖了A、B、C、E,之后A依赖D@1.0,B、E依赖D@2.0,而C也依赖D@1.0,
// 项目的根node_modules
node_modules
A@1.0.0
B@1.0.0
node_modules
D@2.0.0
C@1.0.0
E@1.0.0
node_modules
D@2.0.0
D@1.0.0
可以看到
D@1.0.0已经被提升到外层,此时 D@2.0.0 被B、D依赖就只能在各自的node_modules
中再次重复安装了;即使npm在安装依赖的时候会尽量提高复用率,将重复度最高的进行提升
,但是D@1.0.0
和D@2.0.0
次数一致时就没法进一步优化了。当D是单例模式或者其他情况下会有问题,毕竟不是一个实例;
3. yarn & npm v5
yarn于2016年问世,它也是使用npm v3扁平化结构管理依赖项。在此基础还解决的npm v3的两大痛点:安装依赖速度慢以及不确定性;
依赖安装速度慢
:npm v3是串行安装,按照顺序逐个安装;但是yarn采用并行安装,并且会将包缓存在磁盘上,下次可离线从磁盘上安装;
不确定性
:上面讲到根据 package.json生成的node_modules里的结构并不唯一,yarn新增yarn.lock文件;会将package.json中的依赖进行分析,记录依赖和子依赖的关系、版本号以及获取地址和验证模块完整性的hash;通过这种手段可以达到确定性;在此之后npm v5也发布了带有package-lock.json
的版本(cnpm
无法锁定),也是为了锁定版本,从此无需手动执行npm shrinkwrap
,当npm i
时package-lock.json
文件会直接生成。(多说一句如果两个文件并存在项目根节点,则会优先根据npm-shrinkwrap.json生成)
这里多说一句npm v5后npm i
有了几次变化:
- npm 5.0.x版本,不管package.json怎么变,
npm i
都会根据lock文件下载。 - npm 5.1.0版本后,
npm i
会无视lock文件,直接下载新的npm包; - npm 5.4.2版本后,如果package.json和lock文件不同那么,npm i时会根据package的版本进行下载并更新lock;如果两个文件相同则会根据lock文件下载,不管package有无更新;
但是和npm一样,幽灵依赖
和依赖分身
的问题还是没有得到解决;
4. pnpm
与npm和yarn的依赖提升和扁平化不同,pnpm采取了一套新的策略:内容寻址储存;该策略安装的依赖的每个版本只会在全局中存在唯一一个;
当引用node_module中的依赖时,会通过记录在.pnpm中的信息来使用硬链接与符号链接在全局store中找到这个文件,这里的.pnpm中的数据不是扁平化的。
硬链接 Hard link
:硬链接可以理解为源文件的副本,项目里安装的其实是副本,它使得用户可以通过路径引用查找到全局 store 中的源文件,而且这个副本根本不占任何空间。同时,pnpm 会在全局 store 里存储硬链接,不同的项目可以从全局 store 寻找到同一个依赖,大大地节省了磁盘空间
符号链接 Symbolic link
:也叫软连接,可以理解为快捷方式,pnpm 可以通过它找到对应磁盘目录下的依赖地址。
还是使用上面的例子: 项目依赖了A、B、C,之后A依赖D@1.0,B依赖D@2.0,而C也依赖D@1.0,使用 pnpm 安装依赖后 node_modules 结构如下
// 项目的根node_modules
node_modules
.pnpm
A@1.0.0
node_modules
A => <store>/A@1.0.0
D => ../../D@1.0.0
D@1.0.0
node_modules
D => <store>/D@1.0.0
B@1.0.0
node_modules
B => <store>/B@1.0.0
D => ../../D@2.0.0
C@1.0.0
node_modules
C => <store>/C@1.0.0
D => ../../D@1.0.0
A => .pnpm/A@1.0.0/node_modules/A
B => .pnpm/B@1.0.0/node_modules/B
C => .pnpm/C@1.0.0/node_modules/C
<store>/xxx
开头的路径是硬链接,指向全局 store 中安装的依赖;其余的是软链接,指向依赖的快捷方式。
pnpm的速度要比yarn快很多,对比来看yarn是从缓存中复制文件,而 pnpm 只是从全局存储中链接它们。
pnpm兼容了node的依赖解析并且解决了yarn和npm无法解决的问题:
幽灵依赖问题
:子依赖不会被提升,不会产生幽灵依赖。
依赖分身问题
:相同的依赖只会在全局 store 中安装一次, 不存在多份相统一来的情况。
但也存在一些弊端:
- 在比如 Electron等不支持软链接的环境中,无法使用 pnpm。
- 因为依赖源文件是安装在全局 store 中的,调试依赖或 patch-package 给依赖打补丁也不太方便,可能会影响其他项目。
-----------------------以上结束啦-------------------------
-----------------------下面是npm安装兼容/不兼容多版本包实例-------------------------
1.npm安装可兼容多版本依赖的安装实例
以某一项目中的magic-string
包为例,查看package-lock.json中依赖关系
// 位置一
// 这里`@rollup/plugin-commonjs`引用的是`^0.25.7`版本的`magic-string`
"@rollup/plugin-commonjs": {
"version": "17.0.0",
"resolved": "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-17.0.0.tgz",
"integrity": "sha512-/omBIJG1nHQc+bgkYDuLpb/V08QyutP9amOrJRUSlYJZP+b/68gM//D8sxJe3Yry2QnYIr3QjR3x4AlxJEN3GA==",
"dev": true,
"requires": {
...
"magic-string": "^0.25.7",
...
},
...
},
...
// 位置二
// 这里`@rollup/plugin-commonjs`引用的也是`^0.25.7`版本的`magic-string`
"@rollup/plugin-replace": {
"version": "2.3.4",
"resolved": "https://registry.npmjs.org/@rollup/plugin-replace/-/plugin-replace-2.3.4.tgz",
"integrity": "sha512-waBhMzyAtjCL1GwZes2jaE9MjuQ/DQF2BatH3fRivUF3z0JBFrU0U6iBNC/4WR+2rLKhaAhPWDNPYp4mI6RqdQ==",
"dev": true,
"requires": {
...
"magic-string": "^0.25.7"
...
}
},
// 位置三
// 这里`@rollup/plugin-replace`引用是`^0.25.0`版本的`magic-string`, 出现了不同的版本
"@surma/rollup-plugin-off-main-thread": {
"version": "2.2.2",
"resolved": "https://registry.npmjs.org/@surma/rollup-plugin-off-main-thread/-/rollup-plugin-off-main-thread-2.2.2.tgz",
"integrity": "sha512-dOD6nGZ79RmWKDRQuC7SOGXMvDkkLwBogu+epfVFMKiy2kOUtLZkb8wV/ettuMt37YJAJKYCKUmxSbZL2LkUQg==",
"dev": true,
"requires": {
"ejs": "^3.1.6",
"json5": "^2.2.0",
"magic-string": "^0.25.0"
},
...
},
...
// 位置四
// `magic-string`包是`0.25.7`版本;
"magic-string": {
"version": "0.25.7",
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.7.tgz",
"integrity": "sha512-4CrMT5DOHTDk4HYDlzmwu4FVCcIYI8gauveasrdCu2IKIFOJ3f0v/8MDGJCDL9oD2ppz/Av1b0Nj345H9M+XIA==",
"dev": true,
"requires": {
"sourcemap-codec": "^1.4.4"
}
},
...
我们工程里 package-lock.json 中涉及magic-string
的只有以上4个位置,并且通过查看requires
字段可以发现,我们工程并没有直接安装magic-string
,可以理解为位置四
中的配置项是npm分析过依赖关系后,为了公用而提取到外层的;
那么位置三
的^0.25.0
版本被安装到哪里了么,npm是怎么处理的呢,我们看看node_modules/@surma/rollup-plugin-off-main-thread/package.json
"dependencies": {
"ejs": "^3.1.6",
"json5": "^2.2.0",
"magic-string": "^0.25.0"
},
ok没错使用了magic-string
,但是当我查看node_module/@surma/rollup-plugin-off-main-thread/node_modules
时发现:
![](https://img.haomeiwen.com/i4727382/cb4028675a70630d.png)
里面并没有magic-string
,回头看下,magic-string
一共有有两个版本^0.25.0
和"^0.25.7"
;
这里的^
表示主本兼容,即0.X.X
的版本都可以;
例:^0.25.0
即 >=0.25.0且<1.X.X
;
一切都很明了了,由于外部已经安装了"^0.25.7"
,"^0.25.0"
就无需重复安装了;
2. npm安装无法兼容多版本依赖的安装实例
再拿另一个包来举例:json5
,打开 package-lock.json
// 位置一
// 这里引用是`^2.2.0`版本的`json5`
"@surma/rollup-plugin-off-main-thread": {
"version": "2.2.2",
"resolved": "https://registry.npmjs.org/@surma/rollup-plugin-off-main-thread/-/rollup-plugin-off-main-thread-2.2.2.tgz",
"integrity": "sha512-dOD6nGZ79RmWKDRQuC7SOGXMvDkkLwBogu+epfVFMKiy2kOUtLZkb8wV/ettuMt37YJAJKYCKUmxSbZL2LkUQg==",
"dev": true,
"requires": {
"ejs": "^3.1.6",
"json5": "^2.2.0",
"magic-string": "^0.25.0"
},
"dependencies": {
"json5": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.0.tgz",
"integrity": "sha512-f+8cldu7X/y7RAJurMEJmdoKXGB/X550w2Nr3tTbezL6RwEE/iMcm+tZnXeoZtKuOq6ft8+CqzEkrIgx1fPoQA==",
"dev": true,
"requires": {
"minimist": "^1.2.5"
}
}
}
},
...
// 位置二
// 这里引用是`"^1.0.1`版本的`json5`
"loader-utils": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.4.0.tgz",
"integrity": "sha512-qH0WSMBtn/oHuwjy/NucEgbx5dbxxnxup9s4PVXJUDHZBQY+s0NWA9rJf53RBnQZxfch7euUui7hpoAPvALZdA==",
"dev": true,
"requires": {
"big.js": "^5.2.2",
"emojis-list": "^3.0.0",
"json5": "^1.0.1"
}
},
...
// 位置三
// 这里是"1.0.1“版本的`json5`
"json5": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz",
"integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==",
"dev": true,
"requires": {
"minimist": "^1.2.0"
}
},
同样我们项目的package.json中也并没有直接引用,可以看到工程中安装的是1.0.1
,"@surma/rollup-plugin-off-main-thread"
中需要的是2.2.0
版本无法兼容。查看"@surma/rollup-plugin-off-main-thread"
中的的node_modules
。
![](https://img.haomeiwen.com/i4727382/67a8af8682f00710.png)
所有package的依赖安装时都尽量拍平之后安装到项目根目录的node_modules里,并且避免各个package重复安装第三方依赖,将有冲突的依赖,安装在自己package的node_modules里,解决依赖的版本冲突问题。
至于为什么提升的是1.0.1
而不是2.2.0
版本呢?这个开始以为是和依赖树层级或者包版本大小有关,但不停删除lock和node_module进行重装,实测之后发现是随机的。由于node_module是根据package.lock来安装的,所以所以所以所以!!为了保证唯一性最好将lock也一起进行git托管!!!
网友评论