深入理解并解决 npm ERESOLVE (Peer Conflict) 问题

本文涉及的产品
云解析 DNS,旗舰版 1个月
全局流量管理 GTM,标准版 1个月
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
简介: 如果你持续使用 LTS 版本的 Node.js,或者主动更新了 npm 到 7+,一定见过下面这个难懂的报错: “unable to resolve dependency tree",字面意思就是无法解析依赖树,然后下面一大长串东西都在尝试告诉开发者无法解析的原因,并建议修复依赖间的冲突,但很尴尬的一点是……可能看了之后还是不知道,我要修复什么冲突呢?

引言

如果你持续使用 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 无常的历史,又回到了我们一开始提到的问题:怎么解决这个冲突?

指导思想:让冲突的版本落在不冲突的范围中,要么改版本,要么改范围。

准备工作

  1. 更新 npm 到最新版本,npm 8 低版本有很多 bug。 npm i -g npm@latest
  2. 如果是刚刚切换到 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.0react-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@18react-router@6react-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 的依赖算法时,希望能把这个问题解释清楚。

如果看完之后还是不懂,或者本文的解释有错误的地方,欢迎评论拍砖!

参考资料

目录
相关文章
|
3月前
|
缓存
成功解决:Could not resolve dependency: npm ERR! peer vue@“^3.0.2“ from vuex@4.0.2
这篇文章讨论了在使用npm安装依赖时遇到的一个常见问题,即无法解析依赖导致的"peer dependency"冲突错误。文章提供了几种解决方法,包括清除npm缓存、删除`node_modules`文件夹和`package-lock.json`文件,然后重新尝试安装,以解决版本冲突问题。
|
资源调度 前端开发 JavaScript
什么是 NPM 里的 Peer Dependency
什么是 NPM 里的 Peer Dependency
npm install报错peerDependencies WARNING eslint-plugin-vue@^5.2.3 requires a peer of eslint@^5.0.0 but
npm install 报错,以为是npm问题,改成cnpm install,也还是报错,根据错误信息提示,推断是eslint版本不兼容。
730 0
|
6月前
|
JavaScript
npm install没问题,但npm run dev的时候报Node Sass version 6.0.1 is incompatible with ^4.0.0 ^5.0.0
npm install没问题,但npm run dev的时候报Node Sass version 6.0.1 is incompatible with ^4.0.0 ^5.0.0
54 0
|
5月前
|
前端开发
windows10 安装node npm 等前端环境 并配置国内源
windows10 安装node npm 等前端环境 并配置国内源
313 3
|
14天前
|
缓存 资源调度 JavaScript
npx与npm的差异解析,以及包管理器yarn与Node版本管理工具nvm的使用方法详解
npx与npm的差异解析,以及包管理器yarn与Node版本管理工具nvm的使用方法详解
20 0
2071 verbose node v16.6.0 2072 verbose npm v7.19.1或者 no such file or directory, lstat ‘D:\wor
该博客文章提供了解决在使用npm版本7.19.1时出现的"no such file or directory"错误的具体方法,建议通过降级npm到6.14.8版本来解决问题,并确认了该方法可以成功安装node_modules。
2071 verbose node v16.6.0 2072 verbose npm v7.19.1或者 no such file or directory, lstat ‘D:\wor
|
3月前
|
缓存 JavaScript 前端开发
成功解决:npm 版本不支持node.js。【 npm v9.1.2 does not support Node.js v16.6.0.】
这篇文章介绍了如何解决npm版本与Node.js版本不兼容的问题,提供了查看当前npm和Node.js版本的步骤,以及如何根据Node.js版本选择合适的npm版本并进行升级的详细指导。
成功解决:npm 版本不支持node.js。【 npm v9.1.2 does not support Node.js v16.6.0.】
|
3月前
|
JavaScript
【Deepin 20系统】Jupyter notebook解决ValueError: Please install Node.js and npm before continuing installa
文章讨论了在Deepin 20系统上安装Jupyter Notebook的debug插件时出现的"ValueError: Please install Node.js and npm before continuing installation"错误,并提供了使用conda安装Node.js的解决方法。
114 1
|
3月前
Mac卸载 Node npm,升级 Node
Mac卸载 Node npm,升级 Node
60 0

推荐镜像

更多