添加链接
link之家
链接快照平台
  • 输入网页链接,自动生成快照
  • 标签化管理网页链接
+关注继续查看

引言

如果你持续使用 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.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 的依赖算法时,希望能把这个问题解释清楚。

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

参考资料

系列文章

apt install git:Unable to fetch some archives, maybe run apt-get update or try with --fix-missing?
Failed to fetch http://security.ubuntu.com/ubuntu/pool/main/g/git/git-man_2.17.1-1ubuntu0.11_all.deb 404 Not Found [IP: 91.189.91.39 80] 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版本不兼容。
npm install ,npm ERR code 401 Incorrect or missing password 错误原因与.npmrc 配置文件的使用
npm install ,npm ERR code 401 Incorrect or missing password 错误原因与.npmrc 配置文件的使用
如何解决使用npm install 时报错:npm ERR! { Error: EPERM: operation not permitted, mkdir..
如何解决使用npm install 时报错:npm ERR! { Error: EPERM: operation not permitted, mkdir..
npm WARN deprecated socks@1.1.10: If using 2.x branch, please upgrade to at least 2.1.6
npm WARN deprecated socks@1.1.10: If using 2.x branch, please upgrade to at least 2.1.6
npm 安装出错 npm ERR! request to https://registry.npmjs.org/express failed, reason: unable to verify th
npm 安装出错 npm ERR! request to https://registry.npmjs.org/express failed, reason: unable to verify th