Webpack 5模块联邦微前端架构与跨应用组件共享实战

模块联邦(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/

(0)
小编小编
上一篇 11小时前
下一篇 11小时前

相关推荐