前端依赖版本重写指南

简介: 感谢神奇的 Semver 动态规则,npm 社区经常会发生依赖包更新后引入破坏变更的情况(应用没有使用依赖锁的话),而应用开发者就要在自己的依赖声明里先临时绕过,避免安装到有问题的版本,如果是一级依赖,只需要改 package.json 的声明就可以了,但如果是子依赖,就需要进行版本重写(overrides/resolution)了。本文是一篇针对版本重写功能的指南性文章,当你遇到如下的问题时,就可以按照对应的依赖重写语法,解决这些依赖问题了。

引言

感谢神奇的 Semver 动态规则,npm 社区经常会发生依赖包更新后引入破坏变更的情况(应用没有使用依赖锁的话),而应用开发者就要在自己的依赖声明里先临时绕过,避免安装到有问题的版本,如果是一级依赖,只需要改 package.json 的声明就可以了,但如果是子依赖,就需要进行版本重写(overrides/resolution)了。

本文是一篇针对版本重写功能的指南性文章,当你遇到如下的问题时,就可以按照对应的依赖重写语法,解决这些依赖问题了:

  1. 临时版本修复:需要临时把有问题的子依赖(依赖的依赖)版本降级或升级到正常的版本上。如降级有 Breaking Change 的 @babel/generator。
  2. 强制升级子依赖:使用的一个包更新频率较慢,想更新其子依赖时。如一个老的构建依赖依赖的 css-loader 需要更新到新版本。
  3. 依赖多版本统一:React/Webpack 等包出现多个版本,造成构建或运行时问题,需要统一项目对应依赖的版本到一个版本上。
  4. 子依赖重写:某个子依赖 A 依赖特定版本的 B 时会造成问题,仅需要重写依赖 A 下 B 的版本时。
  5. 删除子依赖:想删除某些子依赖不正确的依赖时,如子依赖把 devDependencies 写到 dependencies 里,或者写错 semver 的,开发者又没有权限发布这个不规范的包,需要删除或者替换子依赖的 Semver 声明。如不规范的依赖包中把应该在 devDependencies 里的包写在了 dependencies 里。

通用术语解释

  • 一级依赖:直接声明在 package.json 的 (dev | optional | peer) dependencies 中的依赖。
  • 子依赖:一级依赖的依赖,无法由应用开发者直接指定版本。
  • 嵌套重写:指仅重写某个依赖下的一级依赖或者子依赖(限制 Scope 重写依赖版本)。

包管理器语法

阿里的前端开发者使用的包管理软件,除了社区常用的 npm, yarn 以及 pnpm 外,还有阿里内部特有的 tnpm 及其不同 mode。他们各自都有不同的依赖安装模式及依赖版本重写的语法,按大版本及语法的不同,把他们分为如下几类:

npm

yarn

tnpm

pnpm

npm@5-6

tnpm@8 (npm mode)

npm@8

tnpm@9 (npm mode)

yarn@1 (classic)

tnpm@* (yarn mode)

yarn@3 (berry)

tnpm@*

pnpm@<6.3.1

pnpm@>6.3.1

依赖重写语法

不支持(需 Hack)

overrides

resolutions

resolutions (yarn 3 语法)

resolutions (yarn 1 语法)

resolutions (pnpm 语法)

pnpm.overrides

锁文件版本

package-lock.json@1

package-lock.json@2

yarn.lock@1

yarn.lock@6

不支持

pnpm-lock.yaml

本部分针对每一种语法都进行了详细的解释与简单的案例,在最后还有实际的例子可以供读者更好的理解使用场景。如果是紧急解决问题,建议先看对应包管理软件的语法,补充参考案例,以更快的解决问题。

Npm 8 Overrides

注:该字段主要在 npm 8 最新版上被支持,npm 7 及以下的版本中并没有这个能力,不过到还是有办法在旧版 npm 上用上这个能力,如果一定要使用 npm 6,请跳转到下一个子标题 Npm 6 Overrides (Hack)。

基本语法

使用前记得更新 npm 到最新版:npm i -g npm@latest

基本的语法有点类似于 CSS 的选择器 - 声明语法,选择器用于匹配到包,声明用于重写版本声明。

# package.json
{
  "overrides": {
    "选择器": "重写版本声明"
  }
}

在如下的例子中,示范了所有选择器和重写版本的语法:

{
  "packageManager": "npm@8.19.2",
  "dependencies": {
    "react": "^18"
  },
  "overrides": {
    # 表示将所有 webpack 的版本全部重写到 5
    "webpack": "5",
    # 将 react 替换成 dependencies 中声明的版本
    "react": "$react",
    # 表示仅将 6.0.0 的 babel 版本重写到 6.0.1
    "babel@6.0.0": "6.0.1", 
    # 表示将满足 ms@^2 的包重写版本声明到 ^1
    "ms@^2": "^1",
    # 把 tnpm 这个包全局替换成一个空包,相当于删除这个依赖
    "tnpm": "npm:noop-package@1.0.0", 
    # 把所有 underscore 替换成 lodash
    "underscore": "npm:lodash", 
    # 把 metameta 替换成一个自己的 fork
    "metameta": "github:some-group/metameta-fork"
  }
}

这个例子中的声明,都是全局替换,无论是子依赖还是子依赖的依赖,只要匹配到选择符,就会应用规则。

嵌套重写

除了全局替换之外,也可以对特定包的子依赖进行重写。在嵌套结构中,用 “.“ 代表嵌套的包本身,有点像 less 里的 &。

# 仅表示 overrides 字段内容,下文不再赘述
{
  "ice.js": {
    ".": "1.2.0", # 重写全部的 ice.js 到 1.2.0
    "webpack": "5" # 重写 ice.js 下级所有的 webpack 依赖版本至 5
  }
}

当出现多个规则应用到同一个包上时,嵌套越深的规则优先级越高:

{
  "foo": {
    ".": "1.0.0",
    "boo": "3.0.0"
  },
  "boo": "1.0.0"
}

最后安装的结果是:

  1. 所有  foo 包的版本被设定成  1.0.0
  2. foo 下的所有  boo 包被设定成  3.0.0
  3. 除了  foo 下的,所有其它的  boo 包被设定成  1.0.0

另外需要注意的是,成文时的 npm 版本有个小问题,在嵌套重写中判断一个规则是否被删除时,判断的是父依赖的规则是否存在,下面的变动将不会把原来的嵌套重写规则从锁中删除:

首次重写:

{
  "overrides": {
    "debug": {
      "ms": "1"
    }
  }
}

仅删除嵌套重写,锁文件依赖保持被重写的状态:

{
  "overrides": {
    "debug": {
    }
  }
}

删除父级依赖的声明,才会把依赖状态修正成无修改的状态:

{
  "overrides": {
  }
}

特殊规则

Overrides 有几条特殊规则,一般在简单情况下遇不到,但还是要学习下,以避免发生预期外的行为。

首次从 package-lock v1 升级到 package-lock v2 时,overrides 可能不生效

如开头的表格中提到的,package-lock v1 是 npm@6 生成的锁,package-lock v2 是 npm@8 生成的锁。在升级 Node 或者主动升级 npm 后,可能见到下面这个提示:

overrides 可能并没有生效,重新再跑一次 npm i 就可以了。

另外,当选择器带版本范围时(如 "ms@^2": "1"),从老锁升级的新锁也有可能不应用 overrides 规则(是 npm 的 bug,如果用到了要观察一下锁文件更新的状态)。如果没生效,把版本范围去了,或者删除锁重新生成即可。

一个相关的 Issue:https://github.com/npm/cli/issues/5051

重写不能与 dependencies 声明冲突

overrides 与各类在 package.json 中直接声明的 dependencies 不能出现冲突,如:

{
	"dependencies": {
		"debug": "^2.1.3"
	},
	"overrides": {
		"debug": "^1"
	}
}

会出现 EOVERRIDE 的报错:

一个包只能被一条规则匹配

也可以理解为:当一个包先匹配了一个选择器后,就会停止后续的匹配。也即先遇到的规则优先级最高,如:

{
  "debug": {
    ".": "3"
  },
  "debug@3": { # foo 包的规则走不到这里,因为上面的规则把所有的 foo 包都匹配了。
    "ms": "1" # 同理,因为选择器没有匹配上,该规则也不生效。
  }
}

如果我们调转上面这个例子的顺序,会产生另一个问题:

{
  "debug@3": {
    "ms": "1"
  },
  "debug": {
    ".": "3"
  }
}

比如依赖中有一个 debug@4,他会被重写到 debug@3,此时,要不要继续应用 debug@3 的重写规则呢?

答案是:会继续应用,因为重写之后重新从上至下匹配时,会先匹配到上面的规则,进而应用 ms 的重写规则。最后的结果是,debug@4 变成 debug@3debug@3 依赖的 ms@2 也会被重写到 ms@1

不过笔者非常不建议在 overrides 中这样写,会造成理解上的混乱。

一个包只要匹配了选择器或者重写的目标,就算该规则被匹配

成文时 npm@8.19 尚未完全实现这个能力,目前遇到多重重写的,这些重复的规则都会被忽略。但 overrides 设计文档中标注了这个行为,所以还是解释一下。

我们把上面的的例子再稍加修改,加入双层对包自身版本的重写:

{
  "debug@4.3.4": {
    ".": "4.3.3"
  },
  "debug@4.3.3": {
    ".": "4.3.2"
  }
}

注意:"debug": "3" 的语法只是 "debug": { ".": "3" } 的语法糖。

此时,debug@4.3.4 最终的版本是 4.3.3 还是 4.3.2 呢?

npm 引入了另一条规则:如果一个包匹配了选择器(debug@4.3.4),或者匹配了重写目标(debug@4.3.3),都算是这个规则被应用了。结合上面的遇到第一个应用的规则即停止的逻辑,就可以得到确定性的结果,即:

  1. 先匹配  debug@4.3.4,得到  debug@4.3.3
  2. 进行下一轮匹配, debug@4.3.3 与首条规则的 ".": "4.3.3"  匹配,则  debug@4.3.4 整条规则视为已匹配,结束后续匹配。
  3. 最后的重写结果就是  4.3.3

Npm 6 Overrides (Hack)

npm 6,7 本身并没有类似于 overrides 的能力,但我们可以利用 npm 8 的算法,生成一颗应用 overrides 规则的依赖树后,再生成 npm 6 兼容的锁文件,来间接达到在 npm 6 上使用 overrides 的能力。

利用 @ali/tnpm-lock-adapter@1.6.0 及以上的版本,可以帮我们利用 npm 8 的算法生成 npm 6 的锁。

配置好 overrides 后,在项目目录下执行如下命令,即可生成应用了 overrides 的兼容 npm 6 的 v2 版本 pacakge-lock.json,此时再利用 npm 6 进行安装,就可以安装到重写后的版本了。

tnpm i -g @ali/tnpm-lock-adapter
tnpm-lock-adapter -i -v 1

项目如果要持续使用 npm 6,可以在 package.json 的 scripts hook 中配置上面 tnpm-lock-adapter 这个命令,以持续应用 overrides 规则,避免失效。

Yarn 1 (Classic)

Yarn 1.x (Classic) 已不再持续维护,Yarn 现在活跃的维护版本是 3.x (Berry)。新老版本的重写语法不一样。

基本语法

# package.json
{
  "resolutions": {
    "选择器": "重写版本声明"
  }
}

与 npm 的选择器不同,yarn 仅支持匹配包名,不支持在选择器中使用版本范围来选择特定包的特定版本。语法与案例如下:

{
  "packageManager": "yarn@1.22.19"
  "dependencies": {
		"@angular/cli": "1.0.3",
		"typescript": "2.3.2"
	},
  "resolutions": {
    # 重写所有 typescript 版本到 2.3.2
    "**/typescript": "2.3.2",
    # 上面语法的语法糖
    "typescript": "2.3.2",
    # 重写除一级依赖之外,所有的 typescript 版本到 ^3
    "typescript": "^3",
    # 重写版本声明同样支持如 npm:noop-package@1.0.0,github 仓库等语法。
      
    # === 嵌套重写 ===
    # 仅重写 @angular/cli 直接依赖的 typescript 版本
    "@angular/cli/typescript": "2.3.2",
    # 重写 @angular/cli 下所有依赖的 typescript
    "@angular/cli/**/typescript": "2.3.2",
    #
  }
}

特殊规则

dependencies 声明的优先级高于 resolutions

如果 dependencies 中的一个依赖 a 出现在 resolutions 中,则这个依赖 a 的版本依然是按 dependencies 中的声明安装,但其它包的子依赖中的 a 则应用 resolutions 规则,如:

{
	"dependencies": {
		"@angular/cli": "1.0.3",
		"typescript": "2.3.2"
	},
  "resolutions": {
		"typescript": "^3"
	}
}

会得到如下的依赖树:

node_modules/
├─ typescript@2.3.2/
├─ @angular/cli@1.0.3/
│  ├─ node_modules/
│  │  ├─ typescript@3.9.10/

Tnpm

npminstall mode(默认安装模式)

npminstall 是 tnpm 8,tnpm 9 的默认安装模式。什么都不配的情况下 tnpm 跑的就是这种安装方式。与 yarn 1 的语法一致,不再赘述。

npm mode

即在 package.json 中配置了:

{
  "tnpm": {
    "mode": "npm",
    "lockfile": "enable" # 可选
  }
}

在 tnpm 8 中,npm mode 使用的 npm 版本为 6,不支持 overrides,如果仍需要使用,请参考本文 npm 6 overrides Hack 部分,并开启 lockfile。

在 tnpm 9 中,npm mode 使用的 npm 版本为 8,可以直接像用 npm 8 那样配置 overrides。

yarn mode

即在 package.json 中配置了:

{
  "tnpm": {
    "mode": "yarn",
    "lockfile": "enable" # 可选
  }
}

tnpm 自带的版本是 yarn 1,如果仓库没有切换过 yarn 3,则使用的就是 yarn 1 的 resolutions 语法。

不过 yarn 因为自带了版本切换机制,如果你的仓库额外配置了 yarn 3,还是要按 yarn 3 的语法进行配置。

rapid mode

在 tnpm 9 中,使用 rapid mode 安装时:

tnpm i --fs=rapid

无论模拟的文件目录结构是 npm(--by=npm) 还是 npminstall,重写的语法与 npm 8 的 overrides 一致,直接使用即可。

Yarn 3 (Berry)

基本语法

与 yarn 1 的语法相似,不过在选择器中支持了版本,且不再支持重写嵌套依赖。

另外,可以用 yarn set resolution -s  来修改 resolutions 字段。

{
  "packageManager": "yarn@3.2.3",
  "resolutions": {
    # 重写所有 relay-compiler 到 3.0.0
    "relay-compiler": "3.0.0",
    # 仅重写 @babel/core 下的一级 @babel/generator
    "@babel/core/@babel/generator": "7.3.4",
    # 仅重写 @babel/core@7.0.0 下的一级 @babel/generator
    "@babel/core@npm:7.0.0/@babel/generator": "7.3.4",
    # @babel/core/**/@babel/generator 注意,** 语法不再支持!
  }
}

特殊规则

resolutions 的优先级比 dependencies 高

当这两个字段中都有同一个包时,dependencies 中的版本声明会被替换掉。

Pnpm

基本语法

pnpm 的语法与 yarn 1 类似,但又不完全一样;另外 "pnpm.overrides" 和 "resolutions" 字段的功能相同,后者是前者的别名,直接看例子:

{
  "packageManager": "pnpm@7.12.2",
  "pnpm": {
    "overrides": {
      # 重写所有 foo
      "foo": "^1.0.0", 
      # 全局替换包
      "quux": "npm:@myorg/quux@^1.0.0",
      # 带版本范围的选择器
      "bar@^2.1.0": "3.0.0",
      # 仅重写 qar@1 的直接依赖 zoo,qar 的依赖的子依赖中的 zoo 不会受到影响
      "qar@1>zoo": "2"
    }
  },
  "resolutions": {} # 同样支持,不过是用于兼容 yarn 迁移的别名
}

特殊规则

overrides 的优先级高于 dependencies

如果 dependencies 和 overrides 中都声明了一个包的版本,则 overrides 会覆盖 dependencies 中的声明。如:

{
	"dependencies": {
		"typescript": "2.3.2"
	},
	"resolutions": {
		"typescript": "^4"
	}
}

会生成如下的 pnpm-lock.yaml

overrides:
  typescript: ^4
dependencies:
  typescript: 4.8.3
嵌套依赖只会重写一级,不会覆盖全部子依赖

与 yarn 和 npm 的重写规则不同,pnpm 在嵌套使用时,只会重写一级,有点类似于 Less 中的 “>” 语法。如有这样的依赖结构:

qar@1/
├─ fab@1/
│  ├─ zoo@2/
├─ zoo@1/

在使用如下 overrides 重写时:

{
  "qar@1>zoo": "3"
}

会得到这样的依赖树,仅有 qar 的直接依赖被重写了。

qar@1/
├─ fab@1/
│  ├─ zoo@2/
├─ zoo@3/

案例

临时版本降级

假设 @babel/core 依赖的 @babel/generator@7.19.0 出了 Bug,应用需要临时降级到 7.18.0

# package.json
{
  "dependencies": {
    "@babel/core": "^7.19.1"
  }
}
# 依赖结构
node_modules/
├─ @babel/core@7.19.1/
│  ├─ @babel/generator@7.19.0/

npm/tnpm npm mode overrides

{
  "dependencies": {
    "@babel/core": "^7.19.1"
  },
  "overrides": {
    "@babel/generator": "7.18.0"
  }
}

tnpm/yarn1/yarn3/pnpm resolutions

{
  "dependencies": {
    "@babel/core": "^7.19.1"
  },
  "resolutions": {
    "@babel/generator": "7.18.0"
  }
}

强制升级 node-saas

node-saas 因为其对 Node 版本有强要求,所以经常会在升级 Node 的过程中出现 node-sass 无法安装的问题(gyp Build Error)。

首先我们建议应用开发者更换 sass 实现到 dart-sass,可以从 sass 官网上获取。如果是子依赖中的 node-sass 版本与 node 冲突,则可以使用依赖重写来解决这个问题。

如原使用的 node-sass 为 4.12 ,不支持 Node 16+,即可利用重写强行写到 6.x。假设有如下的依赖树:

node_modules
├─ @ali/aone-editor
│  ├─ node-sass@4.12.0
├─ build-plugin-ice
│  ├─ node-sass@4.11.0

npm/tnpm npm mode overrides

{
  "overrides": {
    "node-sass": "^6"
  }
}

tnpm/yarn1/yarn3/pnpm resolutions

{
  "resolutions": {
    "node-sass": "^6"
  }
}

依赖多版本统一:webpack

一些不规范的包会引入低版本的 webpack,与其它依赖引用的高版本 webpack 冲突,进而造成问题。如下面这个案例,因为项目中存在多个 webpack 版本,提升(hoist)依赖后,根目录的 webpack 版本不一样,导致另一个不规范引用 webpack 的包(mini-css-extract-plugin)引用的版本不兼容。

{
  "dependencies": {
    "@ali/jstracker": "5.3.2",
    "@alib/build-scripts": "0.1.32"
  }
}

要修复,需要将项目中的 webpack 版本保持统一,或者升级 @ali/jstracker 中的 webpack 版本。

npm/tnpm npm mode overrides

{
  "overrides": {
    "webpack": "4.46.0",
    # 或
    "@ali/jstracker": {
      "webpack": "4.46.0"
    }
  }

tnpm/yarn1 resolutions

{
  "overrides": {
    "webpack": "4.46.0",
    # 或
    "@ali/jstracker/**/webpack": "4.46.0"
  }

yarn3 resolutions

{
  "overrides": {
    "webpack": "4.46.0",
    # 或
    "@ali/jstracker/webpack": "4.46.0"
  }

pnpm overrides

{
  "pnpm": {
  "overrides": {
    "webpack": "4.46.0",
    # 或
    "@ali/jstracker>webpack": "4.46.0"
  }
 }
}

尾注

不知道 npm 有一天能不能真的像它们代码里写的,干掉其它包管理软件,减少点前端开发者需要学的工具的数量……但看目前 Node 的发展趋势,是打算通过 Corepack 和 packageManager 字段,来兼容不同项目下不同的包管理软件了。

不过也正是因为各有所长,才会百花齐放,One for All 的包管理软件还道阻且长呢。前端同学们学不动也得学,毕竟这就是前端生态的特色之一嘛!

参考资料

使用工具

目录
相关文章
|
7月前
|
前端开发 jenkins 持续交付
新的centos7.9安装docker版本的jenkins2.436.1最新版本-前端项目发布(五)
新的centos7.9安装docker版本的jenkins2.436.1最新版本-前端项目发布(五)
185 1
|
1月前
|
前端开发
如何解决前端工程化中出现的版本冲突问题?
如何解决前端工程化中出现的版本冲突问题?
48 4
|
2月前
|
机器学习/深度学习 弹性计算 自然语言处理
前端大模型应用笔记(二):最新llama3.2小参数版本1B的古董机测试 - 支持128K上下文,表现优异,和移动端更配
llama3.1支持128K上下文,6万字+输入,适用于多种场景。模型能力超出预期,但处理中文时需加中英翻译。测试显示,其英文支持较好,中文则需改进。llama3.2 1B参数量小,适合移动端和资源受限环境,可在阿里云2vCPU和4G ECS上运行。
127 1
|
2月前
|
缓存 前端开发 JavaScript
前端架构思考:代码复用带来的隐形耦合,可能让大模型造轮子是更好的选择-从 CDN 依赖包被删导致个站打不开到数年前因11 行代码导致上千项目崩溃谈谈npm黑洞 - 统计下你的项目有多少个依赖吧!
最近,我的个人网站因免费CDN上的Vue.js包路径变更导致无法访问,引发了我对前端依赖管理的深刻反思。文章探讨了NPM依赖陷阱、开源库所有权与维护压力、NPM生态问题,并提出减少不必要的依赖、重视模块设计等建议,以提升前端项目的稳定性和可控性。通过“left_pad”事件及个人经历,强调了依赖管理的重要性和让大模型代替人造轮子的潜在收益
|
4月前
|
JavaScript 前端开发 Java
SpringBoot + Vue 前端后分离项目精进版本
这篇文章详细介绍了一个基于SpringBoot + Vue的前后端分离项目的搭建过程,包括前端Vue项目的初始化、依赖安装、页面创建和路由配置,以及后端SpringBoot项目的依赖添加、配置文件修改、代码实现和跨域问题的解决,最后展示了项目运行效果。
SpringBoot + Vue 前端后分离项目精进版本
|
7月前
|
资源调度 监控 前端开发
第七章(原理篇) 微前端技术之依赖管理与版本控制
第七章(原理篇) 微前端技术之依赖管理与版本控制
221 0
|
2月前
|
前端开发
开发指南047-前端模块版本
平台前端框架内置了一个文件version.vue
|
4月前
|
前端开发 Oracle Java
【前端学java】java开发的依赖安装与环境配置(1)
【8月更文挑战第8天】java开发的依赖安装与环境配置
58 1
【前端学java】java开发的依赖安装与环境配置(1)
|
4月前
|
前端开发 JavaScript 程序员
成功解决:尚硅谷中的谷粒商城前端项目运行依赖问题。【详细图解+问题说明+解决思路】
这篇文章介绍了如何解决尚硅谷谷粒商城前端项目中遇到的依赖问题,通过修改`package.json`和`package-lock.json`中的`node-sass`和`sass-loader`版本,成功解决了node版本与这些依赖的兼容性问题。
成功解决:尚硅谷中的谷粒商城前端项目运行依赖问题。【详细图解+问题说明+解决思路】
|
5月前
|
前端开发
化学元素周期表1.0Vue前端页面版本
化学元素周期表1.0Vue前端页面版本