一文讲透 peerDependencies
前言
这是笔者撰写的第一篇前端相关的文章,如有错误,欢迎在评论区指正!
初识 --legacy-peer-deps
在前端开发中,我们肯定遇到过如下场景:
某一天,小豪接手了一个同事的项目,他将项目代码从gitlab上拉了下来,然后执行npm install大法,等待着依赖安装完毕。但是,没过多久,控制台便出现了一大片刺眼的红色:
小豪很清楚,这是由于版本问题导致的依赖冲突。不过,控制台的报错也给了对应的解决方案:npm i --force或是npm i --legacy-peer-deps。
但是,我们都知道,npm i --force并不是一个很好的解决方案,它会忽略冲突和警告,以及版本锁定策略(package-lock.json),重新生成依赖树。并且这份新的依赖关系很可能只在你的开发环境下可以运行,既不利于团队协作,也可能影响CI/CD环境的版本不一致。从长远来看,这并不是推荐的解法。因此,小豪选择了另一个:npm i --legacy-peer-deps。果不其然,在执行完这行命令后,依赖被顺利的安装了下来,也没有造成依赖版本的大范围重写。
那么,为什么使用--legacy-peer-deps就可以解决上述的依赖冲突呢?它是否会引入其它的潜在问题呢?它又与我们本文要聊的peerDependencies又有什么关系呢?以及为什么npm要有peerDependencies这个字段呢?我们将在下文中一个个揭晓。
什么是peerDependencies(同级依赖)
用比较官方的话术来讲,peerDependencies是一种在npm包中声明的依赖关系。它用来表示当前包的消费者(即依赖该包的另一个包/项目)应该安装的依赖包版本。而当前包与当前包的消费者即构成了“同级依赖”:两个包必须共享同一个依赖版本。
抽象的概念并不便于理解,我们直接上代码示例:
{
"name": "my-package",
"version": "1.0.0",
"peerDependencies": {
"vue": ">=3.0.0"
}
}如上代码意味着,使用my-package的项目也必须安装兼容版本的vue(版本大于等于3.0.0)。而此时vue也就是我们说的同级依赖。
当我们在安装项目依赖时,npm会检查peerDependencies与当前的项目依赖是否符合,若不符合且无法兼容则会抛出错误,并终止此次依赖安装。
具体作用
业务开发的前端同学可能很少会去指定peerDependencies字段。而peerDependencies也主要是为库/插件作者提供帮助。
我们来看如下场景:
某一天小豪接到了一个需求,需求的主要内容是开发一个webpack插件(我们暂且将其命名为awsome-webpack-plugin)。而这个插件依赖了webpack5的一些特性,因此需要确保使用awsome-webpack-plugin的项目安装了webpack5。于是小豪在插件包的package.json中添加了如下代码:
// awesome-webpack-plugin/package.json
{
"name": "awesome-webpack-plugin",
"version": "1.0.0",
"peerDependencies": {
"webpack": "^5.0.0"
}
}这样就避免了插件在不兼容的版本中运行时可能出现的问题。
再遇--legacy-peer-deps
但是,插件的作者有时候或许不是方方面面都能考虑到的。接下来再看另一种有点特殊的情况:
小豪这次又接到了一个需求,同样是做webpack插件。但这次插件的功能十分简单,只是做一些简单的文本复制功能(我们暂且将其命名为simple-copy-plugin)。可以看出该插件其实是与webpack版本无关的。但小豪作为插件作者,深知该插件当前仅被使用在一个技术栈较老的项目中,而该项目的webpack版本是4.x。不过小豪出于稳妥起见,还是指定了同级依赖:
// simple-copy-plugin/package.json
{
"name": "simple-copy-plugin",
"version": "1.0.0",
"peerDependencies": {
"webpack": "^4.0.0"
}
}这样导致的后果就是,非兼容版本的项目无法安装该插件。但该插件本不应被指定版本(因为在webpack4和5中都可以正常运行)。
那么,怎么解决这种情况呢?回到本文的开头:--legacy-peer-deps!
peerDependencies的处理策略
在此之前,先介绍一下不同的npm版本对于peerDependencies的处理策略。
npm 7 之前
- 声明但不自动安装:
peerDependencies仅作为一种声明,npm 不会自动安装这些依赖。开发者需要手动安装并管理这些依赖。 - 安装警告:如果
peerDependencies没有满足要求,npm 会在安装时发出警告,但不会阻止安装过程。
这种方式的优点是避免了自动安装引起的冲突,但需要开发者手动管理依赖,增加了管理的复杂性。
npm 7 之后
- 自动安装:npm 7 开始,
peerDependencies会自动安装。这简化了依赖管理,减少了开发者的手动操作。 - 严格验证:npm 7 会严格验证
peerDependencies的版本要求。如果版本不兼容,会导致安装失败,从而确保所有依赖的版本一致性。
自动安装和严格验证虽然简化了依赖管理,但在遇到版本冲突时也更容易导致安装失败。
而--legacy-peer-deps参数的作用就是:告诉npm在处理 peerDependencies 时,采用旧版本(npm6及之前)的行为方式,忽略掉这些冲突,让依赖安装可以正常进行。
依赖冲突与包管理器
其实我们在日常开发中可以发现,大部分依赖报错都是在使用npm时引起的。而为什么使用yarn和pnpm则不太会发生类似的情况呢?这就不得不讲讲不同包管理器的依赖管理策略了。
npm
- npm 尝试将所有的依赖安装在项目的根目录下,这种方式叫做“依赖扁平化”。
- 当不同的依赖需要同一个库的不同版本时,npm 可能会将某些版本安装在子节点的
node_modules中。这种处理方式可能会导致依赖冲突和版本不一致的问题。 - npm v7+ 引入了自动安装
peerDependencies的特性,这在某些情况下会导致更多的依赖冲突和安装问题。
yarn
锁文件与一致性
- Yarn 强调确定性和一致性,它会创建一个
yarn.lock文件来精确记录每个依赖的版本。这样确保了每次安装的依赖版本都一致。 - Yarn 在处理
peerDependencies时更为宽松,不会自动安装它们,而是要求用户在主项目中明确指定这些依赖,减少了冲突的发生。
package-lock.json
这时或许有的小伙伴会说了,npm安装依赖时不是也会生成 package-lock.json吗?这个文件就是用来锁定依赖版本的呀!但其实很多时候我们发现,重新执行npm i命令时,也可能引起package-lock.json文件的更新。
因为npm是遵循语义化版本规范的,在安装依赖时,会尽可能去满足规范约束的范围内的最新版本。例如,如果 package.json 中指定了某个依赖的版本为 ^1.0.0,而该依赖在更新后推出了 1.1.0,则 npm install 可能会更新到 1.1.0。
如果想让npm完全按照 package-lock.json去安装依赖,可以选择npm ci,但该命令一般多用于CI/CD环境。本地开发拉取依赖时不推荐该做法。
yarn在这方面做的就比npm好很多,完全按照 yarn.lock 文件记录的版本去安装对应依赖。
依赖树优化
- Yarn 的依赖树结构优化较好,通过更好的算法来处理依赖关系,减少版本冲突和重复安装的问题。
pnpm
全局存储(store)+ 硬链接 + 符号链接:
- 全局存储(内容寻址):pnpm 会把下载到的包文件放进全局的 store(按内容哈希组织)。多个项目/多个版本范围只要最终解析到同一份文件内容,就可以复用这份 store 文件,节省磁盘空间。
- 硬链接(或 reflink)复用文件:安装时,pnpm 通常不会把 store 里的文件“symlink 到项目里”,而是把 store 里的文件**硬链接(hardlink)**到项目的“虚拟仓库”目录(一般是
node_modules/.pnpm/)里;因此你在项目里看到的是真实文件(硬链接指向同一个 inode/内容),读取性能好、也更符合很多工具对“真实文件”的预期。 - 最外层用符号链接(symlink)拼装依赖图:项目最外层的
node_modules/<pkg>往往是一个 symlink,它指向node_modules/.pnpm/<pkg>@<version>/node_modules/<pkg>。pnpm 用这种方式把“包本体”放在.pnpm里,再用 symlink 在顶层把依赖关系“接出来”。
严格的模块隔离
- 模块隔离(更接近 Node 的真实解析规则):pnpm 的
node_modules/.pnpm/<pkg>@<version>/node_modules/下,只会放该包的直接依赖(以及它们继续向下的依赖)。这会让“没声明在 dependencies 里却能被 require/import 到”的情况更容易暴露出来。 - 减少幽灵依赖与版本冲突:因为依赖不会被无差别提升(hoist)到顶层,项目里不太容易出现“某个包意外用到了别的包的依赖版本”的幽灵依赖问题;同一个依赖的不同版本也能更清晰地共存。
总结
对于大型项目和复杂依赖,还是推荐使用pnpm和yarn进行管理。尤其是pnpm,不仅仅在规避依赖冲突上有着很大的优势,在节省磁盘空间、依赖安装速度、天然支持Monorepo等方面都有得天独厚的优势。