Webpack 5 引入的 Module Federation(模块联邦)常被看作一种让多个构建产物在运行时互相合作的能力,也就是官方提到的 cooperation 场景。如果你的前端系统由多个独立部署的子应用组成,传统做法要么用 iframe 隔离,要么把公共组件发布成 npm 包再各自升级,而 Module Federation 提供了第三条路:应用 A 在构建时暴露自己的一部分模块,应用 B 在运行时直接加载并使用这些模块,双方互不干扰,又能共享同一份依赖实例。

这种合作不是简单的代码复制,而是把远程入口文件作为桥梁,在浏览器里动态建立模块关系。下面从核心概念、配置实现、共享依赖和动态加载几个角度展开。
一、Module Federation 的核心概念与解决的问题
在 Module Federation 出现之前,前端微应用之间的模块复用通常有三种方式。第一种是 iframe 嵌套,它能提供最强的隔离性,但父子页面通信需要借助 postMessage,样式和弹窗处理也很麻烦。第二种是把公共组件发布到 npm 仓库,各子应用分别安装,但这要求任何组件更新都要重新发布并让所有消费者升级依赖、重新构建,部署链路很长。第三种是使用 single-spa 之类的微前端框架统一注册应用,虽然解决了加载问题,但模块之间的依赖共享仍然不够灵活。
Module Federation 的思路是把每个独立的 Webpack 构建产物看作一个容器,这个容器可以选择暴露部分模块给外部使用,也可以在运行时消费其他容器暴露出来的模块。暴露方不需要把代码发布到 npm,消费方也不需要提前安装依赖,只需要在构建配置中声明远程地址,剩下的加载和依赖协调工作由 Webpack 在浏览器里完成。这种合作机制特别适合多团队并行开发的场景:每个团队独立开发、独立部署,只在运行时按需共享组件或工具函数。
要实现这种合作,需要理解四个核心字段。name 表示当前容器的唯一标识;filename 指定远程入口文件的文件名,通常命名为 remoteEntry.js;exposes 定义当前应用对外暴露哪些模块;remotes 声明当前应用需要消费哪些远程容器。还有一个 shared 字段用来声明共享依赖,后文会详细说明。
二、从零配置远程模块与宿主应用
先看远程应用(remote)的配置。假设远程应用运行在 3001 端口,它希望把一个按钮组件暴露出去给其他应用使用。Webpack 配置如下:
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: {
'./Button': './src/components/Button'
},
shared: {
react: { singleton: true },
'react-dom': { singleton: true }
}
}),
new HtmlWebpackPlugin({ template: './public/index.html' })
]
};
这里的 exposes 对象把本地路径 ./src/components/Button 映射为一个公开路径 ./Button。远程应用构建后,会在输出目录生成 remoteEntry.js,这个文件记录了模块的加载映射关系,宿主应用就是通过它来发现远程模块。
接下来配置宿主应用(host)。宿主应用运行在 3000 端口,它希望加载远程应用暴露的按钮组件。对应的 Webpack 配置如下:
const HtmlWebpackPlugin = require('html-webpack-plugin');
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 }
}
}),
new HtmlWebpackPlugin({ template: './public/index.html' })
]
};
在 remotes 字段中,键 remoteApp 是宿主应用内部使用的命名空间,值由三部分组成:远程容器的 name、@ 符号和远程入口文件的完整 URL。配置完成后,宿主应用代码里就可以通过 import('remoteApp/Button') 动态加载远程按钮组件。Webpack 会在构建时保留这个动态导入,在运行时根据 remotes 配置解析出真实地址并加载脚本。
这种配置方式的最大好处是解耦。远程组件改变内部实现时,只要保持暴露路径不变,宿主应用无需任何改动,也不需要重新安装依赖或重新构建。远程应用可以随时独立发布,宿主应用下次刷新页面就会拿到最新版本的远程模块。
三、共享依赖与版本冲突处理
如果宿主应用和远程应用都使用 React,但各自打包了一份 React 实例,运行时就可能出现两个不同的 React 副本。这会导致 hooks 状态异常、上下文丢失等问题。Module Federation 通过 shared 字段解决这个问题:把公共依赖声明为共享模块,Webpack 会协调各容器尽量复用同一份实例。
下面是一个更完整的共享依赖配置示例:
shared: {
react: {
singleton: true,
requiredVersion: '^18.2.0'
},
'react-dom': {
singleton: true,
requiredVersion: '^18.2.0'
},
lodash: {
singleton: false
}
}
singleton: true 表示整个运行时只允许存在一个 React 实例。如果多个容器声明的版本范围能够兼容,Webpack 会加载一个共享实例;如果版本不兼容,Webpack 会根据策略回退到各自打包的版本,并在控制台输出警告。requiredVersion 用来声明当前容器期望的版本范围,Webpack 会根据所有容器的声明计算一个可满足的版本。对于 lodash 这类工具库,如果不需要严格单例,可以设置 singleton: false,允许不同版本共存。
还有一个 eager 选项值得注意。当宿主应用的入口代码同步依赖了某个共享模块时,如果该模块被配置为异步加载,Webpack 可能无法在入口执行前完成远程模块的初始化。此时可以设置 eager: true 让该共享模块立即初始化,但这会增加首屏加载体积。一般来说,入口文件里同步使用的依赖才需要开启 eager,异步组件内部使用的依赖保持默认即可。
四、动态远程加载与常见避坑点
远程模块通常通过动态导入来加载,这样可以把加载时机推迟到真正需要的时候。以 React 为例,可以在宿主应用中使用 React.lazy 配合 Suspense 来加载远程按钮:
import React from 'react';
const RemoteButton = React.lazy(() => import('remoteApp/Button'));
export default function App() {
return React.createElement(
'div',
null,
React.createElement(
React.Suspense,
{ fallback: React.createElement('div', null, 'Loading remote button...') },
React.createElement(RemoteButton, null)
)
);
}
这段代码没有使用 JSX,而是用 React.createElement 来构造界面,这样可以避免在构建演示代码时引入额外的转换配置。实际项目中可以使用 JSX,只要确保远程模块路径正确即可。动态导入会触发 Webpack 的远程加载逻辑:先请求远程入口文件,解析模块映射,再请求对应的 chunk 文件,最后返回模块导出内容。
实际使用中还有几个容易踩的坑。第一是远程入口地址必须保证在任何部署环境下都可访问,建议使用绝对 URL,并通过环境变量控制协议和域名。第二是远程应用的 publicPath 要配置正确,否则远程入口内部加载 chunk 时会拼接出错误的资源地址。第三是共享依赖如果配置不当,可能会出现 React 实例不唯一的报错,此时需要检查各容器的 shared 字段是否一致。第四是 CSS 样式没有自动隔离,远程组件引入的样式会直接注入到宿主页面,如果双方类名冲突需要自行处理命名空间或使用 CSS Modules。
总的来说,Module Federation 把模块共享从构建期提前到了运行期,让多个独立应用可以像调用本地模块一样调用远程模块。这种合作机制不仅适用于微前端架构,也可以用于组件市场、插件系统等需要动态扩展的场景。理解远程入口、模块暴露和共享依赖三个核心点,就能比较顺利地搭建起一个稳定的跨应用模块合作体系。

