目录
[发布一个 npm 包](#发布一个 npm 包)
[不使用 peerDependencies](#不使用 peerDependencies)
[使用 peerDependencies](#使用 peerDependencies)
[pnpm 下的 peerDependencies](#pnpm 下的 peerDependencies)
[不同 xxxDependencies 依赖类型声明的区别](#不同 xxxDependencies 依赖类型声明的区别)
前言
peerDependencies 表示同版本依赖,简单来说就是,如果你安装我,那么你最好也安装我对应的依赖。比如说 Element Plus 组件库,它需要宿主环境提供指定的 Vue 版本来搭配使用,所以 Element Plus 组件库项目需要在 package.json 文件中进行如下配置:
"peerDependencies": {
"vue": "^3.2.0"
}
我们知道 Element Plus 是 Vue3 的组件库,所以必然是需要运行在 Vue3 的环境下。
那么为什么 npm 包规范需要在 package.json 定义一个 peerDependencies 属性呢?使用了 peerDependencies 之后又会造成什么问题呢?不使用 peerDependencies 可不可以呢?本文将围绕这些疑问进行发布一个 npm 包来展开探索。
依赖版本号的前缀代表的意思
我们今天是探讨 peerDependencies 属性,所以我们创建一个名为 peer-dependencies-test-npm目录,然后进到目录之后初始化 npm 项目,也就是在命令终端运行以下命令:
npm init -y
运行命令之后会在项目根目录下生成一个 package.json 的文件。我们假定我们这个 npm 包依赖 vue@3.2.0,所以我们需要进行指定版本安装:npm install vue@3.2.0。安装之后我们就可以在 package.json 下出现了以下内容:
"dependencies": {
"vue": "^3.2.0"
}
又因为安装的时候会默认使用前缀 ^,我们是指定版本,我们不希望添加前缀 ^,所以我们在安装之前需要进行去除默认使用前缀的操作。
我们删掉上述安装的 vue@3.2.0 的包:npm uninstall vue,之后我们再进行以下操作。
不使用前缀,保存确切版本 :项目根创建.npmrc
registry=https://registry.npmjs.org/
# 默认不带 ^
save-exact=true
执行上述操作之后,我们再进行安装:npm install vue@3.2.0。安装之后我们发现 package.json 中关于 vue 版本的定义选项中就没有了前缀 ^ 符号了。
"dependencies": {
"vue": "3.2.0"
}
package.json 中依赖版本号的前缀代表的意思:
~波浪号,匹配最新补丁版本号,即版本号的第三个数字,例如~5.0.0就会匹配 5.0.x 版本,将在 5.1.0 停止^插入符号,匹配次要的版本号,即版本号的第二个数字,例如^5.2.0就会匹配等于或大于设置的版本(这里就是 >= 5.2.0 < 6.0.0 ),将在 6.0.0 停止>、<、>=、<=比较运算符,匹配的就是这个区间的版本,例如>2.0.0 <= 2.1.4,就会匹配这个区间的版本号
发布一个 npm 包
发布一个 npm 包其实很简单,我们先要在 npm 官网:www.npmjs.com 注册一个账号。然后打开终端切换到对应的 npm 包所在的目录,然后进行以下操作:
在终端登录 npm
切换到 npm 官方源
npm config set registry https://registry.npmjs.org/
开启双重身份验证 (2FA)认证开启教程
登录 npm
npm login
发布 npm 包
接着在终端运行 npm publish 命令,就可以将包发布到 npm 上了。
npm publish
注意:包名不能重复,发布之前最好去 npm 官网查询一下,看看对应的包名被占用了没有。
发布成功之后在终端会出现以下内容:
peer-dependencies-test-npm@1.0.0
删除已发布的包
最后删除已发布的包,发布的时候需要慎重一点,尽量不要往 npm 上发布没有意义的包。比如我们这次发布的包只是为了测试,所以测试完毕我们就要把它删除掉。
删除已发布的包,只需要运行 npm unpublish 包名 --force 命令,即可从 npm 删除已发布的包。另外需要注意的是只能删除72小时以内的包,删除之后24小时以内不允许重复发布。
npm unpublish peer-dependencies-test-npm --force
不使用 peerDependencies
我们再进行创建一个测试项目进行测试。创建 test-peer 目录,然后进到 test-peer 根目录下运行 npm init -y 初始化 npm 项目。
然后我们安装 Vue:npm install vue,这样会默认安装最新版本的 Vue。我们看到 package.json 的 dependencies 选项会出现 vue 相关的内容:
"dependencies": {
"vue": "^3.5.28"
}
我们可以看到 3.2.28 是目前 Vue 最新的版本。此外我们也可以在 node_modules 目录下看到了 Vue 的相关文件。

接着我们安装我们上面发布到 npm 的包 npm install peer-dependencies-test-npm,安装完之后我们发现node_modules 目录下的 peer-dependencies-test-npm 目录中还存在 node_modules 目录,目录中的内便是 Vue 相关的包目录。

这个时候我们需要复习一下 npm install 的时候发生了什么,
执行 npm install 的时候会检查项目中有没有 package-lock.json 文件,有则检查 package-lock.json 文件和 package.json 文件的依赖声明版本是否一致。
如果没有 package-lock.json 文件,则会根据 package.json 文件递归构建依赖树,然后按照构建好的依赖树下载完整的依赖资源。
关键在构建依赖树的时候,会遵循扁平化的原则,无论是直接依赖还是子依赖的依赖,都优先将其放在 node_modules 根目录下。在这个过程中遇到相同的依赖时,会先判断已经放置在依赖数中的依赖版本是否符合新模块对依赖版本的要求,如果符合就将其丢弃,不符合则在新模块的 node_modules 目录下放置该依赖。
很明显我们新项目中安装 Vue 版本是 3.5.28,而我们在上面设置 peer-dependencies-test-npm 包对 Vue 的依赖版本恒定为 3.2.0,所以 npm install 安装构建依赖树的时候,首先对项目中的声明进行依赖构建,那么项目中的 Vue 依赖声明的版本是 3.5.28 ,然后再进行递归循环构建子依赖的依赖的时候,在依赖包 peer-dependencies-test-npm 中发现相同的 Vue 依赖,然后进行版本判断的时候,发现已经存在依赖树中的 Vue 版本依赖不符合 peer-dependencies-test-npm 模块对 Vue 依赖版本的要求,所以就需要在 peer-dependencies-test-npm 模块下的 node_modules 目录中构建新的 Vue 依赖。
如果我们不希望出现这种重复安装依赖的情况,我们可以根据上文中说到的 package.json 中依赖版本号的前缀代表的意思,进行设置。比如我们就可以在peer-dependencies-test-npm 包中设置对 Vue 的版本依赖不是恒定的,比如设置 ^ 开头。^ 插入符号,匹配次要的版本号,即版本号的第二个数字,例如上面设置 peer-dependencies-test-npm 包对 Vue 的依赖版本更改为 ^3.2.0 就会匹配任何 3.x.x版本,将在 4.0.0 停止。所以我们 test-peer 项目中安装的 vue@3.5.28 是符合要求的,这样就不会重复安装依赖了。
更改之后的安装依赖情况截图:

可以看到我们把 peer-dependencies-test-npm 包对 Vue 的依赖版本更改为 ^3.2.0 之后再次发布新版本的 npm 包,再在进行安装的时候,根目录的 node_modules 目录下的 peer-dependencies-test-npm 依赖包已经没有了 Vue 相关的依赖了。
但很明显这种利用依赖前缀的方式解决宿主环境依赖与插件依赖包不一致的问题,只能解决同一个大版本号下面的小版本号不同的情况,并不能解决大版本号不同的时候依赖包不一致的问题。
事实上像 Element Plus 这类 UI 组件库又或者像一些 Webpack、Vite、Babel、Eslint 等的插件,它们都有一个共同特点,就是不能单独运行且毫无意义,它们必须运行在各自的本体环境下,比如 Element Plus 就必须运行在本体 Vue3 的环境下。它们无须对本体依赖进行声明,而应该直接使用宿主项目中的本体依赖。这个时候我们就可以使用 peerDependencies 选项对本体依赖进行声明了。
使用 peerDependencies
我们对 peer-dependencies-test-npm 包进行更改,把 Vue 依赖的声明放置到 peerDependencies 属性选项中,然后再进行更新到 npm 中。
"peerDependencies": {
"vue": "^3.2.0"
}
这时,我们把 test-peer 项目中的 peer-dependencies-test-npm 依赖删除掉,即运行此命令 npm uninstall peer-dependencies-test-npm 。然后再重新安装上面更新之后的 peer-dependencies-test-npm 包,我们再看看根目录 node_modules 下的 peer-dependencies-test-npm 目录。

此时我们发现peer-dependencies-test-npm目录下也没有 Vue 相关的依赖了。
我们把 test-peer 项目中的根目录 node_modules 文件删了,直接安装 peer-dependencies-test-npm 。
npm i peer-dependencies-test-npm
安装的依赖结果:

我们发现当宿主环境中没有子依赖 peerDependencies 中声明的依赖时,也会把 peerDependencies 中声明的依赖进行安装。
注意我测试安装的 npm 版本为 10.8.2 结果都如上图。所以可以得出的结论是:如果宿主环境没有依赖核心库,则会按照 peerDependencies 的声明将相关依赖安装到宿主项目的根目录的 node_modules 中。
我们把 test-peer 项目中的根目录 node_modules 文件删了,然后安装一个 Vue2 的包。
npm install vue@2.6.0
然后我们再安装 peer-dependencies-test-npm 包
npm i peer-dependencies-test-npm

我们发现在此种情况下安装的话会报错了。所以我们可以得出结论:在 npm 7 + 中,如果存在无法自动解决的依赖冲突,将会阻止安装并抛出错误。
从上述抛出的错误中可以看到官方给出的解决方案。
npm ERR! Fix the upstream dependency conflict, or retry npm ERR! this command with --force, or --legacy-peer-deps npm ERR! to accept an incorrect (and potentially broken) dependency resolution.
在错误中建议我们使用 --force 或 --legacy-peer-deps 命令进行重新安装。那么我们就先来了解一下这两个命令是什么意思。
- --legacy-peer-deps:安装的时候忽略所有对等依赖(peerDependencies),以 npm v4 ~ v6 的方式安装
- --force 或 -f: 强制安装
也就是说在 npm 7 中我们遇到对等依赖冲突我们就可以使用以下方式进行安装:
npm install --legacy-peer-deps
// 或者
npm install --force
我们通过上述方式进行安装的时候,我们发现又可以成功安装了。

使用 --force 安装则会有警告

这里有一点需要注意的是,我们使用 --legacy-peer-deps 或者 --force 进行安装,只是以 npm v4 ~ v6 的方式进行安装,**npm v4 ~ v6 的方式就是当有 peerDependencies 声明的依赖发生冲突时,以宿主环境的依赖为准进行安装,**所以即便安装成功,也不一定代表你的项目能正常运行。
比如说我们上面测试项目中的宿主环境的 Vue 版本是 2.6.0,但 peer-dependencies-test-npm 包中的依赖要求的是 vue@3.2.0,那么是否能正常运行就看插件 peer-dependencies-test-npm 是否有进行相应的兼容性处理了,毕竟 Vue3 也是可以兼容部分 Vue2 的写法的,但像我们研究的 Element Plus 则是完全不兼容 Vue2 的,所以 Element Plus 是无法运行在一个宿主环境 Vue 依赖的版本是 2.x.x 及以下的版本的。
在此我们可以做个小总结:在 npm 的之前版本(v4 - v6)中,安装的时候发生 peerDependencies 依赖冲突会有版本不兼容的警告,但仍会安装依赖并不会抛出错误,但在 npm 7 中,如果存在无法自动解决的依赖冲突,将会阻止安装,并抛出错误。这个其实主要是为了确保一个项目中的依赖版本是唯一。
pnpm 下的 peerDependencies
我们知道 pnpm 是最新的 npm 包管理工具,官方的定义是 - 速度快、节省磁盘空间的软件包管理器。 我们研究的 Element Plus 组件库就是基于 pnpm 提供的 Monorepo 功能进行设计的代码项目架构。那么使用 pnpm 进行安装的时候遇到 peerDependencies 又会发生些什么呢?我们接下来就根据上面的测试,继续使用 pnpm 再进行测试一遍。
首先是宿主环境没有子依赖在 peerDependencies 中声明的依赖的情况。具体在我们测试项目中就是项目中没有安装 Vue 相关的版本的包,然后进行安装 peer-dependencies-test-npm 包。
pnpm i peer-dependencies-test-npm
安装结果:

这个时候我们便可以看到根目录 node_modules 下的 .pnpm 文件中多了 Vue 相关的依赖包了。也就是当在进行 pnpm install 的时候,会自动安装 peerDependencies 中的依赖
默认将自动安装任何缺少的非可选对等依赖。
接着我们看看依赖有冲突的情况。

我们看到当宿主依赖与子依赖的 peerDependencies 依赖发生冲突时,还是能安装,但报了一个错误

从根目录 node_modules 安装的依赖来看,当发生 peerDependencies 依赖冲突时,是以宿主环境中依赖为准。 不会安装 peerDependencies依赖, 只是会提醒你这个版本不符合插件的要求。
auto-install-peers
- 默认值:true
- 类型:Boolean
当值为 true 时,将自动安装任何缺少的非可选对等依赖。
版本冲突
如果不同软件包对某个依赖项的版本要求存在冲突,pnpm 不会自动安装任何冲突的依赖项版本,而是会打印一条警告信息。比如说我们上面测试项目中的宿主环境的 Vue 版本是 2.6.0,但 peer-dependencies-test-npm 包中的依赖要求的是 vue@3.2.0 则这些要求相互冲突,不会进行自动安装。
冲突解决
如果出现版本冲突,您需要自行评估要安装哪个版本的对等依赖项,或者更新依赖项以使其对等依赖项要求保持一致。
strict-peer-dependencies
- 默认值: false
- 类型:Boolean
如果启用了此选项,那么在依赖树中存在缺失或无效的 peer 依赖关系时,命令将执行失败。

到这里我们可以发现 pnpm 7 和 npm 7 对 peer 依赖的处理方式是一致的,都是要求依赖要一致才能将命令执行成功。但在 npm 4 ~ 6 时代,我们养成的习惯是,peer 依赖是我们自己解决,而且作为一个成熟的开发者应该是知道相关插件的 peer 依赖与宿主环境依赖的关系的,就比如说我们研究的 Element Plus 的 peer 依赖是 Vue3 一样,我们是十分清楚的,所以我们不会在 Vue2 的环境下去使用 Element Plus。因此strict-peer-dependencies的默认值改为了false
不同 xxxDependencies 依赖类型声明的区别
我们知道在 package.json 文件中常见的依赖声明类型有 dependencies(生产依赖)、devDependencies(开发依赖) 和 本文中讨论的 peerDependencies(同版本依赖),主要是这三种,此外还有 bundledDependencies(捆绑依赖)、optionalDependencies(可选依赖)。
dependencies 表示生产依赖,在此选项中定义的依赖都是需要在生产环境上运行的。当它作为一个 npm 包中的依赖声明被引用下载时,在此选项中定义的依赖都会被下载,其中还有一个点就是如果在依赖树中已经存在相关依赖了,那么就需要进行依赖版本的判断,如果符合那么就会被丢弃,不符合那么就会在该模块下的 node_modules 中新建一个依赖项。
devDependencies 表示开发依赖,在此选项中定义的依赖都是不需要在生产环境上运行的,一般只在开发阶段起作用或者只在开发环境中被用到。当它作为一个 npm 包中的依赖声明被引用下载时,在此选项中定义的依赖都不会被下载。
peerDependencies 表示同版本依赖,简单来说就是,如果你安装我,那么你最好也安装我对应的依赖。就比如你安装 Element Plus,那么你也需要安装 Element Plus 的 peer 依赖,因为 Element Plus 脱离了 peer 依赖是无法单独运行的,它必须宿主环境提供对应的 peer 依赖。
我们在讲 dependencies 时说到,执行 npm install 时当子依赖的依赖版本在已经构建的依赖树中匹配不到时,就会重复下载。而 peerDependencies 则可以解决这个问题,在 peerDependencies 中定义的依赖,不会重复下载,但会要求宿主环境提供 peer 依赖一致的版本,否则在安装的时候提示警报亦或报错中断命令的执行。
这里我们还需要说明的是,执行 npm install 的时候,会同时安装 dependencies 和 devDependencies 中的依赖到 node_modules 中。所以在一个宿主项目中,其实把依赖在 dependencies 或者 devDependencies 中进行声明,本质上没区别。但如果我们的项目是一个 npm 包的项目,将来会被当作 npm 包进行引用下载的时候,就需要注意了。我们就需要把那些只在开发阶段起作用或只在开发阶段使用的依赖,比如 webpack、eslint、babel 等放在 devDependencies 中进行声明了**。因为将来安装 npm 包的时候,devDependencies 中声明的依赖是不会被下载的。**
最后说一下打包的问题,不管是 dependencies 还是 devDependencies 还是 peerDependencies 中的依赖,只要在项目中进行了引用,那么打包的时候都会把在项目中引用了的依赖进行打包,也就是说并不是放在 devDependencies 中依赖就不会被打包。即便在 dependencies 进行了声明,也可以在打包工具中进行设置不进行打包到 bundle 文件中。一般来说在 peerDependencies 中的声明的依赖都在打包工具中设置不打包到 bundle 文件中,从而达到减少打包文件的体积。
所以对于一般普通项目来说,dependencies 和 devDependencies 选项更多的是起到规范的作用。
这里我们只作常用的三个依赖声明类型的讲解,其他选项就不作过多解读了。
总结
为了深入理解 peerDependencies 我们发布了一个 npm 包进行了各种环境下的测试,从而加深了 npm install 机制的理解。
如果在一个 npm 包的 dependencies 中定义依赖,执行 npm install 时当子依赖的依赖版本在已经构建的依赖树中匹配不到时,就会重复下载。而 peerDependencies 则可以解决这个问题,在 peerDependencies 中定义的依赖,不会重复下载,但会要求宿主环境提供 peer 依赖一致的版本,否则在安装的时候提示警报亦或报错中断命令的执行。
具体就是不管是 npm 还是 pnpm 都会以宿主环境中依赖版本为主,区别就是不同版本的处理方式不一致,npm v4 ~ v6,发生 peer 依赖不一致的时候,会继续安装完成,(以宿主环境的依赖为准进行安装) 但会提示一个警报。
最新 npm 则会报错并中断命令执行。
- --legacy-peer-deps:安装的时候忽略所有对等依赖(peerDependencies),以 npm v4 ~ v6 的方式安装
- --force 或 -f: 强制安装
当有 peerDependencies 声明的依赖发生冲突时,以宿主环境的依赖为准进行安装,
最新 pnpm将自动安装任何缺少的非可选对等依赖。但是冲突的时候,不会进行自动安装。需要自行评估要安装哪个版本的对等依赖项,或者更新依赖项以使其对等依赖项要求保持一致。
文章采用的npm & pnpm 版本

在不同的环境下执行安装命令,peerDependencies 选项都会有不一样的表现,但总的来说无论是 npm 还是 pnpm 都是为了更好地遵循 peerDependencies 的核心要义,就是要求宿主环境提供 peer 依赖一致的版本。
只有深入理解了这些,才会深刻理解组件库或插件中为什么要在 package.json 文件中定义 peerDependencies 选项。