什么是 Warring Universe 战争宇宙
Webpack 5 官方发布时提出了一些标志性概念,其中 Warring Universe 战争宇宙是一个形象化的比喻。它描述的是这样一个场景:过去多个团队各自构建自己的前端应用,每个应用都是一座孤岛,构建产物之间无法直接共享代码。如果想在一个页面中同时运行两个独立部署的应用,往往需要iframe嵌套、加载器 hack 或者把公共代码重复打包多份。战争宇宙的含义就是多个应用像多个宇宙一样彼此独立运行,同时又能在运行时互通有无。
这个概念落地的技术载体就是 Module Federation 模块联邦。模块联邦允许一个 Webpack 构建产物在运行时动态加载另一个独立构建产物的模块,并且双方可以协商共享依赖。换句话说,应用 A 可以直接 import 应用 B 暴露出来的组件,而无需把 B 的源码打进 A 的构建流程。这在 Webpack 4 时代几乎是不可能优雅实现的事情。
理解战争宇宙的关键在于两点:一是构建期的独立性,每个应用仍然用自己的仓库、自己的构建流水线、自己的发布节奏;二是运行期的互联性,模块在浏览器端按需拉取、按需实例化。这两点结合,让前端工程从单体巨石架构自然演进出多团队协作的形态。
模块联邦的核心配置与工作原理
模块联邦涉及两个角色:提供方 Host 暴露模块供别人使用,消费方 Remote 引用别人的模块,一个应用可以同时扮演两种角色。下面是一个典型的提供方配置:
// webpack.config.js —— 应用A作为提供方
const { ModuleFederationPlugin } = require('webpack').container;
module.exports = {
plugins: [
new ModuleFederationPlugin({
name: 'appA',
filename: 'remoteEntry.js',
exposes: {
'./Button': './src/components/Button.vue',
'./utils': './src/utils/index.js',
},
shared: {
vue: { singleton: true },
axios: { singleton: true },
},
}),
],
};
提供方通过 exposes 字段声明对外暴露的模块路径,构建时会生成一个 remoteEntry.js 入口文件。这个文件本质上是一个运行时容器,里面记录了暴露模块的映射关系和加载逻辑。消费方拿到这个文件的地址后,就可以在浏览器里按需请求具体模块。
再看消费方的配置方式:
// webpack.config.js —— 应用B作为消费方
new ModuleFederationPlugin({
name: 'appB',
remotes: {
appA: 'appA@https://cdn.ipipp.com/remoteEntry.js',
},
shared: {
vue: { singleton: true },
axios: { singleton: true },
},
});
配置完成后,应用B的业务代码里可以直接这样引用应用A的组件:
// 异步加载远程模块
import('appA/Button').then((Module) => {
const RemoteButton = Module.default;
// 像使用本地组件一样渲染
});
背后的原理可以拆成三步。第一步是协商共享:构建时双方都在产物中记录自己共享了哪些依赖、需要哪些依赖,以及各自依赖的版本范围。第二步是运行时仲裁:页面加载时联邦运行时会先检查共享模块的注册情况,如果宿主环境已经加载了满足版本要求的实例就直接复用,否则按需加载。第三步是异步边界处理:由于远程模块的加载是异步的,Webpack 会要求入口文件使用异步 chunk 的方式引入,这也是官方示例中 bootstrap.js 模式的原因。
共享依赖与版本仲裁的细节陷阱
shared 配置是模块联邦里最容易踩坑的部分。当多个应用共享同一个库时,Webpack 需要决定浏览器里到底用哪一个副本。默认情况下,shared 中的依赖只有满足语义化版本范围才会被复用,否则会加载自己的副本。如果把这个开关开得太宽,可能加载到不兼容的版本;如果太严格,又会导致同一个库被重复下载,共享就失去了意义。
其中 singleton 选项值得重点说明。对于 Vue、React 这类持有全局状态的框架,页面上绝对不能同时存在两份实例,否则会出现组件分属两个实例、响应式系统互不相通的问题。设置 singleton: true 可以强制全局只保留一份,但要意识到这样做的代价是消费方必须接受提供方的版本,哪怕自己的 package.json 里声明的是另一个版本。官方还提供了 strictVersion 选项,用于在版本不满足时直接抛错而不是静默降级。
另一个常见问题是异步加载边界。模块联邦要求每个参与方的入口都做成异步初始化,通常的写法是拆出一个 bootstrap.js:
// src/index.js —— 异步入口
import('./bootstrap');
// src/bootstrap.js —— 真正的应用启动逻辑
import { createApp } from 'vue';
import App from './App.vue';
createApp(App).mount('#app');
这个结构看起来多余,实际上是必须的。因为 index.js 需要变成异步 chunk,联邦运行时才有机会在应用启动前完成共享模块的协商和远程容器的初始化。如果所有逻辑都写在同步入口里,就会出现远程模块尚未就绪应用已经执行完的时序错误。
与传统微前端方案及微前端框架的对比
提到多应用集成,绕不开与 qiankun 等微前端框架的比较。qiankun 基于 single-spa,通过劫持 window 事件、代理沙箱隔离 JS 运行环境,应用之间以生命周期的方式挂载卸载。这种方式隔离性强,老项目改造成本相对可控,但沙箱代理带来的性能损耗和全局对象污染问题是绕不开的痛点。
模块联邦走的是另一条路:它不做沙箱隔离,而是让应用直接共享同一个 JS 运行环境,天然要求团队之间有更强的契约意识。它的优势是性能开销极小、依赖共享彻底、接入方式就是标准的 import 语法,心智负担低;劣势是没有运行时隔离,样式冲突和全局变量污染需要自己通过命名空间、CSS Modules 等手段解决。
选型建议:如果是从零搭建的多团队中台,模块联邦更轻快;如果是大量历史遗留系统的渐进式整合,qiankun 的沙箱隔离反而更稳妥。两者并不互斥,部分大型项目也会同时使用。
此外,模块联邦不绑定框架。Vue、React、Angular 甚至是纯 JS 模块都可以通过联邦互通,只要处理好共享依赖的版本问题。这让它比很多特定于某框架的微前端方案具有更广的适用面。
生产环境落地的实践建议
在生产环境使用模块联邦,remoteEntry.js 的地址管理是第一个要解决的问题。通常做法是把各应用的远程入口地址收敛到一份配置中,由配置中心或 CDN 上的 manifest 文件统一维护,消费方启动时先拉取这份清单再初始化联邦,这样提供方发布新版本时消费方无需重新构建。
其次是容错设计。远程模块本质上是一次跨域的网络请求,必须考虑加载失败的情况。建议对远程模块的 import 统一封装一层高阶组件或工具函数,失败时降级渲染占位内容并上报监控,避免一个子应用挂掉拖垮整个页面。
最后是版本契约治理。共享依赖建议通过 package.json 的依赖声明自动生成版本范围,而不是在 shared 里写死字符串。团队之间暴露的模块要有类似 API 的文档约定,模块签名变更要走通知流程。战争宇宙听起来自由,但真正的稳定运转依赖的是清晰的边界和契约,这也是这套机制在工程管理层面给出的启示。
Webpack 5Warring Universe模块联邦修改时间:2026-09-06 17:07:19