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

2022-06-29 14:49:25 浏览数 (3)

笔者之前在开发模块分析工具,使用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前端团队

0 人点赞