引言
如果你持续使用 LTS 版本的 Node.js,或者主动更新了 npm 到 7+,一定见过下面这个难懂的报错:
一眼看过去,除了最显眼的满屏 ERR! ,就是顶部的 “unable to resolve dependency tree",字面意思就是无法解析依赖树,然后下面一大长串东西都在尝试告诉开发者无法解析的原因,并建议修复依赖间的冲突,但很尴尬的一点是……可能看了之后还是不知道,我要修复什么冲突呢?
万恶之源:Peer Depencency
先来看看这个错误产生的原因。
自 npm 创始起就引入的 “peerDependencies” 字段,并不被多数前端应用开发者所熟悉,但被广泛应用于类库的开发中。它的主要作用就是:让一个插件包标记其间接依赖的主包的版本范围。
用 react-router@6.4.0 来举例,可以看到在它的 package.json 中,声明了对 react@>=16.8 的 peer 依赖,但在生产依赖中,却没有 react。
此处可以翻译为:“我是 react 的一个插件包,我需要运行在 react 16.8 以上版本中,但我不强制规定应用在引用我时,到底给我提供的是 react 16 还是 17 还是 18,反正只要大于 16.8 我就能正常工作啦。”
这样的声明有什么实际作用呢?我们知道,在 React 16.8 版本中引入了全新的 API 体系:Hooks,如果 react-router 使用了 Hooks,就意味着它必须运行在 React 16.8 环境中,否则无法正确工作。同样地,我们在编写 React 组件的过程中如果使用了 Hooks,也同样需要在这个组件的 peerDependencies 中声明 react@>=16.8。
而如果我在开发应用时,使用了错误的 react 的版本,此时 npm 就会提示我:“ERESOLVE!你依赖的两个包版本不匹配,无法正常工作!”,比如:
{
"name": "conflict-react",
"dependencies": {
"react": "16.6",
"react-router": "^6.4.0"
}
}
这样的一个 package.json,就发生了上文中提到了版本不匹配问题,react-router 必须使用 react@16.8 以上的版本,但应用又声明了 react@16.6,在这种情况下,react-router 无法工作,而 npm 则在安装依赖时就会提示我们开头见过的错误:
解构:如何阅读 npm 的报错
叮!您的智能翻译助手已上线,我们一行一行来看上面这个报错:
While resolving: conflict-react@1.0.0 Found: react@16.6.3 node_modules/react react@"16.6" from the root project Could not resolve dependency: peer react@">=16.8" from react-router@6.4.0 node_modules/react-router react-router@"^6.4.0" from the root project |
在解析 conflict-react 这个项目的依赖时 项目中 package.json name 字段定义的就是 conflict-react,就是项目名 发现已经安装了 react@16.6.3 这个版本, 这个版本安装在 node_modules/react 这个文件夹下。 react@16.6.3 这个版本是因为在项目依赖中声明了 react@16.6 而被引入的。 此处的“已经安装”,指的是在解析时先解析了,可以理解为是计算依赖树后,应该装这个版本在这,但实际的安装过程还没有发生。因为上面 react 的版本已存在,下面这个依赖无法被安装: 在 react-router@6.4.0 这个版本中,声明了一个 peer 依赖 react@>=16.8,而上面的 react@16.6.3 不满足这个依赖要求。 react-router@6.4.0 应该被安装在 node_modules/react-router 下 react-router@6.4.0 这个版本是因为在项目依赖中声明了 react-router@^6.4.0 而被引入的。 |
连起来就是:项目声明了 react@16.6,应该安装 react@16.6.3。但是因为项目声明的 react-router@6.4.0 又声明自己依赖 react 的版本要大于 16.8,和项目声明的版本冲突了,所以无法安装。
要额外注意的是,在左侧报错中的每次递进,都是在解释上一层的依赖为什么会安装这个版本,以及声明对这个版本依赖的原始声明 Semver(类似于 npm why)。而递进的终点都是 “root project”,即项目的 package.json。
所以真正读起来,会发现这个报错的递进,在逻辑上是要 “反着读” 的。
复杂案例:多层声明与锁冲突
多层声明指向同一个依赖
我们再来看一个真实项目中的复杂报错:
# package.json
{
"name": "conflict-react",
"version": "1.0.0",
"dependencies": {
"@alife/next-dom": "^0.2.18",
"@alife/whale-sortable": "^0.1.12"
}
}
大体的结构没有改变,上面是已经被解析出的依赖版本,下面是出冲突的版本声明。但出现了并列的递进情况,这里,npm 在尝试解释,这里的 react 是哪来的,我们重点看同层递进的条目:
@alifd/next 这个依赖与 @alifd/meet-react 这个依赖都声明了 peerDependencies react@>=16.0.0,最终安装的 react 版本是 18.2.0。 |
竖着读完,我们再来横着读。报错中的每一层递进,都是进一步的解释了这个依赖又是哪来的,直到解释到最终的项目一级依赖上,注意,这个时候就要反着读了,从最深的一层开始向外翻译:
项目 package.json 的 dependencies 声明了 @alife/whale-sortable@^0.1.12,对应安装了 0.1.12 这个版本; @alife/whale-sortable@0.1.12 这个版本,又声明了 @alife/next@^1.x 这个 peerDependencies,对应安装到 @alife/next@1.26.1; @alife/next@1.26.1 这个版本,又声明了 peerDependencies react@>=16.0.0;最终安装到的符合 Semver 范围的版本是 react@18.2.0。 |
而最后的冲突依赖,因为只有一层,就比较好解释了:
因为上面安装了 react@18.2.0; 而根目录声明的 @alife/whale-sortable@0.1.12 中,又强制要求 react 在^16.0.0 的范围内(16.0.0 < 版本 < 17.0.0),不满足要求,所以出现冲突。 |
最后会发现,只要引入这个依赖包 @alife/whale-sortable,就必定会出冲突,我们稍后会告诉大家该怎么解决这个问题。
锁中现存版本混淆信息
再来看另一个例子:
在这个例子中,因为锁文件的存在,peer react@^15 || ^16 被锁定到了 react@15.7.0,所以初看起来会觉得有点奇怪,怎么一个 ^16.14.0 的声明会安装到 15.7.0 上去?但在往后看时就会发现,这个 16.14.0 也是被解释了的冲突点,只不过包含在了另一个依赖中了。
究其原因,npm 报这个错的方式一般都是遇到第一个就会抛错,所以可能会出现解决了一个 peer 冲突,又出另一个 peer 冲突问题。在解决之初,也要考虑锁文件带来的影响。
这类反复出现的问题,通常都是刚刚升级项目的 npm 时出现的,所以笔者建议如果没影响,先删了原来的锁文件,然后重新生成锁,再来解决新的冲突问题,能避免一些重复工作。如果不行,那就只能乖乖一个一个解决了。
来 去 之 间:npm 不同版本上 peer 行为的无常
来去之间是个微博上的梗(meme),本来是微博老板的昵称,后被网友们赋予了多重含义。此处请理解为反复无常。
为什么升级 npm 会造成这个问题?其实不能怪 peerDependencies 本身,要怪也应该怪 npm 行为上的反复无常。
在 npm 版本 1-2 上,peerDependencies 会像 dependencies 一样,被自动安装。而在 npm 版本 3-6 中,peerDepedencies 就不安装了,如果出现冲突或者缺失,只会有一个警告:
到了 npm 7,因为 npm 重写了树解析算法,又把 peerDependencies 的自动安装行为给加回来了。结果社区在这 6 年中积累的大量没人管的警告,就变成了 ERR! 暴露了出来。
在 npm 8 中,npm 为了区别可选的 peer 与必须的 peer,又为包开发者新增了 peerDependenciesMeta 字段来标记可选 peer,而这个字段并不被 npm 7 以前的版本所支持。
军刀:解决冲突
讲了这么多,终于读懂了这个晦涩的报错,也听了 npm 无常的历史,又回到了我们一开始提到的问题:怎么解决这个冲突?
指导思想:让冲突的版本落在不冲突的范围中,要么改版本,要么改范围。
准备工作
-
更新 npm 到最新版本,npm 8 低版本有很多 bug。 npm i -g npm@latest
-
如果是刚刚切换到 npm 7+ 版本的仓库,建议 删除原有锁文件与 node_modules,以避免问题太多搞不过来,以及与锁中的版本出现冲突。 rm -rf node_modules package-lock.json
Case 1:一级依赖冲突
{
"dependencies": {
"react": "^18",
"react-ace": "^5.10.0"
}
}
其中,react-ace@5 中声明了 react 的 peerDependencies ^0.13.0 || ^0.14.0 || ^15.0.1 || ^16.0.0,与项目一级依赖冲突。
解法 1:升级一级依赖
如果你使用的依赖有较新的版本,升级了 peerDeps 中的声明,可以尝试升级该依赖。如本例中的 react-ace@5 就可以升级到 react-ace@10 来解决这个问题。
解法 2:降级一级依赖
这里,出冲突的依赖是我们能直接修改一级依赖解决的,我们直接把 react 版本降至 16,与子依赖的要求保持一致(^16.0.0),就不会出现这个问题了。
{
"dependencies": {
"react": "^16",
"react-ace": "^5.10.0"
}
}
解法 3:无法降级一级依赖,全局覆写二级 peer 依赖
如果项目本身强依赖 react 18,无法降级;也无法升级其它包,那就需要把子依赖中的 react 声明强制改为与项目一致。
可以使用 npm 8.7+ 的 overrides 能力(这功能在低版本中有 Bug!),对子依赖的版本进行重写。
{
"dependencies": {
"react": "^18",
"react-ace": "^5.10.0"
},
"overrides": {
"react": "$react"
}
}
此处的 $react代表引用了项目 dependencies 中的 react 版本。
你也可以选择在 overrides 时使用具体的 Semver 或者版本,但如果不小心没有和 dependencies 里的版本写成一样的,就会报错:
# ❌ 错误案例
{
"dependencies": {
"react": "^18",
"react-ace": "^5.10.0"
},
"overrides": {
"react": "^17" # 注意这个和 dependencies 中的没有完全重合!
}
}
Overrides 科普文占楼,正在写 ing……先看官方文档吧~
Case 2:依赖间 Peer 冲突
{
"dependencies": {
"react-router": "^2",
"react-router-dom": "^6"
}
}
此处是 react-router@2 声明了 peer react@^15.0.0,react-router-dom@6 声明了 peer react@>=16,没有交集,产生冲突。
解法 1:升级/降级一级依赖
同 Case 1 的解法 1/2,把 react-router 升个级,或者把 react-router-dom 降个级就可以了。
解法 2:全局覆写 peer 依赖版本
同 Case 1 的解法 3,用 overrides 固定 react 版本即可。
Case 3:子依赖的 peer 间产生冲突
我们先造一个 peer 间一定会冲突的 npm 包:
{
"name": "@ali/dongdong-test-react-sub-conflict",
"version": "1.0.0",
"dependencies": {
"react-router": "^2",
"react-router-dom": "^6"
}
}
然后在另一个应用中,引用这个有问题的依赖,再加上一个 react 声明:
{
"dependencies": {
"react": "^18",
"@ali/dongdong-test-react-sub-conflict": "^1.0.0"
}
}
此时,npm 不会报 ERR,反而是报出了 WARN overriding peer dependency。
读者可以先基于上面的解析,先尝试自己翻译一下,再看答案哦~提示:与 npm 的提升(hoist)逻辑有关。
答案
@ali/dongdong 的依赖 react-router@2.8.1 声明了 peer react@15.7.0,并安装在 @ali/dongdong/react 下。
npm 本来打算将这个 react-router 2 -> react 15 的这个依赖链直接提升到(hoist)node_modules 下,但 node_modules 下已经有 react 18 了,所以无法提升,只能安装到 node_modules/@ali/dongdong/node_modules 下面。
而 @ali/dongdong 的依赖 react-router-dom@6.4.0 声明了 react-dom@>=16.8 的 peer 依赖,react-dom@18.2.0 继而声明了 react@18.2.0 的 peer 依赖,在相同的目录下,已经存在了 react@15.7.0,不满足要求,上面的提升也失败了,继而产生了冲突。
扩展一下:要是不加 react 的话,npm 并不会报错,而是会把 react@15 和 react-router@2 装在 node_modules 下,把 react@18,react-router@6,react-router-dom@6 装在 @ali/dongdong-test-react-sub-conflict 这个包的 node_modules 下,形成这样的目录结构:
app/
├─ node_modules/
│ ├─ @ali/
│ │ ├─ dongdong-test-react-sub-conflict/
│ │ │ ├─ node_modules/
│ │ │ │ ├─ react@18.2.0/
│ │ │ │ ├─ react-dom@18.2.0/
│ │ │ │ ├─ react-router-dom@6.4.0/
│ ├─ react@15.7.0/
│ ├─ react-router@2.8.1/
这会导致根目录引用的版本是 react@15,但实际在运行 react-router-dom 时跑的就是 react@18 了,页面可能会无法渲染,或者出现多 react 实例。读者可以自己复制这个例子试一下,看看 node_modules 下的效果。
解法 1: 覆写子依赖版本
在这种情况下,可以考虑用 overrides 升级/降级子依赖的版本,继而让这个出问题的子依赖的 peer 声明更新,以匹配依赖要求。
{
"dependencies": {
"react": "^18",
"@ali/dongdong-test-react-sub-conflict": "^1.0.0"
},
"overrides": {
"@ali/dongdong-test-react-sub-conflict": {
"react-router": "^6"
}
}
解法 2:全局覆写出冲突的依赖版本
直接指定 react 的版本唯一也是一个解决的办法,但不一定能保证低版本的 react-router 工作在高版本的 react 下哦~具体能否工作还要看具体的项目了。
这种改法也能避免多 react 版本实例的问题。
{
"dependencies": {
"react": "^18.0.0",
"@ali/dongdong-test-react-sub-conflict": "^1.0.0"
},
"overrides": {
"react": "^18.0.0"
}
}
此处因 npm bug 无法使用 $react 来引用了。
Bug Case:冲突的 Semver 间有同版本范围
我们会发现,这几个冲突的版本范围,实际上是有交集区间的:>=16.0.0 ∩ ^16.0.0 => ^16.0.0。
遇到这个问题,请 npm install -g npm@latest升级 npm 版本,这个是 npm 早期的 bug 造成的没有自动解析出来。
实在没办法了的办法:忽略大法
让 npm 7+ 模拟 npm 6 的行为,只需要在项目根目录的 .npmrc 中加入如下配置:
legacy-peer-deps=true
就能让 npm 不再尝试自动安装 peerDependencies。在着急的时候,或者项目用的依赖实在太老旧无法修复的时候,先把这个搞上吧。
笔者建议在项目中配置,而不是在全局 npm 用户配置中修改该选项,这样能保证项目多人开发时行为一致。
实在没办法了的办法 2:降级 npm 到 6
你可以假装没看到这条解决方案,虽然它真的是个方案,但真的不建议这样做。
Under the hood
[本次不写,这个有点麻烦,要单独成篇,欢迎钉钉投喂催稿]
后记
其实 npm 还给开发者埋了一个坑,如果你多多试验会发现,不同的依赖声明顺序,会产生不同的报错,因为 npm 是按遇到依赖的顺序来对依赖进行计算的,所以后面的依赖才会和前面先遇到并已经放好的依赖产生冲突,后续介绍 npm 8 的依赖算法时,希望能把这个问题解释清楚。
如果看完之后还是不懂,或者本文的解释有错误的地方,欢迎评论拍砖!