前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >从 UNMET PEER DEPENDENCY 中理解依赖版本管理

从 UNMET PEER DEPENDENCY 中理解依赖版本管理

作者头像
用户1097444
发布2022-06-29 14:49:25
4.2K0
发布2022-06-29 14:49:25
举报
文章被收录于专栏:腾讯IMWeb前端团队

笔者之前在开发模块分析工具,使用npm list命令时遇到 UNMET PEER DEPENDENCY 这个问题,在探究解决方法的时候对npm的包管理机制有了很多新的认识,分享一下过程中的思考。

UNMET PEER DEPENDENCY 是什么 ?

你在使用npm list命令的时候,可能遇到过下面这种npm ERR:

UNMET PEER DEPENDENCY ERR

当你去检查依赖的树状结果,你会发现每一行npm ERR都有对应一行这样的结果:

UNMET PEER DEPENDENCY,翻译过来还挺难理解的,意思是说父依赖缺少了这个依赖的对等版本。拿上面的例子来说,就是eslint-config-imweb的0.2.10版本,需要版本在4.9.0到5.0.0这个区间(左闭右开)的eslint包。

你可能会发现上面例子中,imweb的eslint规则是从airbnb风格继承而来的,所以这个版本的eslint其实是airbnb这个包所缺失的。缺失的这个版本的eslint包没有被安装,它在依赖树中所在的层级尚不明确,因此在eslint-config-imweb、eslint-config-airbnb下都出现了UNMET PEER DEPENDENCY的提示。

按理说,执行过npm install,我的node_modules就已经有一个eslint了,怎么会提示我缺了eslint。其实这正是模块分析工具的需求痛点,项目下的某个包,往往会在依赖树的不同节点,存在多种版本。在深究原因之前,我们需要了解平时常见的版本号规则,以及npm在install的时候是如何进行依赖管理的。

依赖版本管理规则

我们开发者在发布自己的npm包时,当然是力求功能稳定,往往会在package.json的dependencies字段对相关依赖设定不同程度的约束:

代码语言:javascript
复制
"dependencies": {
   "signale": "1.4.0",
   "figlet": "*",
   "react": "16.x",
   "table": "~5.4.6",
   "yargs": "^14.0.0"
}

上面的这些版本号表示,都是基于SemVer规范而来的。它是由Github起草的一个具有指导意义的,统一的版本号表示规则。实际上就是Semantic Version(语义化版本)的缩写。

SemVer规范官网:https://semver.org/

像前面三个包的形式很容易理解:

代码语言:javascript
复制
"signale": "1.4.0": 固定版本号
"figlet": "*": 任意版本(>=0.0.0)
"react": "16.x": 匹配主要版本(>=16.0.0 <17.0.0)
"react": "16.3.x": 匹配主要版本和次要版本(>=16.3.0 <16.4.0)

^和~则比较特别,它们分别可以做到上面第三条规则和第四条规则的效果(最高版本为最新版本),同时又兼容了主版本号/次版本号为0的情况:

代码语言:javascript
复制
~: 当安装依赖时获取到有新版本时,安装到 x.y.z 中 z 的最新的版本。即保持主版本号、次版本号不变的情况下,保持修订号的最新版本。
^: 当安装依赖时获取到有新版本时,安装到 x.y.z 中 y 和 z 都为最新版本。 即保持主版本号不变的情况下,保持次版本号、修订版本号为最新版本。

当主版本号为 0 的情况,会被认为是一个不稳定版本,情况与上面不同:
主版本号和次版本号都为 0: ^0.0.z、~0.0.z 都被当作固定版本,安装依赖时均不会发生变化。
主版本号为 0: ^0.y.z 表现和 ~0.y.z 相同,只保持修订号为最新版本。

发布包的时候,我们也需要严格按SemVer规范来指定版本号,可以用semver这个npm包来帮助我们对版本号做一些比较。

semver文档:https://github.com/npm/node-semver

  • 安装
代码语言:javascript
复制
npm install semver
  • 判断版本号是否符合规范,返回解析后符合规范的版本号
代码语言:javascript
复制
semver.valid('1.2.3') // '1.2.3'
semver.valid('a.b.c') // null
  • 一些其他用法
代码语言:javascript
复制
semver.clean('  =v1.2.3   ') // '1.2.3'
semver.satisfies('1.2.3', '1.x || >=2.5.0 || 5.0.0 - 7.2.3') // true
semver.minVersion('>=1.0.0') // '1.0.0'

npm install 的时候,间接依赖呈现怎样的结构 ?

在理解了版本号规则之后,我们可以开始慢慢窥探npm依赖管理背后的问题了。开发者在publish一个npm包之后,或多或少要约束某些包的版本,防止相关依赖的更新,造成功能的变化,尤其是在相关依赖还没有经过完善的测试的情况下。比如说,我发布了一个A包,里面依赖了lodash的^2.2.0:

代码语言:javascript
复制
# node_modules/A/package.json
"dependencies": {
   "lodash": "^2.2.0"
}

在某个项目中,我使用到了A包:

代码语言:javascript
复制
# project/package.json
"dependencies": {
   "A": "^1.0.0"
}

对于项目—>A包->lodash这样一条简单的间接依赖链路,似乎没有看出太大问题,只要A包的开发者足够信任lodash的测试和发布环节,A包的功能不会出太多岔子。我们尝试npm install之后,依赖树大概会是这样子的:

代码语言:javascript
复制
`-- A@1.1.0
  `-- lodash@2.9.9

显然lodash有着更新的版本,但A包并没使用到,它的package.json写死了低版本。假如现在我们的项目又引入了其他的依赖,比如说一个B包,人家用的lodash是最新的( ^4.17.20)。

代码语言:javascript
复制
# project/package.json
"dependencies": {
   "B": "^4.3.2",
   "A": "^1.0.0"
}

再次尝试npm install,依赖树是这样子的:

代码语言:javascript
复制
+-- lodash@4.17.20
+-- B@4.3.2
`-- A@1.1.0
  `-- lodash@2.9.9

现在我们有两条间接依赖的链路了,分别是项目—>A包->lodash,项目—>B包->lodash,而且lodash版本不相同,其中B包的lodash来到了和A包/B包同一层级的位置。这是 npm 3.x 版本以后 node_modules 的扁平结构。npm install时会将dependencies中位置靠前的包中的依赖,提升到上一级,这是为了解决 npm 3.x 版本之前嵌套结构造成的模块冗余问题,当父级目录的lodash能够满足C包、D包等依赖的lodash版本,那么就不必重复安装,npm install将会跳过这一过程。

罪魁祸首——peerDependencies

到这里,我们大概已经知道npm install给我们的node_modules形成了怎样的结构,现在可以来看看UNMET PEER DEPENDENCY是怎么出现的了。首先来介绍一下,package.json中和依赖管理相关的几个字段:

  • dependencies
  • devDependencies
  • optionalDependencies 可选择的依赖包
  • peerDependencies 同等依赖
  • bundledDependencies 捆绑依赖包

UNMET PEER DEPENDENCY 的成因,就是和 peerDependencies 这个字段密切相关。这五个字段的区别和应用场景,我们可以都看一下。因为,你可能不止会遇到UNMET PEER DEPENDENCY,还有UNMET OPTIONAL DEPENDENCY之类的,当你理解了这五个字段之后,你就知道应该如何处理UNMET DEPENDENCY系列的问题了。

1、dependencies dependencies 是无论在开发环境还是在生产环境都必须使用的依赖,是我们最常用的依赖包管理对象,例如 React,Loadsh,Axios 等,通过 npm install XXX 下载的包都会默认安装在 dependencies 对象中,也可以使用 npm install XXX --save 下载 dependencies 中的包; 2、devDependencies devDependencies 是指可以在开发环境使用的依赖,例如 eslint,debug 等,通过 npm install packageName --save-dev 下载的包都会在 devDependencies 对象中; dependencies 和 devDependencies 最大的区别是在打包运行时,执行 npm install 时默认会把所有依赖全部安装,但是如果使用 npm install --production 时就只会安装 dependencies 中的依赖,如果是 node 服务项目,就可以采用这样的方式用于服务运行时安装和打包,减少包大小。 3、optionalDependencies optionalDependencies 指的是可以选择的依赖,当你希望某些依赖即使下载失败或者没有找到时,项目依然可以正常运行或者 npm 继续运行的时,就可以把这些依赖放在 optionalDependencies 对象中,但是 optionalDependencies 会覆盖 dependencies 中的同名依赖包,所以不要把一个包同时写进这两个对象中。 optionalDependencies 就像是我们的代码的一种保护机制一样,如果包存在的话就走存在的逻辑,不存在的就走不存在的逻辑。 4、peerDependencies peerDependencies 用于指定你当前的插件兼容的宿主必须要安装的包的版本。举个例子:我们常用的 react 组件库 ant-design@3.x 的 package.json 中的配置如下: "peerDependencies": {  "react": ">=16.9.0",  "react-dom": ">=16.9.0"  },  假设我们创建了一个名为 project 的项目,在此项目中我们要使用 ant-design@3.x 这个插件,此时我们的项目就必须先安装 React >= 16.9.0 和 React-dom >= 16.9.0 的版本。 在 npm 2 中,当我们下载 ant-design@3.x 时,peerDependencies 中指定的依赖会随着 ant-design@3.x 一起被强制安装,所以我们不需要在宿主项目的 package.json 文件中指定 peerDependencies 中的依赖,但是在 npm 3 中,不会再强制安装 peerDependencies 中所指定的包,而是通过警告的方式来提示我们,此时就需要手动在 package.json 文件中手动添加依赖; 5、bundledDependencies 这个依赖项也可以记为 bundleDependencies,与其他几种依赖项不同,他不是一个键值对的对象,而是一个数组,数组里是包名的字符串,例如: {  "name": "project",  "version": "1.0.0",  "bundleDependencies": [    "axios",     "lodash"  ]  } 当使用 npm pack 的方式来打包时,上述的例子会生成一个 project-1.0.0.tgz 的文件,在使用了 bundledDependencies 后,打包时会把 Axios 和 Lodash 这两个依赖一起放入包中,之后有人使用 npm install project-1.0.0.tgz 下载包时,Axios 和 Lodash 这两个依赖也会被安装。需要注意的是安装之后 Axios 和 Lodash 这两个包的信息在 dependencies 中,并且不包括版本信息。 "bundleDependencies": [  "axios",  "lodash"  ], "dependencies": {  "axios": "*",  "lodash": "*"  },  如果我们使用常规的 npm publish 来发布的话,这个属性是不会生效的,所以日常情况中使用的较少。

怎么解决 UNMET PEER DEPENDENCY ?

peerDependencies尽管指定了使用某些插件时,必须要安装的包的版本。但在不影响开发的情况下,UNMET PEER DEPENDENCY一般是可以无视的,因为现存的很多UNMET PEER DEPENDENCY错误,都将已安装的包版本指向了一个较低的版本。或者这么说,开发者已经很久没对peerDependencies这个字段进行更新了,像我们在描述间接依赖的时候,A包可能在peerDependencies这个字段里面,制定我们的lodash必须安装^2.2.0版本,可我们项目全局早就有一个4.17.20的船新版本了。

比方说,我们采用手动安装的方式去安装我们缺失的peerDependencies:

代码语言:javascript
复制
npm install lodash@^2.2.0

猜猜会发生什么?这不就是49年入国军嘛,我们项目全局的4.17.20版本被替换掉了,变成了一个2.9.9的版本了。

实际上,也确实如此,在我的项目中,遇到了stylelint-webpack-plugin的0.10.5版本,显然它的peerDependencies是包含了stylelint,并通过警告的方式,要求我安装一个低版本的stylelint,那我装一下试试,看看能不能解决npm ERR:

现实往往是,不能两全其美。我通过这种手动安装的方式,是对项目全局的依赖进行了降级,如果有其他的子依赖也用到了stylelint的高版本,就受到了影响。

所以当出现这种问题了,其实应该尽可能要求包的发布者去更新一下peerDependencies。当然,如果你是个强迫症,不想看到这恼人的npm ERR,可以试试下面的方法。

强迫症看这里

1、根据我在google上搜索的一些解决方法,最简单的方法是在系统全局安装缺失的依赖(不需要指定版本),参考这个StackOverflow:https://stackoverflow.com/questions/35419179/unmet-peer-dependency-generator-karma-0-9-0 也就是把npm ERR这个错误报出来的所有包,一行全局安装。缺点是,只能解决其中一个子依赖抛出的peerDependencies。假如还有很多子依赖,用到了更低的版本,那就用下面这种吧。

2、另一种方法是对每个npm ERR报出的包,进入到node_modules中对应包的目录中,进行单独的安装,并指定版本(想想就麻烦)。

写在最后

其实这篇文章的重点,不在于说怎么去解决 UNMET PEER DEPENDENCY 这个问题,而是希望通过这个奇怪的现象,去理解包的依赖管理,以及npm install过程中的一些细节。在最初遇到这个问题的时候,我查阅了很多资料,最后发现仅仅是npm设计上的一些怪异之处。但在过程中其实对package.json,扁平结构和lock等设计都有了崭新的认识。

参考文章

[1] 剖析npm包管理机制

[2] npm 依赖管理中被忽略的那些细节

IMWeb 团队隶属腾讯公司,是国内最专业的前端团队之一。

我们专注前端领域多年,负责过 QQ 资料、QQ 注册、QQ 群等亿级业务。目前聚焦于在线教育领域,精心打磨 腾讯课堂、企鹅辅导 及 ABCMouse 三大产品。

扫码关注 腾讯IMWeb前端团队

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2020-10-17,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 腾讯IMWeb前端团队 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • UNMET PEER DEPENDENCY 是什么 ?
  • 依赖版本管理规则
  • npm install 的时候,间接依赖呈现怎样的结构 ?
  • 罪魁祸首——peerDependencies
  • 怎么解决 UNMET PEER DEPENDENCY ?
  • 强迫症看这里
  • 写在最后
    • 参考文章
    领券
    问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档