Webpack 5 的模块联邦机制从构建层面解决了微前端架构中远程模块加载和依赖共享的两大核心问题。在模块联邦出现之前,开发者通常需要借助 single-spa 或 qiankun 等框架,将不同子应用打包成独立的脚本,再通过主应用统一调度。这种做法虽然可行,但各子应用之间的组件复用、状态共享和依赖版本控制往往需要额外的胶水代码。Webpack 5 原生提供的 ModuleFederationPlugin 让这些能力成为构建配置的一部分,极大地简化了微前端的落地成本。

一、模块联邦如何打破构建边界
传统 Webpack 构建会把所有依赖打进当前应用的产物中,每个应用都是一个相对封闭的 bundle。这种方式在单体应用中没有问题,但到了微前端场景,多个子应用运行在同一个页面时,如果每个子应用都打包了一份 React 或 Vue,不仅会造成资源浪费,还可能导致运行时出现多个实例,破坏上下文依赖。模块联邦的出现正是为了打破这种构建边界。
模块联邦允许一个应用在运行时动态加载另一个应用暴露出来的模块,而这些模块可以像本地模块一样被引用,尽管它们来自不同的构建产物。具体来说,每个应用可以通过 ModuleFederationPlugin 声明自己暴露哪些模块(exposes),同时声明自己需要从哪些远程应用加载模块(remotes),以及哪些依赖需要共享(shared)。这种机制让代码共享从构建期提前到了运行期,并且保留了各自独立构建和部署的能力。
下面是一个典型的远程应用配置片段,它将一个按钮组件暴露给其他应用使用:
const ModuleFederationPlugin = require('webpack/lib/container/ModuleFederationPlugin');
module.exports = {
entry: './src/index',
mode: 'development',
devServer: {
port: 3001,
},
plugins: [
new ModuleFederationPlugin({
name: 'remoteApp',
filename: 'remoteEntry.js',
exposes: {
'./Button': './src/components/Button',
},
shared: {
react: { singleton: true, requiredVersion: '^17.0.0' },
'react-dom': { singleton: true },
},
}),
],
};
在这个配置中,name 是远程应用的唯一标识,filename 是远程入口文件的名称,它会被宿主应用在运行时加载。exposes 指定了对外暴露的模块路径,宿主应用可以通过 import('remoteApp/Button') 的方式来异步加载这个组件。shared 则声明了哪些依赖应该被共享,singleton 为 true 表示整个页面只允许存在一个 React 实例,避免版本冲突。
二、从零搭建一个微前端示例
为了更直观地理解模块联邦的工作方式,我们可以搭建一个最小示例:两个独立的应用,一个是远程应用 remote-app,另一个是宿主应用 host-app。远程应用负责暴露一个简单的按钮组件,宿主应用在运行时加载这个组件并渲染到页面上。
远程应用的目录结构如下:
remote-app/ ├── src/ │ ├── components/ │ │ └── Button.jsx │ └── index.js ├── webpack.config.js └── package.json
在远程应用中,Button 组件可以是一个简单的 React 组件:
// src/components/Button.jsx
import React from 'react';
const Button = ({ label }) => {
return <button>{label}</button>;
};
export default Button;
宿主应用的 webpack 配置则需要声明 remotes,指向远程应用的入口文件地址:
const ModuleFederationPlugin = require('webpack/lib/container/ModuleFederationPlugin');
module.exports = {
entry: './src/index',
mode: 'development',
devServer: {
port: 3000,
},
plugins: [
new ModuleFederationPlugin({
name: 'hostApp',
remotes: {
remoteApp: 'remoteApp@http://localhost:3001/remoteEntry.js',
},
shared: {
react: { singleton: true, requiredVersion: '^17.0.0' },
'react-dom': { singleton: true },
},
}),
],
};
宿主应用在代码中通过动态 import 语法加载远程组件:
// src/index.js
import React from 'react';
import ReactDOM from 'react-dom';
const RemoteButton = React.lazy(() => import('remoteApp/Button'));
const App = () => (
<div>
<h1>Host Application</h1>
<React.Suspense fallback="Loading Button...">
<RemoteButton label="Click Me" />
</React.Suspense>
</div>
);
ReactDOM.render(<App />, document.getElementById('root'));
启动两个 devServer 后,宿主应用会在浏览器中加载远程应用暴露的 Button 组件。整个过程中,远程应用的代码并没有被打包进宿主应用的 bundle,而是通过运行时加载 remoteEntry.js 来获取。这种方式让两个团队可以独立部署自己的代码,远程应用更新后,宿主应用无需重新构建即可获取到最新的组件版本。
当然,实际生产环境还需要处理远程入口地址的管理、加载失败的回退、以及更复杂的依赖共享策略。但上述示例已经展示了模块联邦的核心用法,即通过配置实现跨应用模块共享。
三、共享依赖与版本冲突的处理
共享依赖是模块联邦中最容易出错的部分。当宿主应用和远程应用都依赖 React 时,如果两个应用各自打包了一份 React,页面上就会出现两个 React 实例,这会导致 Hooks 状态异常、Context 失效等问题。模块联邦通过 shared 配置来解决这个问题。
shared 配置可以是一个对象,也可以是一个数组。对象形式下,每个 key 是依赖包名,value 是一个配置对象。常见的配置项包括 singleton、requiredVersion、eager 等。singleton 为 true 表示整个应用只能有一个该依赖的实例,Webpack 会在运行时检查是否已经存在符合版本要求的实例,如果存在则复用,否则加载当前应用提供的版本。requiredVersion 用于指定最低版本要求,如果远程应用提供的版本不满足宿主应用的要求,Webpack 会发出警告。
下面是一个更完整的 shared 配置示例:
shared: {
react: {
singleton: true,
requiredVersion: '^17.0.0',
eager: false,
},
'react-dom': {
singleton: true,
requiredVersion: '^17.0.0',
},
lodash: {
singleton: false,
requiredVersion: '^4.17.0',
},
}
eager 为 false 表示该共享模块不会被立即加载,而是等到真正使用时才异步加载。对于 React 这种基础库,通常建议设置为 true,以避免在初始渲染阶段出现异步等待。但 eager 也会增加初始包体积,需要根据实际情况权衡。
除了 singleton,还可以通过版本范围来控制共享行为。如果两个应用使用的 React 版本相差较大,例如一个使用 17.x,另一个使用 18.x,那么强制 singleton 可能会引发运行时错误。这时可以考虑不设置 singleton,让每个应用加载自己的 React 实例,但这样又回到了多实例的问题。更合理的做法是统一团队之间的依赖版本,或者在远程应用中使用与宿主应用相同的版本范围。
四、模块联邦与 single-spa/qiankun 的选型思考
模块联邦、single-spa 和 qiankun 都可以用来实现微前端,但它们解决的问题层次不同。single-spa 是一个运行时微前端框架,它提供应用注册、生命周期管理、路由匹配等能力,但自身不解决模块共享和构建问题。qiankun 基于 single-spa 封装,增加了 JS 沙箱、样式隔离、资源加载等能力,更适合国内业务场景。模块联邦则是 Webpack 5 的构建特性,它解决的是跨应用模块共享和依赖复用问题,不提供应用级的生命周期和路由管理。
从架构角度看,模块联邦更适合组件级共享和真正的独立部署。比如一个大型中后台系统,多个团队分别维护不同的业务模块,但都需要使用同一个设计系统组件库。使用模块联邦可以把组件库作为远程应用暴露出来,各个业务应用按需加载,组件库更新后所有应用自动生效。这种场景下,模块联邦比 single-spa/qiankun 更轻量,因为不需要额外的运行时框架。
然而,模块联邦并不具备应用级别的隔离能力。如果子应用之间存在全局变量污染、样式冲突或者需要独立的路由和状态管理,模块联邦本身无法解决这些问 题,需要配合其他手段。此时 qiankun 的沙箱和样式隔离能力就显得尤为重要。因此,在实际项目中,模块联邦和 qiankun 并不互斥,可以组合使用。例如,使用 qiankun 做子应用的加载和隔离,同时利用模块联邦在子应用之间共享公共组件和工具函数。
选型时需要明确团队的技术栈和业务特点。如果团队已经深度使用 Webpack 并且主要痛点是代码重复和依赖版本不一致,模块联邦是低成本高效率的方案。如果需要完整的微前端治理能力,比如子应用独立发布、灰度、回滚、监控等,则建议采用 qiankun 或 single-spa 这类框架,并在构建层配合模块联邦优化资源共享。