Webpack Module Federation 原理与实现

一、解决了什么问题

传统多应用架构的痛点:

问题 场景
重复加载依赖 A、B 两个应用都用 React,用户访问时加载了两份
跨应用共享代码困难 公共组件只能发 npm 包,改一次发布一次
巨石应用难拆分 一个大 repo,构建慢、团队协作难
微前端集成复杂 iframe、single-spa 各有局限,运行时共享难

核心解法:让独立部署的多个 JS 应用,在运行时互相共享代码和依赖,而不是构建时打包在一起。


二、核心概念

Host(宿主)             Remote(远程)
┌──────────────┐         ┌──────────────┐
│  主应用       │ ──加载── │  子应用       │
│              │         │  exposes:    │
│  remotes: {  │         │   ./Button   │
│   app2: URL  │         │   ./Header   │
│  }           │         │              │
└──────────────┘         └──────────────┘
        ↑                       ↑
        └──── shared: [react] ──┘
             (运行时协商,只加载一份)
角色 职责
Host(宿主) 消费其他应用暴露的模块
Remote(远程) 暴露自己的模块给别人用
Shared(共享) 声明哪些依赖在运行时共享(如 React)

一个应用可以同时是 Host 和 Remote。


三、配置示例

Remote 端(被消费方)

// webpack.config.js
new ModuleFederationPlugin({
  name: 'app2',
  filename: 'remoteEntry.js',       // 模块清单入口文件
  exposes: {
    './Button': './src/Button',     // 暴露的模块
    './Header': './src/Header',
  },
  shared: {
    react: { singleton: true },
    'react-dom': { singleton: true },
  },
})

Host 端(消费方)

// webpack.config.js
new ModuleFederationPlugin({
  name: 'app1',
  remotes: {
    app2: 'app2@https://cdn.com/remoteEntry.js',  // 运行时才加载
  },
  shared: {
    react: { singleton: true },
    'react-dom': { singleton: true },
  },
})

Host 中使用远程模块

// 像普通 import 一样写,实际是运行时异步加载
const Button = React.lazy(() => import('app2/Button'))

function App() {
  return (
    <Suspense fallback={<div>加载中...</div>}>
      <Button />
    </Suspense>

  )
}

四、React.lazy + import() 的含义

React.lazy(() => import('app2/Button'))

两层嵌套,各自独立:

  • import('app2/Button'):动态 import,返回 Promise。在 MF 场景下,app2 是 remotes 配置里的别名,运行时去远程 URL 拉取代码。
  • React.lazy(...):接收一个返回 Promise 的函数,首次渲染时才触发加载。

完整时序:

渲染 <Button />
    ↓
React.lazy 触发 import()
    ↓
浏览器请求 remoteEntry.js(模块清单)
    ↓
协商 shared 依赖(react 已有则复用)
    ↓
拉取 Button 的 chunk 文件
    ↓
Suspense fallback 消失,Button 渲染

五、运行时协商原理(shared 依赖)

没有 shared 时的问题

Host 打包:  [react@18.2] + [自己的代码]
Remote 打包:[react@18.2] + [Button 组件]

用户访问:
  加载 Host  → 下载一份 react
  加载 Remote → 又下载一份 react  ← 重复!
  内存里两个 React 实例 → Hooks 报错

加了 shared 之后

构建时:两边的 react 不打进 bundle,而是在 remoteEntry.js 里写入声明:"我提供/需要 react@18.2"。

运行时:MF runtime 执行协商:

Host 说:    我有 react@18.2,放进 shared scope
Remote 说:  我需要 react,版本要求 ^18.0.0

runtime 检查:18.2 满足 ^18.0.0 ✓
结论:Remote 复用 Host 已加载的 react,不重新下载
shared scope(全局共享空间)
┌─────────────────────────────┐
│  react@18.2  ← 只有这一份   │
│     ↑               ↑       │
│   Host 放入      Remote 复用 │
└─────────────────────────────┘

版本冲突处理

场景 singleton: true singleton: false
版本兼容 复用高版本 各自加载
版本冲突 强制用高版本 + 警告 各自加载两份

**React 必须设 **singleton: true,否则两个 React 实例并存,Hooks 校验失败报错。


六、shared 关键配置项

shared: {
  react: {
    singleton: true,          // 全局只允许一个实例
    requiredVersion: '^18.0.0', // 可接受的版本范围
    eager: false,             // true = 打进入口 chunk(增大体积,避免懒加载)
    strictVersion: false,     // true = 版本不匹配直接报错(不降级)
  }
}

七、构建产物说明

Remote 构建后会生成:

dist/
├── remoteEntry.js      ← 模块清单 + runtime(Host 首先加载这个)
├── src_Button_js.chunk.js  ← Button 组件的实际代码
└── ...

remoteEntry.js 是 MF 的核心文件,记录了:

  • 该 Remote 暴露了哪些模块
  • 每个模块对应哪个 chunk 文件
  • 声明了哪些 shared 依赖及版本

八、与其他方案对比

方案 共享依赖 独立部署 运行时通信 复杂度
npm 包 构建时合并 需重新发布
iframe 完全隔离 postMessage
single-spa 需手动协调 自定义
Module Federation 运行时共享 原生 import

九、适用场景

  • 多团队维护的微前端架构
  • 需要跨应用复用组件/工具库,但不想每次都发 npm 包
  • 大型应用拆分,独立部署、独立发布
  • 需要在运行时动态加载远程模块(插件化系统)
posted @ 2026-06-09 10:28  HuangBingQuan  阅读(33)  评论(0)    收藏  举报