把多个独立部署的前端应用拆成独立构建单元,同时让它们像在同一个代码库里一样共享组件,这种能力来自 Webpack 5 内置的模块联邦(Module Federation)。它常被拿来和“分割宇宙”类比,因为每个应用保持自己的打包边界、版本策略和发布节奏,又可以建立点对点的远程模块加载通道。下文将拆解它的配置模型、加载过程和避坑思路。

“分割宇宙”到底解决什么问题
传统前端项目通常围绕一个构建上下文展开:入口文件、依赖图、打包产物、部署版本都是统一规划的。随着团队规模扩大,这种单体式构建会带来部署耦合、技术栈锁死和构建时间膨胀等问题。模块联邦把每个子应用设计成独立构建、独立版本、独立部署的单元,这就像把一个大宇宙拆成多个小宇宙,每个宇宙有自己的物理规则和生命周期。
在模块联邦出现前,团队一般通过公共依赖包、全局变量或微前端容器来实现跨应用复用。但这些方式要么需要提前协调版本,要么在运行时引入额外容器层,反而增加了维护成本。Webpack 5 的 ModuleFederationPlugin 直接在 webpack 层面提供远程入口,应用 A 可以把某个模块暴露出去,应用 B 只需要声明远程地址,就能像 import 本地模块一样 import 远程模块。拆分的粒度由团队决定,可以是一个组件、一个工具函数、一个页面级模块,甚至是整个路由。
这种“分割宇宙”的思路并不意味着应用之间完全隔离。相反,模块联邦通过 shared 配置在宇宙之间建立共享依赖协商机制。React、Vue、lodash 等公共库不会在每个远程包里重复打包,而是在运行时根据版本要求协商加载,避免多个实例导致的 Hook 失效、全局状态不一致等问题。理解这一点,是配置稳定模块联邦架构的关键。
模块联邦核心配置与远程加载流程
ModuleFederationPlugin 的配置项主要分为两类。远程应用使用 name、filename 和 exposes 声明自己的身份和可共享模块,主应用使用 remotes 声明远程入口地址,再通过动态 import 加载远程模块。下面的远程应用配置暴露了一个 Counter 组件:
const HtmlWebpackPlugin = require('html-webpack-plugin');
const { ModuleFederationPlugin } = require('webpack').container;
module.exports = {
entry: './src/index.js',
mode: 'development',
devServer: { port: 3001 },
plugins: [
new ModuleFederationPlugin({
name: 'remoteApp',
filename: 'remoteEntry.js',
exposes: {
'./Counter': './src/components/Counter',
},
shared: {
react: { singleton: true },
'react-dom': { singleton: true },
},
}),
new HtmlWebpackPlugin({
template: './public/index.html',
}),
],
};
远程应用的 filename 指定了远程入口文件的名称,默认是 remoteEntry.js。这个文件在构建时会生成,包含模块映射、共享依赖信息和异步加载逻辑。主应用需要配置 remotes,键名就是远程应用的 name,值由远程名称和入口 URL 组成。下面的主应用配置指向本机 3001 端口的远程入口:
const { ModuleFederationPlugin } = require('webpack').container;
module.exports = {
entry: './src/index.js',
mode: 'development',
devServer: { port: 3000 },
plugins: [
new ModuleFederationPlugin({
name: 'hostApp',
remotes: {
remoteApp: 'remoteApp@http://localhost:3001/remoteEntry.js',
},
shared: {
react: { singleton: true },
'react-dom': { singleton: true },
},
}),
],
};
配置完成后,主应用可以通过动态 import 加载远程模块。动态 import 会触发 webpack 的远程模块运行时,先加载 remoteEntry.js,再根据模块 ID 获取实际代码。由于远程模块是异步获取的,页面中需要使用 Suspense 或加载态处理延迟:
import React, { lazy, Suspense } from 'react';
const RemoteCounter = lazy(() => import('remoteApp/Counter'));
export default function App() {
return (
<Suspense fallback={<span>Loading...</span>}>
<RemoteCounter />
</Suspense>
);
}
这里的加载流程可以概括为:浏览器解析主应用入口,遇到动态 import('remoteApp/Counter'),webpack 运行环境检测到 remoteApp 是远程模块,然后加载远程入口文件,查询暴露模块 './Counter',执行远程模块并返回。整个链路在浏览器运行时完成,不依赖构建时的合并。shared 中的 react 和 react-dom 被标记为 singleton,意味着主应用和远程应用共用同一个 React 实例。如果远端没有可用的共享版本,主应用会回退到自己的依赖,或者在 requiredVersion 不匹配时给出警告。
共享依赖冲突与常见陷阱
模块联邦最容易踩的坑集中在 shared 配置。假设主应用和远程应用都依赖 React,但没有设置 singleton,webpack 可能会加载两份 React 副本。函数组件本身可能工作正常,但一旦使用 useContext、useState 之外的全局特性,或者组件依赖 React 内部状态,就可能出现无法共享上下文、Hook 顺序错乱等问题。修复方式是把 react 和 react-dom 都设置为 singleton,并在 requiredVersion 中声明兼容版本:
shared: {
react: {
singleton: true,
requiredVersion: '^18.2.0',
eager: false,
},
'react-dom': {
singleton: true,
requiredVersion: '^18.2.0',
},
}
第二个常见问题是远程入口路径配置错误。开发环境中远程入口地址通常是 http://localhost:3001/remoteEntry.js,部署到生产时需要替换成实际 CDN 或静态资源地址。团队可以把远程地址放在环境变量中,在 webpack 配置里通过函数动态生成。不要硬编码 localhost,否则构建产物会在测试、预发、生产环境出现加载失败。
第三个容易忽略的点是样式隔离。模块联邦只共享 JavaScript 模块,不会自动处理 CSS 作用域。远程组件加载后,样式会直接插入主应用的 head,可能污染全局类名。实践中最好使用 CSS Modules、CSS-in-JS 或 BEM 命名规范,让远程组件样式本身具备局部作用域。对于一些老组件库,也可以通过 postcss 前缀插件在构建阶段隔离。
另一个需要提前规划的是应用间通信。模块联邦不提供通信规范,远程组件与主应用之间的数据传递、事件订阅仍要依赖 props、回调、自定义事件或全局状态管理。把远程组件理解成普通的异步组件,通过 props 传参、通过回调返回结果,可以避免过度耦合。跨应用状态同步可以借助浏览器自带的 CustomEvent 或 shared 中暴露的轻量 store,但不建议把整套 Redux 实例跨应用共享。
模块联邦与常见微前端方案对比
微前端领域早期主要使用 <iframe> 实现隔离。内嵌框架能提供天然的 JavaScript 和 CSS 隔离,但通信成本高、路由同步复杂、弹窗遮罩等问题多,体验较差。single-spa 通过注册子应用并监听路由变化来挂载和卸载应用,解决了路由编排问题,但仍需要统一的容器和加载入口。qiankun 基于 single-spa 做了更多开箱能力,包括 JS 沙箱和样式隔离,适合已有系统改造。
模块联邦与这些方案最大的区别在于,它不要求统一的基座应用和中心化注册表。每个远程应用保持独立构建和部署,共享的是代码模块而不是运行时实例。这降低了基础设施要求,但同时也把版本管理和运行态冲突的复杂度转移到了每个应用的 webpack 配置上。团队需要在构建粒度、依赖版本策略、远程入口管理和监控上投入更多精力。
下面是一个简单的对比表:
| 方案 | 隔离性 | 通信成本 | 构建集成要求 |
|---|---|---|---|
| <iframe> | 强 | 高 | 低 |
| single-spa | 中 | 中 | 中 |
| 模块联邦 | 弱到中 | 低 | 高 |
从这个对比可以看出,模块联邦更适合多个团队技术栈相近、组件复用频繁、并且已经统一使用 Webpack 5 的场景。如果子应用技术栈差异巨大,或者对样式和 JS 沙箱有强隔离要求,仍然需要结合其他方案。不少团队会把模块联邦用在组件共享层,而不是完整页面级微前端,这种混合架构反而更容易落地。