模块联邦(Module Federation)是Webpack 5引入的微前端核心能力,允许多个独立构建的应用在运行时共享模块。前端工程化实践中,模块联邦解决了大型前端项目拆分为独立子应用后的组件共享、依赖管理和独立部署问题。相比single-spa等运行时方案,模块联邦由构建工具原生支持,无需额外框架,支持按需加载和Tree Shaking。
模块联邦架构设计与微前端方案选型
模块联邦的核心概念包括宿主(Host)和远程(Remote)。Host应用在运行时从Remote应用动态加载模块,两个应用独立构建、独立部署,通过HTTP协议在运行时连接。这种架构适合大型团队拆分为多个子团队,各自独立开发部署的场景。
模块联邦与其他微前端方案对比:single-spa需要在主应用中注册子应用入口HTML,耦合度较高;iframe方案天然隔离但通信困难和样式割裂;模块联邦在JavaScript层面共享模块,支持Tree Shaking和按需加载,通信方式与同构应用一致。选择模块联邦的前提是各子应用使用兼容的Webpack 5构建。
典型应用场景:电商平台中商品页、购物车、用户中心分属不同团队,各自使用React/Vue独立开发,通过模块联邦在运行时组合为单页应用。
Webpack 5 ModuleFederationPlugin配置
Remote应用(提供组件方)的webpack配置:
// remote/webpack.config.js
const ModuleFederationPlugin = require("webpack/lib/container/ModuleFederationPlugin");
const deps = require("./package.json").dependencies;
module.exports = {
entry: "./src/index.js",
mode: "production",
output: { publicPath: "https://remote.yunthe.com/" },
module: {
rules: [
{ test: /\.jsx?$/, exclude: /node_modules/,
use: { loader: "babel-loader", options: { presets: ["@babel/preset-react"] } } },
{ test: /\.css$/, use: ["style-loader", "css-loader"] },
],
},
plugins: [
new ModuleFederationPlugin({
name: "remoteApp",
filename: "remoteEntry.js",
exposes: {
"./ProductList": "./src/components/ProductList",
"./ProductCard": "./src/components/ProductCard",
"./useProducts": "./src/hooks/useProducts",
},
shared: {
react: { singleton: true, requiredVersion: deps.react, eager: false },
"react-dom": { singleton: true, requiredVersion: deps["react-dom"] },
},
}),
],
};
配置参数说明:name是Remote应用的唯一标识;filename是Remote入口文件名,Host通过加载此文件获取模块;exposes定义对外暴露的模块路径映射;shared配置共享依赖,singleton: true确保React等库只加载一份实例,避免Hooks失效等问题。
Host应用(消费组件方)的webpack配置:
// host/webpack.config.js
const ModuleFederationPlugin = require("webpack/lib/container/ModuleFederationPlugin");
const deps = require("./package.json").dependencies;
module.exports = {
entry: "./src/index.js",
mode: "production",
output: { publicPath: "auto" },
module: {
rules: [
{ test: /\.jsx?$/, exclude: /node_modules/,
use: { loader: "babel-loader", options: { presets: ["@babel/preset-react"] } } },
{ test: /\.css$/, use: ["style-loader", "css-loader"] },
],
},
plugins: [
new ModuleFederationPlugin({
name: "hostApp",
remotes: { remoteApp: "remoteApp@https://remote.yunthe.com/remoteEntry.js" },
shared: {
react: { singleton: true, requiredVersion: deps.react },
"react-dom": { singleton: true, requiredVersion: deps["react-dom"] },
},
}),
],
};
remotes配置中,键remoteApp是引用别名,值格式为name@url,指向Remote应用的remoteEntry.js地址。
宿主应用与远程应用搭建
Host应用通过动态import加载Remote模块。React组件中使用React.lazy和Suspense实现按需加载:
// host/src/App.jsx
import React, { Suspense, lazy, useState } from "react";
const ProductList = lazy(() => import("remoteApp/ProductList"));
const ProductCard = lazy(() => import("remoteApp/ProductCard"));
function App() {
const [selectedProduct, setSelectedProduct] = useState(null);
return (
<div className="app-container">
<h1>商品中心</h1>
<Suspense fallback={<div>加载中...</div>}>
<ProductList onSelect={setSelectedProduct} />
</Suspense>
{selectedProduct && (
<Suspense fallback={<div>加载中...</div>}>
<ProductCard product={selectedProduct} />
</Suspense>
)}
</div>
);
}
export default App;
Host应用入口需要处理异步边界(Async Boundary)。模块联邦的加载是异步的,入口文件必须是异步形式:
// host/src/index.js
import React from "react";
import { createRoot } from "react-dom/client";
import("./App").then(({ default: App }) => {
const root = createRoot(document.getElementById("root"));
root.render(<React.StrictMode><App /></React.StrictMode>);
});
Remote应用的入口文件需要导出bootstrap异步函数,确保Webpack能正确处理异步边界:
// remote/src/index.js
import("./bootstrap");
// remote/src/bootstrap.js
import ProductList from "./components/ProductList";
import ProductCard from "./components/ProductCard";
export { ProductList, ProductCard };
跨应用组件共享与依赖管理
共享依赖是模块联邦的关键能力。shared配置确保React、ReactDOM等公共库只加载一份实例。版本兼容性处理:
shared: {
react: { singleton: true, requiredVersion: "^18.0.0", strictVersion: false },
"react-dom": { singleton: true, requiredVersion: "^18.0.0" },
"antd": { singleton: true, requiredVersion: "^5.0.0" },
}
strictVersion设为false时,当Remote和Host的React版本不一致,会使用Host的版本并输出警告。设为true则版本不匹配时构建失败,适合需要严格版本控制的场景。
跨应用状态共享。模块联邦本身不提供状态管理,需要借助Event Bus进行跨应用通信,避免Store共享的版本耦合问题:
class EventBus {
constructor() { this.events = {}; }
on(event, callback) { (this.events[event] ||= []).push(callback); }
emit(event, data) { (this.events[event] || []).forEach(cb => cb(data)); }
}
export const eventBus = new EventBus();
// Remote发送事件
eventBus.emit("productSelected", product);
// Host监听事件
eventBus.on("productSelected", (product) => setSelectedProduct(product));
模块联邦动态加载与路由集成
结合React Router实现路由级别的微前端拆分。每个子应用对应一组路由,按需加载:
// host/src/App.jsx
import React, { Suspense, lazy } from "react";
import { BrowserRouter, Routes, Route } from "react-router-dom";
import Home from "./pages/Home";
const ProductPage = lazy(() => import("remoteApp/ProductList"));
const CartPage = lazy(() => import("cartApp/Cart"));
const UserPage = lazy(() => import("userApp/Profile"));
function App() {
return (
<BrowserRouter>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/products/*" element={
<Suspense fallback={<LoadingSpinner />}><ProductPage /></Suspense>
} />
<Route path="/cart/*" element={
<Suspense fallback={<LoadingSpinner />}><CartPage /></Suspense>
} />
</Routes>
</BrowserRouter>
);
}
动态Remote配置。当Remote应用地址不固定时,可以在运行时动态注入Remote URL:
async function loadRemoteComponent(url, scope, module) {
await __webpack_init_sharing__("default");
await new Promise((resolve, reject) => {
const script = document.createElement("script");
script.src = url;
script.onload = resolve;
script.onerror = reject;
document.head.appendChild(script);
});
const container = window[scope];
await container.init(__webpack_share_scopes__.default);
const factory = await container.get(module);
return factory();
}
const ProductList = lazy(() =>
loadRemoteComponent("https://remote.yunthe.com/remoteEntry.js", "remoteApp", "./ProductList")
);
动态加载方案适合Remote地址需要在运行时根据环境变量或配置中心决定场景,如灰度发布时将部分用户流量导向新版本的Remote应用。
微前端部署策略与性能优化
Remote应用的remoteEntry.js需要配置CDN缓存策略。该文件包含模块映射信息,内容随构建变化,应设置较短的缓存时间(如5分钟),而具体模块chunk文件内容稳定,可以设置长缓存(如1年):
# Nginx配置示例
location ~ remoteEntry\.js$ {
add_header Cache-Control "public, max-age=300";
}
location ~ \.js$ {
add_header Cache-Control "public, max-age=31536000, immutable";
}
预加载Remote模块。在用户可能访问Remote页面之前预加载remoteEntry.js,减少首次加载延迟:
<link rel="preload" href="https://remote.yunthe.com/remoteEntry.js" as="script">
// 或在用户hover导航链接时预加载
<Link to="/products" onMouseEnter={() => import("remoteApp/ProductList")}>
商品中心
</Link>
构建体积优化。使用Webpack Bundle Analyzer分析模块联邦的共享依赖大小,确保shared配置没有引入不必要的依赖。生产环境建议将共享库的外部化(externals)与CDN加载结合:
shared: {
react: { singleton: true, import: false },
"react-dom": { singleton: true, import: false },
}
// import: false 表示不从打包产物中提供该依赖,而是从Host或CDN获取
错误边界处理。Remote应用加载失败时需要优雅降级,避免白屏:
class ErrorBoundary extends React.Component {
constructor(props) { super(props); this.state = { hasError: false }; }
static getDerivedStateFromError() { return { hasError: true }; }
componentDidCatch(error) { console.error("Remote模块加载失败:", error); }
render() {
if (this.state.hasError) return <div>该模块暂时不可用</div>;
return this.props.children;
}
}
<ErrorBoundary>
<Suspense fallback={<LoadingSpinner />}>
<ProductPage />
</Suspense>
</ErrorBoundary>
错误边界配合Suspense使用,Suspense处理加载中的状态,ErrorBoundary处理加载失败的错误,两者组合提供完整的异步加载体验。
原创文章,作者:小编,如若转载,请注明出处:https://www.yunthe.com/webpack5-mo-kuai-lian-bang-wei-qian-duan-jia-gou-yu-kua/