-
Notifications
You must be signed in to change notification settings - Fork 19
Description
我们业务在 CI 中接入了 [statoscope] 插件,帮助我们分析构建产物的内容,通过 statoscope 插件,发现了构建产物中引入了一些重复的npm包
我们将内容进行简化归类得到下面 3 种重复包的情况
1. package-a 有两个不同的版本
2. package-a 有两个相同的版本,但是链接符后面的包版本不一致
node_modules/.pnpm/@pkg-a@1.93.0_@pkg-b@0.2.2_@pkg-c@1.12.0_@ten_el4uhwub3qermsb2ug24hbwt4q/node_modules/pkg-a
node_modules/.pnpm/pkg-a@1.93.0_@pkg-b@0.3.0_@pkg-c@1.12.0__@ten_l3pmpxjfhmbdedp66axwibu23u/node_modules/@pkg-a
3. package-a 有两个相同的版本,连接符后面的包版本也是相同的
node_modules/.pnpm/@pkg-a@4.0.63_@pkg-b@3.13.0_@tenc_wzqvgos5wvtse35fehnxnhj2da/node_modules/@pkg-a
node_modules/.pnpm/@pkg-a@4.0.63_@pkg-b@3.13.0_@tenc_7zuxjpgexhn6u5ehnylczjhybe/node_modules/@pkg-a
问题分析
1. 两个不同版本的包
package-a 有两个不同的版本
这种情况大部分原因是因为依赖包的版本写死了,导致package-a的版本无法满足更高版本的version,导致安装了两个不同版本,这种问题比较常见于 dependencies 中,如果不是指定了某一个确认版本,那么建议在安装依赖时使用 ^ 来表达,当依赖的是1.0.0 版本以下的包时,^也是不会升级高版本的
2. 有两个相同的版本,但是连接符后面的包版本不一致
.pnpm目录下的目录生成规则是 package-a@版本_(package-a-peer)@版本
所以后面的连接符代表的是当前包的 peer 依赖版本,因为 peer 的版本不一致,所以 pnpm 认为当前 package-a 也应该是两个包,聊到这里,就需要讲到 peer transitive 的概念了,具体可以看下面这篇文档
pkg-a
dep:
- pkg-b
- lodash@1.0
pkg-b:
dep: pkg-c@1.0
pkg-c:
peer: lodash
由于 peer transitive,所以 pkg-c 最终安装的版本是
pkg-c@1.0_lodash@1.0
所以当我们项目下有两个相同版本不同peer 版本的实例时,就说明
pkg-a 中声明的peer包版本有无法兼容的包版本实例,因此因为 peer 版本的不同,所以导致 pkg-a也有了多个版本
3. 有两个相同的版本,连接符后面的版本也一致
这个也是 peer 引发的问题中最隐蔽的
pnpm ls pkg --depth Infinity
我们通过上述命令查看当前包的依赖关系,在 .pnpm 目录下找到两个版本包对应的引用,真相终于浮出水面

因为依赖关系比较绕,我们直接用真实环境包名的缩写来描述了
当我们项目下安装了 package-a和utils两个包时,我们最终安装了两个相同版本的 utils实例,我们来看看这里的依赖关系
package-a
dep:
- landuage-pack
- utils
- i18n@20
language-pack
dep: i18n@21
peer: i18n
utils
dep: language-pack
首先 language-pack 包中声明了 i18n为 peer dep,
所以package-a 依赖的 language-pack是 language-pack+i18n@21 的版本
而 package-a中的 utils 包也依赖了 language-pack, 但是由于 peer transitive,
所以 package-a中的 utils 包依赖的language-pack是 language-pack+i18n@20 的版本
到这里为止由于 peer的依赖版本不同,所以导致了 language-pack包是两份实例,
那么最关键的来了,我们根目录下也依赖了 utils 包,而这个utils包依赖的 language-pack是 language-pack+i18n@21
而package-a中的utils包依赖的是language-pack+i18n@20 的版本
所以,即使utils包的版本是同一个,但是由于他们的 dep language-pack的不同,所以也导致了 utils 的不同
这就是这个问题的原因
总结
看到这里,上面的多实例问题也全部都找到了原因,很多包希望将另一个包 b 作为单例的方式引入,但同时又依赖它,所以就想通过声明为了 peer,使用 peer 来保证 npm 包的单例,但这本身就是 hack 的一种方式,因为你必须要保证所有依赖了你这个包的任意包,都将包 b声明为 peer,这样你才能使用和真正的宿主相同的版本,否则就会被提前拦截,导致一些非预期内的行为,不仅希望的package-b不是单例,有可能包本身也会被打包成两份
那究竟什么时候才能使用 peer 呢🤔?它是用来解决什么问题的,我们看看开源框架是怎么应用的


可以看到,比如 webpack-dev-middleware 是 webpack 的中间件,所以必须在 webpack 中使用
当把一个依赖声明为 peer 时,它代表着表示当前这个包的运行环境,而并非依赖,依赖应该声明在 dep 中
综上,当我们考虑发布一个npm包时,需要更加谨慎的使用 peer,因为它不仅会使我们的构建体积增大,也会因为两个实例给我们的代码埋下隐患