前端微服务架构实战:基于 Module Federation 的微前端落地指南

By | 2026年10月11日

为什么需要前端微服务?

随着业务增长,单体前端应用逐渐变得臃肿:构建慢、部署耦合、技术栈难以升级。微前端(Micro Frontends)将前端拆分为多个可独立开发、测试、部署的微应用,而 Webpack 5 的 Module Federation 让这一架构变得前所未有的简单。

本文将以一个真实场景为例:一个电商后台,包含 主应用(容器)、商品微应用、订单微应用。我们将从零搭建,实现运行时模块共享、独立部署、跨应用通信。

> 注意:本文基于 Webpack 5.75+、React 18、React Router 6,所有代码均可直接运行。

1. 架构设计

  • 主应用(host):负责布局、路由分发、共享依赖(React、ReactDOM)。
  • 微应用(remote):独立开发部署,暴露 ./App 组件,可被主应用动态加载。
  • 通信机制:通过自定义事件(CustomEvent)和共享状态库(如 Zustand)实现跨应用通信。
  • 路由:主应用使用 React Router 的 * 通配符挂载微应用,微应用内部使用 MemoryRouter 避免路由冲突。

2. 环境准备

创建三个独立项目(建议使用 pnpm workspace 管理):


mkdir micro-frontend-demo && cd micro-frontend-demo
pnpm init
pnpm create react-app host --template typescript
pnpm create react-app products --template typescript
pnpm create react-app orders --template typescript

安装 Webpack 5 及相关依赖(CRA 已内置 Webpack 5,但需覆盖配置)。推荐使用 @craco/craco 修改配置:


pnpm add -D @craco/craco

3. 配置微应用(Remote)

以 products 为例,创建 craco.config.js:


const { ModuleFederationPlugin } = require('webpack').container;
const deps = require('./package.json').dependencies;

module.exports = {
  webpack: {
    configure: (webpackConfig) => {
      webpackConfig.plugins.push(
        new ModuleFederationPlugin({
          name: 'products',
          filename: 'remoteEntry.js',
          exposes: {
            './App': './src/App',
          },
          shared: {
            react: { singleton: true, requiredVersion: deps.react },
            'react-dom': { singleton: true, requiredVersion: deps['react-dom'] },
            'react-router-dom': { singleton: true, requiredVersion: deps['react-router-dom'] },
          },
        })
      );
      // 允许跨域加载 remoteEntry.js
      webpackConfig.devServer = {
        ...webpackConfig.devServer,
        port: 3001,
        headers: { 'Access-Control-Allow-Origin': '*' },
      };
      return webpackConfig;
    },
  },
};

修改 products/src/App.tsx,确保默认导出组件:


import React from 'react';
import { Routes, Route, Link } from 'react-router-dom';

const ProductList = () => <div>商品列表(微应用)</div>;
const ProductDetail = () => <div>商品详情(微应用)</div>;

const App = () => (
  <div style={{ padding: 20, border: '2px solid blue' }}>
    <h2>商品微应用</h2>
    <nav>
      <Link to="/">列表</Link> | <Link to="/detail">详情</Link>
    </nav>
    <Routes>
      <Route path="/" element={<ProductList />} />
      <Route path="/detail" element={<ProductDetail />} />
    </Routes>
  </div>
);

export default App;

启动微应用:pnpm start(端口 3001)。同理配置 orders,端口 3002,暴露 ./App。

4. 配置主应用(Host)

主应用的 craco.config.js:


const { ModuleFederationPlugin } = require('webpack').container;
const deps = require('./package.json').dependencies;

module.exports = {
  webpack: {
    configure: (webpackConfig) => {
      webpackConfig.plugins.push(
        new ModuleFederationPlugin({
          name: 'host',
          remotes: {
            products: 'products@http://localhost:3001/remoteEntry.js',
            orders: 'orders@http://localhost:3002/remoteEntry.js',
          },
          shared: {
            react: { singleton: true, requiredVersion: deps.react },
            'react-dom': { singleton: true, requiredVersion: deps['react-dom'] },
            'react-router-dom': { singleton: true, requiredVersion: deps['react-router-dom'] },
          },
        })
      );
      webpackConfig.devServer = {
        ...webpackConfig.devServer,
        port: 3000,
      };
      return webpackConfig;
    },
  },
};

4.1 动态加载远程组件

创建 src/components/RemoteApp.tsx,使用 React.lazy + Suspense 加载:


import React, { Suspense, lazy } from 'react';

const RemoteApp = ({ scope, module }: { scope: string; module: string }) => {
  const Component = lazy(() => import(`${scope}/${module}`));
  return (
    <Suspense fallback={<div>加载中...</div>}>
      <Component />
    </Suspense>
  );
};

export default RemoteApp;

> 注意:动态导入的路径必须是静态字符串,否则 Webpack 无法解析。这里通过变量拼接会失败,正确做法是使用 import(/ webpackIgnore: true / ...) 或更简单的显式导入:


const ProductsApp = lazy(() => import('products/App'));
const OrdersApp = lazy(() => import('orders/App'));

4.2 主应用路由与布局


// src/App.tsx
import React, { Suspense, lazy } from 'react';
import { BrowserRouter, Routes, Route, Link } from 'react-router-dom';

const ProductsApp = lazy(() => import('products/App'));
const OrdersApp = lazy(() => import('orders/App'));

const Home = () => <div>主应用首页</div>;

const App = () => (
  <BrowserRouter>
    <div style={{ display: 'flex', minHeight: '100vh' }}>
      <aside style={{ width: 200, background: '#f0f0f0', padding: 20 }}>
        <h3>主应用</h3>
        <nav>
          <div><Link to="/">首页</Link></div>
          <div><Link to="/products">商品</Link></div>
          <div><Link to="/orders">订单</Link></div>
        </nav>
      </aside>
      <main style={{ flex: 1, padding: 20 }}>
        <Routes>
          <Route path="/" element={<Home />} />
          <Route path="/products/*" element={
            <Suspense fallback={<div>加载商品微应用...</div>}>
              <ProductsApp />
            </Suspense>
          } />
          <Route path="/orders/*" element={
            <Suspense fallback={<div>加载订单微应用...</div>}>
              <OrdersApp />
            </Suspense>
          } />
        </Routes>
      </main>
    </div>
  </BrowserRouter>
);

export default App;

启动主应用(端口 3000),访问 http://localhost:3000,点击「商品」即可看到蓝色边框的微应用。

5. 跨应用通信

微应用之间不应直接依赖,推荐使用 CustomEvent 或 共享状态库。这里演示基于 Zustand 的共享 store:

  1. 在主应用和微应用中都安装 zustand。
  2. 在主应用中创建 src/store/userStore.ts:

import { create } from 'zustand';

interface UserState {
  username: string;
  setUsername: (name: string) => void;
}

export const useUserStore = create<UserState>((set) => ({
  username: '访客',
  setUsername: (name) => set({ username: name }),
}));
  1. 在 ModuleFederationPlugin 的 shared 中加入 zustand,确保单例:

shared: {
  // ...
  zustand: { singleton: true, requiredVersion: deps.zustand },
}
  1. 在微应用(products)中直接 import { useUserStore } from 'host/store/userStore'?不行,因为微应用不应依赖主应用。正确做法:将 store 作为独立 npm 包发布,或通过 exposes 暴露给微应用。这里采用后者:主应用暴露 ./store:

// host craco.config.js
exposes: {
  './store': './src/store/userStore',
},

微应用通过 import { useUserStore } from 'host/store' 使用。但这样微应用就依赖了 host,形成双向依赖。更优雅的方案:使用自定义事件。


// 主应用触发
window.dispatchEvent(new CustomEvent('user-login', { detail: { username: '张三' } }));

// 微应用监听
useEffect(() => {
  const handler = (e: CustomEvent) => setUser(e.detail.username);
  window.addEventListener('user-login', handler as EventListener);
  return () => window.removeEventListener('user-login', handler as EventListener);
}, []);

> 💡 最佳实践:通信尽量通过 props 或回调函数,若必须跨应用,优先使用事件总线或发布订阅,避免共享可变状态。

6. 独立部署与版本管理

生产环境需将 remoteEntry.js 部署到 CDN,并动态指定 URL。可在主应用中使用环境变量:


remotes: {
  products: `products@${process.env.REACT_APP_PRODUCTS_URL}/remoteEntry.js`,
}

微应用独立部署后,主应用无需重新构建即可加载最新版本。但要注意 缓存问题:给 remoteEntry.js 加上 hash 或使用 Cache-Control: no-cache。

7. 常见坑与优化

  • React 版本不一致:shared 中必须设置 singleton: true,否则会出现多个 React 实例,导致 hooks 报错。
  • 路由冲突:微应用内部应使用 MemoryRouter 或 BrowserRouter 的 basename,避免与主应用路由互相干扰。
  • 样式隔离:推荐使用 CSS Modules 或 Shadow DOM。若使用全局样式,建议加命名空间前缀。
  • 性能:预加载微应用资源,使用 React.lazy + Suspense 实现按需加载;对于高频微应用,可在主应用空闲时预加载。
  • 错误边界:每个微应用包裹 ErrorBoundary,避免一个微应用崩溃导致整个页面白屏。

8. 总结

本文从零实现了基于 Module Federation 的前端微服务架构,涵盖配置、路由、通信、部署全流程。核心要点:

  1. 主应用与微应用通过 remotes 和 exposes 建立连接。
  2. 共享依赖必须声明为单例,尤其是 React 相关库。
  3. 通信推荐使用自定义事件或发布订阅,保持微应用自治。
  4. 独立部署时注意 CDN 路径和缓存策略。

下一步可探索:微前端框架 Qiankun、Single-SPA 的对比;基于 Web Components 的样式隔离;以及结合 CI/CD 实现自动化版本发布。

> 本文代码已放在 GitHub 示例仓库(模拟链接),欢迎 star 交流。