如果你试图在 Webpack 5 的官方迁移指南里搜索 Colliding Universe,大概率会空手而归。这个词并不是某个内置插件的代号,也不是官方 API 的命名,而是开发者社区对 Module Federation 带来的多构建共享现象的一种形象称呼。所谓碰撞宇宙,可以理解为两个原本独立编译、独立部署的前端应用,在浏览器运行时突然发生了模块层面的交汇,就像两个平行的宇宙撞在一起,组件、状态、依赖可以互相渗透。这种能力在传统单体构建模式下几乎无法实现,因为它要求应用在构建阶段就确定所有依赖;而 Module Federation 通过远程入口文件,把依赖解析推迟到了运行时。接下来就从这个比喻出发,看看它背后真实的 Webpack 5 特性、配置方式以及生产环境中的注意事项。

Colliding Universe 对应的真实特性:Module Federation
要理解碰撞宇宙,先得承认一个事实:Webpack 5 的模块联邦并不是什么突然冒出来的黑科技,它解决的是微前端架构中一个非常具体的痛点。过去多个团队协作同一个应用时,通常有三种做法。第一种是 iframe 隔离,优点是天然独立,缺点是通信困难、样式和交互割裂;第二种是 single-spa 这类框架,它要求所有子应用按照统一规范注册生命周期,主应用需要预先知道子应用的存在;第三种是把公共组件发布成 npm 包,各团队升级版本后重新构建,这种方式的发布链路重、版本对齐成本高。模块联邦提供了第四种思路:远程应用的模块可以在运行时被主应用动态加载,主应用不需要重新编译,也不需要预先安装远程应用的依赖。
在模块联邦的语境中,通常会区分 host(宿主)和 remote(远程)。host 是负责消费远程模块的应用,remote 则是暴露模块供其他应用使用的应用。远程应用构建时会产出一个名为 remoteEntry.js 的文件,这个文件可以看作远程宇宙的入口坐标。host 应用通过配置 remotes 指向这个文件,就能在代码中动态 import 远程组件。两个构建产物在浏览器中交汇时,Webpack 5 运行时会负责协调模块的加载、共享依赖的复用以及代码分割的衔接。这就是碰撞宇宙这个比喻最贴切的地方:两个独立构建在运行时突然产生了模块级别的联系。
需要注意的是,模块联邦并不是简单地做异步加载。它本质上是构建工具在运行时共享模块映射关系。webpack 会在远程入口中留下一个模块容器,容器内部记录了该远程应用暴露的所有模块和共享依赖的版本信息。当 host 消费某个远程模块时,Webpack 运行时会先加载远程入口,再根据容器信息找到对应模块并执行。这个过程中还会处理共享依赖的复用逻辑,避免远程模块和宿主应用各自加载一份 React 导致不可预期的状态错误。
核心配置拆解与 shared 共享机制
模块联邦的配置入口是 ModuleFederationPlugin,它从 Webpack 5 开始被内置在 webpack/lib/container 路径下。一个典型的 host 配置如下:
const ModuleFederationPlugin = require('webpack/lib/container/ModuleFederationPlugin');
module.exports = {
plugins: [
new ModuleFederationPlugin({
name: 'hostApp',
remotes: {
remoteApp: 'remoteApp@http://localhost:3001/remoteEntry.js',
},
shared: {
react: { singleton: true, eager: true },
'react-dom': { singleton: true, eager: true },
},
}),
],
};
这里的 name 表示当前构建的名称,remotes 声明了远程应用及其入口地址。键 remoteApp 是远程应用的命名空间,后面的字符串由远程应用名和入口 URL 组成,中间用 @ 分隔。当 host 代码中出现 import('remoteApp/Button') 时,Webpack 运行时会根据这条配置找到对应的远程入口并加载。远程应用的配置同样使用这个插件,区别在于它声明的是 exposes 而不是 remotes。
const ModuleFederationPlugin = require('webpack/lib/container/ModuleFederationPlugin');
module.exports = {
plugins: [
new ModuleFederationPlugin({
name: 'remoteApp',
filename: 'remoteEntry.js',
exposes: {
'./Button': './src/components/Button',
},
shared: {
react: { singleton: true },
'react-dom': { singleton: true },
},
}),
],
};
exposes 对象决定了远程应用暴露哪些本地模块给外部使用。键是外部引用时的模块路径,值是本地文件的相对路径。例如 host 中可以使用 import('remoteApp/Button') 来加载远程应用的 src/components/Button 组件。filename 指定远程入口文件的名称,默认是 remoteEntry.js,生产环境中通常会配合 CDN 缓存策略使用。
配置里最值得关注的是 shared。如果远程应用和宿主应用都依赖 React,而彼此没有共享机制,浏览器中就会存在两份 React 实例。一旦远程组件内部使用 useState 这类 hooks,宿主应用里的 React 上下文就会错乱,直接抛出 Invalid hook call 错误。而 shared 可以让 Webpack 运行时在加载远程模块时,优先复用宿主应用已经加载的依赖。当双方都声明了同一个共享依赖且版本兼容时,Webpack 只会加载一次。如果远程应用打包时带了自己的 React,但宿主应用已经有 React,运行时就会丢弃远程自带的版本,使用宿主提供的实例。
共享依赖的行为可以通过几个参数精细控制。singleton 设为 true 表示该依赖在全局只允许存在一个实例;eager 为 true 时,共享依赖会被立即初始化,而不是等待异步模块加载;requiredVersion 可以指定版本范围。对于 React 这类对单例要求极高的库,通常会把 singleton 和 eager 同时打开,避免异步加载时出现版本协商失败导致的加载延迟或警告。
实战:从零构建一个跨应用按钮共享
为了更直观地展示碰撞宇宙的构建过程,我们准备两个独立的项目:remote-app 和 host-app。远程应用先创建一个简单的按钮组件:
// remote-app/src/components/Button.jsx
import React from 'react';
const Button = ({ label = 'Remote Button' }) => {
return (
<button className="remote-btn">
{label}
</button>
);
};
export default Button;
远程应用的 webpack 配置按照前面所示设置 name、filename 和 exposes。启动远程应用后,访问 http://localhost:3001/remoteEntry.js 应该能看到一个 JavaScript 文件,里面包含了模块容器的信息。这个入口文件并不包含按钮组件的业务逻辑,它更像一张地图,告诉 Webpack 运行时按钮组件的实际代码块在哪里。
宿主应用中的消费方式也很直接。在需要加载远程组件的地方,使用动态导入并配合 React 的 lazy 和 Suspense:
import React, { lazy, Suspense } from 'react';
const RemoteButton = lazy(() => import('remoteApp/Button'));
const App = () => {
return (
<div>
<h2>Host Application</h2>
<Suspense fallback={<div>Loading remote button...</div>}>
<RemoteButton label="From Host Universe" />
</Suspense>
</div>
);
};
export default App;
动态导入返回的是一个 Promise,Webpack 运行时会先解析 remoteApp 这个命名空间对应的远程入口,然后根据远程容器定位 Button 模块,最后执行模块代码。如果远程入口加载失败,比如远程应用尚未启动或网络异常,lazy 会抛出加载错误,可以在外层用 ErrorBoundary 捕获。这种错误处理在传统本地代码分割中并不常见,但在模块联邦场景中必须纳入设计。
运行宿主应用后打开浏览器控制台,你会看到宿主应用只加载了一份 React,远程按钮组件复用了这份 React 实例。如果此时修改远程按钮的样式或逻辑,重新构建并部署远程应用,宿主应用不需要重新编译就能在刷新后拿到最新的远程模块。这正是碰撞宇宙带来的核心价值:两个应用可以独立演进,却在运行时保持紧密的模块协作。
生产环境注意点与性能优化
模块联邦虽然灵活,但在生产环境中会引入一些额外的性能开销。最明显的一点是浏览器需要先加载远程入口文件,再根据入口信息去加载真正的远程模块代码块。这个链路比普通的本地异步加载多了一次请求。如果远程应用数量较多,首屏的加载瀑布会明显变长。为了缓解这个问题,可以在宿主应用中提前建立远程连接,例如在应用初始化时手动执行一次 import('remoteApp/Button'),让远程入口尽早开始下载。对于使用 HTTP/2 的场景,多个远程入口可以并行加载,影响会相对较小;但 HTTP/1.1 下仍建议控制远程应用数量。
共享依赖的版本协商是另一个容易出问题的地方。当远程应用和宿主应用对同一依赖声明的版本范围不兼容时,Webpack 运行时会尝试加载两份不同的版本。这会导致包体积增加,甚至出现运行时行为差异。因此团队之间需要对核心依赖的版本保持沟通,尽量统一大版本。对于像 react 这种必须单例的依赖,可以在构建时通过 shared 的 strictVersion 或 requiredVersion 强制校验,并在 CI 流程中加入版本一致性检查。
远程入口文件的缓存策略也需要特别设计。由于远程入口只是模块映射表,体积很小,可以给它设置较短的缓存时间,或者使用协商缓存。而真正包含业务代码的 chunk 文件可以使用长缓存,因为它们的内容哈希会随代码变化而改变。如果远程入口被 CDN 长期缓存,宿主应用将无法感知远程模块的新版本,导致更新延迟。常见做法是在远程入口 URL 后附加版本查询参数,例如 remoteApp@http://cdn.ipipp.com/remoteEntry.js?v=1.2.0,这样既能保持文件名稳定,又能精确控制版本。
最后需要强调的是,碰撞宇宙并不是银弹。它适合多个团队独立维护、但需要共享组件或页面的场景。如果团队规模较小,或者模块之间耦合度极高,使用传统的 npm 包发布或本地 monorepo 可能更简单。模块联邦的运行时开销、错误处理复杂度和跨团队协作成本,都需要纳入架构决策。理解了这些,再回头看 Colliding Universe 这个称呼,你会发现它不只是一个有趣的比喻,更是对模块联邦打破构建边界、让独立系统在运行时产生连接这一特性的精准概括。
Webpack 5模块联邦Colliding Universe修改时间:2026-10-05 14:00:12