引言
如果你持续使用 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 的依赖算法时,希望能把这个问题解释清楚。
如果看完之后还是不懂,或者本文的解释有错误的地方,欢迎评论拍砖!
参考资料
系列文章