在微前端和模块联邦架构中,某个基础库被多个远程应用重复打包几乎是常态。比如页面同时加载了三个不同的 React 副本,Hooks 内部状态隔离就会导致 TypeError 或数据不同步。Webpack 5 引入的 Balanced Universe 平衡宇宙机制并不是一个独立插件,而是模块联邦共享依赖协商策略的统称。它让宿主与远程应用在运行时像处于同一个引力场中一样,对共享模块的版本、实例和加载时机进行平衡。

一、Balanced Universe 试图解决什么:从多副本冲突谈起
在一个典型的模块联邦项目里,宿主应用 appShell 可能同时加载 catalog 和 checkout 两个远程应用。如果三个构建单元都没有配置共享依赖,那么每个 remoteEntry 都会携带自己的 React 与 ReactDOM。三份 React 同时进入页面,意味着 useEffect 的闭包可能来自不同模块实例,useContext 的 Provider 与 Consumer 也无法匹配。更隐蔽的问题是,某些全局状态库会在 window 上注册事件,不同副本之间互相覆盖,调试成本非常高。
Balanced Universe 的核心在于将这类基础依赖提升为共享契约。开发者在 webpack 容器的 shared 配置中声明依赖名称以及版本要求,容器运行时会把这些声明汇总,优先加载一个满足所有消费者的单例版本。只有版本范围确实无法兼容时,才会回退到多副本隔离。这个协商过程不是构建期写死的,而是在浏览器加载 remoteEntry 之后动态完成。
可以把整个页面理解成一个宇宙,每个远程应用是宇宙中的星体。它们各自携带的依赖版本如果没有约束,就会产生引力混乱。而 Balanced Universe 通过 shared 配置建立统一的版本轨道,让大多数基础库保持同步运行。对于确实无法同步的依赖,它也能自动隔离,避免影响其他星体。
二、shared 配置实战:singleton、requiredVersion 与 eager
要启用平衡宇宙,必须在模块联邦插件中显式声明 shared。下面是一个包含远程应用和共享依赖的 webpack 配置示例:
const { ModuleFederationPlugin } = require('webpack').container;
module.exports = {
plugins: [
new ModuleFederationPlugin({
name: 'appShell',
remotes: {
catalog: 'catalog@http://localhost:3001/remoteEntry.js',
checkout: 'checkout@http://localhost:3002/remoteEntry.js'
},
shared: {
react: {
singleton: true,
requiredVersion: '^18.0.0',
eager: true
},
'react-dom': {
singleton: true,
requiredVersion: '^18.0.0',
eager: true
},
lodash: {
singleton: false,
version: '4.17.21'
}
}
})
]
};shared 中的每个键代表一个依赖。singleton 为 true 时,共享作用域只允许存在一个实例,适合 react、react-dom 这类对单例要求极高的库。requiredVersion 使用 semver 范围,表示当前应用需要哪个版本区间。eager 为 true 时,该共享依赖会随着容器启动立即加载,通常用于宿主为远程应用提供全局单例的场景。false 则交由远程应用按需加载。
最佳实践是不要对所有依赖设置 singleton。react 和 react-dom 必须单例,否则 Hooks 会直接报错。而像 lodash、axios 这类工具库可以保留多副本,甚至不写 requiredVersion,只通过 version 表示兼容基线。这样既能减少协商冲突,也能保留按需加载和 tree shaking 的优化空间。
另一个容易忽略的细节是版本范围写法。requiredVersion 不是固定版本号,而是 semver 表达式。若宿主写 ^18.0.0,远程应用写 ~18.2.0,两者可以合并为满足交集的最新版本。如果远程应用写 ^17.0.0,则无法合并,运行时只能回退到多副本隔离。
三、运行时回退与误区:平衡宇宙不是万能保险
Balanced Universe 的回退逻辑非常清晰,但它也有边界。当两个应用分别要求 ^17.0.0 和 ^18.0.0 时,没有任何一个 React 版本可以同时满足两边的范围,平衡宇宙会选择不共享,每个应用继续加载自己的版本。这时页面仍然会出现多份 React,只是这种多副本是由版本范围冲突造成的,而不是配置遗漏。
加载顺序同样会影响最终选定的共享版本。如果远程应用先注册了 17.x 的 React,而宿主随后要求 18.x,由于单例已经被占用,宿主要么等待一个兼容版本,要么产生回退副本。实际项目中,可以通过把宿主作为主控制器并设置 eager: true 来稳定版本,避免远程应用的加载顺序干扰整体依赖图。
常见误区有三个。第一,认为 singleton: true 一定可以消除重复实例,但如果 requiredVersion 冲突,硬性单例反而会让某个应用无法启动。第二,给所有共享依赖都加 eager: true,导致首屏下载体积明显增加,破坏按需加载。第三,忽略 shareScope 的作用域隔离,如果不同容器使用了不同的 shareScope,即便依赖名称和版本都一致,运行时也不会复用实例。
可以通过浏览器控制台查看当前共享作用域,验证实际生效的依赖版本:
// 在浏览器控制台中查看当前共享作用域
const scopes = window.__webpack_share_scopes__;
if (scopes) {
Object.keys(scopes.default || {}).forEach(key => {
console.log(key, scopes.default[key]);
});
}这段代码会打印 default 共享作用域下每个依赖的注册信息,包括 get 函数、from 来源和是否 eager。开发者可以据此快速确认运行时到底加载了哪个版本,以及是否存在多副本回退。掌握这个调试入口,排查共享依赖冲突会高效很多。
Balanced Universe 平衡宇宙的价值不在于绝对消灭多副本,而在于提供一套可预期、可配置、可观测的共享机制。它让微前端架构下的依赖治理从各自为政走向统一协调,既保留了模块联邦的灵活性,又避免了基础库多实例引发的隐性故障。