目录

Webpack Module Federation 实战:构建企业级微前端应用

为什么需要 Module Federation?

传统微前端方案(iframe、路由分发、乾坤 qiankun)各有痛点:

方案 缺点
iframe 上下文隔离强但通信麻烦,SEO 不友好
路由分发 多应用需独立部署,体验割裂
qiankun 侵入性强,子应用改造成本高

Webpack 5 Module Federation 提供了一种全新的思路:在构建时定义共享模块,运行时动态加载远程代码,子应用和主应用共享同一个运行时,通信零成本。

基础架构

一个典型的 Module Federation 架构包含两种角色:

  • Host(宿主):加载远程模块的主应用,负责路由分发、公共 Layout
  • Remote(远程):独立开发的子应用,暴露出可消费的组件/页面
1
2
3
4
┌─────────────┐     ┌──────────────┐     ┌──────────────┐
│   Host App  │────▶│ Remote App A │     │ Remote App B │
│ (container) │◀────│   (商品页)    │     │   (用户页)    │
└─────────────┘     └──────────────┘     └──────────────┘

实战:搭建 Host + Remote

1. Remote 应用配置

Remote 应用通过 ModuleFederationPlugin 暴露自身组件:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
// remote/webpack.config.js
const { ModuleFederationPlugin } = require('webpack').container;

module.exports = {
  plugins: [
    new ModuleFederationPlugin({
      name: 'product_remote',       // 唯一标识
      filename: 'remoteEntry.js',    // 远程入口文件名
      exposes: {
        './ProductList': './src/components/ProductList',
        './ProductDetail': './src/pages/ProductDetail',
      },
      shared: {
        react: { singleton: true, requiredVersion: '^18.0.0' },
        'react-dom': { singleton: true },
      },
    }),
  ],
};

关键点:

  • name 全局唯一,不能与其他 remote 冲突
  • exposes 暴露的模块路径:key 是远程路径,value 是本地的模块路径
  • shared 共享的依赖库:singleton: true 确保整个应用只加载一个 React 实例

2. Host 应用配置

Host 声明需要从哪些 remote 加载模块:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
// host/webpack.config.js
new ModuleFederationPlugin({
  name: 'host_app',
  remotes: {
    ProductApp: 'product_remote@http://localhost:3001/remoteEntry.js',
  },
  shared: {
    react: { singleton: true, requiredVersion: '^18.0.0' },
    'react-dom': { singleton: true },
  },
});

remotes 字段的格式:{别名}: {remoteName}@{remoteEntryUrl}

3. 在 Host 中引用远程组件

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
// host/src/App.jsx
import React, { Suspense, lazy } from 'react';

// 使用 lazy 加载远程组件
const ProductList = lazy(() => import('ProductApp/ProductList'));

function App() {
  return (
    <div>
      <h1>主应用</h1>
      <Suspense fallback={<div>加载中...</div>}>
        <ProductList />
      </Suspense>
    </div>
  );
}

注意:必须用 React.lazy 或动态 import(),因为远程模块是异步加载的。

共享依赖策略

Module Federation 最关键也最容易出错的是共享依赖配置:

singleton + 版本锁定

1
2
3
4
5
6
7
8
9
shared: {
  react: {
    singleton: true,          // 仅一个实例
    requiredVersion: '^18.0.0', // 版本约束
    eager: false,             // 首次访问时异步加载
  },
  // 不带版本的全局工具库,用 eager 避免白屏
  'lodash-es': { singleton: true, eager: true },
}
  • React/Next.js 等框架库:设 singleton: true,否则会有多个 React 实例导致 hooks 报错
  • 轻量工具库(lodash、dayjs):可 singleton: true + eager: true,直接内联
  • 组件库(antd、MUI):建议 singleton: true,否则样式和 context 冲突

版本不匹配的处理

当 host 和 remote 依赖版本不同时,Webpack 默认使用声明版本更高的那个

1
2
3
4
5
6
7
// 通过 shared 的 version 字段也可以手动指定
shared: {
  dayjs: {
    singleton: true,
    version: '1.11.0',  // 降级到统一版本
  },
}

动态加载与路由集成

项目中更常见的是按路由粒度加载远程模块:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// host/src/router.jsx
const routes = [
  {
    path: '/products',
    component: lazy(() => import('ProductApp/ProductList')),
  },
  {
    path: '/products/:id',
    component: lazy(() => import('ProductApp/ProductDetail')),
  },
  {
    path: '/user',
    component: lazy(() => import('UserApp/UserProfile')),
  },
];

function AppRouter() {
  return (
    <BrowserRouter>
      <Layout>
        <Suspense fallback={<PageSkeleton />}>
          <Routes>
            {routes.map(r => (
              <Route key={r.path} path={r.path} element={<r.component />} />
            ))}
          </Routes>
        </Suspense>
      </Layout>
    </BrowserRouter>
  );
}

生产部署实战

统一版本号管理

1
2
3
4
5
6
7
// 每个 remote 发布时携带版本号
// product_remote@1.2.3 -> remoteEntry.1.2.3.js

new ModuleFederationPlugin({
  filename: `remoteEntry.${pkg.version}.js`,
  // ... 其余配置
})

用 CI 自动注入 remote 地址

开发和生产环境的 remote 地址不同,用环境变量动态注入:

1
2
3
4
5
6
7
8
// host/webpack.config.js
const REMOTE_URL = process.env.REMOTE_URL || 'http://localhost:3001';

new ModuleFederationPlugin({
  remotes: {
    ProductApp: `product_remote@${REMOTE_URL}/remoteEntry.js`,
  },
})

Nginx 缓存策略

remoteEntry.js 会被频繁引用,建议短缓存或 no-cache:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
# 远程入口文件不缓存,保证 host 总是获取最新版本
location /remoteEntry {
  add_header Cache-Control 'no-cache, must-revalidate';
}

# 业务 JS 可以强缓存(文件名含 hash)
location /static {
  expires 1y;
  add_header Cache-Control 'public, immutable';
}

常见坑点

1. 样式冲突

不同 remote 使用不同的 CSS-in-JS 方案或 UI 库版本,可能导致全局样式覆盖。

解决方案:使用 CSS Modules 或 CSS-in-JS 的命名空间隔离:

1
2
3
4
5
6
7
8
// webpack.config.js
{
  test: /\.module\.css$/,
  use: ['style-loader', {
    loader: 'css-loader',
    options: { modules: { localIdentName: '[name]__[local]--[hash:base64:5]' } }
  }],
}

2. Context 丢失

如果 host 和 remote 各有一个 React 实例,useContext 将无法共享数据。

解决方案

  1. 确保 reactreact-dom 在 shared 中设为 singleton: true
  2. 全局数据用 Vite/Rspack 的 definewindow 对象传递
  3. 复杂场景使用 React.createContext 结合 shared 策略

3. TypeScript 支持

远程组件在 host 中没有类型定义。推荐使用 @module-federation/typescript 插件:

1
npm install @module-federation/typescript -D
1
2
3
4
5
6
7
// webpack.config.js
const { FederatedTypesPlugin } = require('@module-federation/typescript');

plugins: [
  new ModuleFederationPlugin({ /* ... */ }),
  new FederatedTypesPlugin(),
],

4. 回退策略

当 remote 服务宕机时,host 应提供降级体验:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
// 封装远程加载组件
function RemoteWrapper({ fallback = <div>服务暂不可用</div>, children }) {
  const [hasError, setHasError] = useState(false);

  return hasError ? fallback : (
    <ErrorBoundary onError={() => setHasError(true)}>
      <Suspense fallback={<Skeleton />}>
        {children}
      </Suspense>
    </ErrorBoundary>
  );
}

总结

Module Federation 的核心优势在于:

  1. 运行时集成:无需 CI 协调即可独立部署各模块
  2. 共享运行时:只加载一次 React,避免重复下载
  3. 渐进迁移:老应用可以逐步暴露出模块,无需大重构

适合的场景:大型后台管理系统、B2B 门户、多团队协作的中台项目。如果你的项目小于 3 个页面或团队只有 1-2 人,用 Vite 的单体应用更省心。