一、Module Federation解决的核心问题

传统微前端方案(iframe、single-spa)都面临一个根本矛盾:应用之间无法共享运行时依赖,导致巨大的bundle重复和加载时间问题。Module Federation(模块联邦)从根本上打破了这一限制。

1.1 共享依赖的困境与联邦方案的突破

// 传统方案:独立构建,共享依赖时产生重复
//
// App Shell (host)
//
// app-vendor.js     ← lodash + react + react-dom = 200KB
// app-main.js       ← 业务代码 = 150KB
// Total: 350KB
//
// ┌──────────────────────────────────────┐
// │  子应用A (remote)                     │
// │  a-vendor.js  ← lodash+react = 200KB 重复! │
// │  a-main.js    ← 业务代码 = 80KB        │
// │  Total: 280KB(200KB浪费了)          │
// └──────────────────────────────────────┘
// ┌──────────────────────────────────────┐
// │  子应用B (remote)                     │
// │  b-vendor.js  ← lodash+react = 200KB  重复! │
// │  b-main.js    ← 业务代码 = 120KB       │
// │  Total: 320KB(200KB浪费了)          │
// └──────────────────────────────────────┘
// 总体:950KB(其中400KB是重复的)

// Module Federation的架构:
//
// ┌──────────────────────────────────────────────┐
// │           webpack Module Federation            │
// │                                              │
// │   Host App                                   │
// │   ┌────────────────────────────────────┐   │
// │   │  @module-federation/manifest      │   │
// │   │  <script src="http://remote/remote.js">│   │
// │   │  在运行时拉取远程模块              │   │
// │   └────────────────────────────────────┘   │
// │                                              │
// │   Shared: react ★ shared                   │
// │        ↑ 仅加载一次,多应用共用             │
// │                                              │
// │   ┌──────────┐  ┌──────────┐  ┌──────────┐│
// │   │ Host App │  │ Remote A │  │ Remote B ││
// │   │ 350KB    │  │  80KB    │  │ 120KB    ││
// │   └──────────┘  └──────────┘  └──────────┘│
// │        ↑           ↑           ↑            │
// │        └───────────┴───────────┘            │
// │           react/react-dom = 200KB (only 1x) │
// │   Total: 770KB → 节省 180KB + N*200KB     │
// └──────────────────────────────────────────────┘

二、联邦模块的配置模型

2.1 Host与Remote的完整配置

// Host(宿主应用)webpack配置
const HtmlWebpackPlugin = require('html-webpack-plugin');
const { ModuleFederationPlugin } = require('webpack').container;
const path = require('path');

module.exports = {
  mode: 'production',
  entry: './src/index.js',
  output: {
    publicPath: 'auto',  // 'auto'让webpack自动选择CDN路径
  },
  plugins: [
    new ModuleFederationPlugin({
      name: 'host_app',
      // 暴露自身哪些模块给其他应用使用
      exposes: {
        './Header': './src/components/Header.jsx',
        './Store': './src/store/index.js',
      },
      // 共享依赖声明(自动加载和版本冲突解决)
      shared: {
        react: {
          singleton: true,     // 强制单例(只允许一个版本)
          requiredVersion: '^18.0.0',
          eager: false,        // false=懒加载,不影响首屏速度
        },
        'react-dom': {
          singleton: true,
          requiredVersion: '^18.0.0',
        },
        // 共享多个版本时允许不同版本共存
        'lodash': {
          singleton: false,    // 允许多版本共存
          strictVersion: false,
        },
      },
    }),
    new HtmlWebpackPlugin({
      template: './public/index.html',
    }),
  ],
};

2.2 Remote(远程应用)配置

// Remote(被消费应用)webpack配置
const { ModuleFederationPlugin } = require('webpack').container;
const HtmlWebpackPlugin = require('html-webpack-plugin');

module.exports = {
  mode: 'production',
  entry: './src/bootstrap.js',  // 入口改为bootstrap
  output: {
    publicPath: 'https://remote-a.example.com/',
  },
  plugins: [
    new ModuleFederationPlugin({
      name: 'remote_app_a',
      // 暴露给宿主使用的模块
      filename: 'remoteEntry.js',   // 生成的远程入口文件
      exposes: {
        './ProductList': './src/components/ProductList.jsx',
        './ProductDetail': './src/components/ProductDetail.jsx',
      },
      shared: {
        react: { singleton: true, requiredVersion: '^18.0.0' },
        'react-dom': { singleton: true, requiredVersion: '^18.0.0' },
      },
    }),
  ],
};

// bootstrap.js(代替index.js作为入口)
import('react').then(React => {
  import('./App.jsx').then(({ default: App }) => {
    ReactDOM.createRoot(document.getElementById('root')).render(<App />);
  });
});
// 为什么要用bootstrap.js?
// 懒加载react(不在script标签里同步加载)
// 这样host可以先加载自己的react版本

三、运行时加载与版本协商机制

3.1 动态远程模块加载

// 在Host应用中动态消费Remote模块
// 方法一:同步导入(必须在bootstrap前声明)
// remoteEntry.js已暴露ProductList
// import ProductList from 'remote_app_a/ProductList';

// 方法二:运行时动态加载(更灵活)
// src/dynamicRemote.js
const loadRemote = async (scope, module) => {
  // 初始化容器(每个remote容器只初始化一次)
  await __webpack_init_sharing__('default');

  const container = window[scope]; // window.remote_app_a
  if (!container) throw new Error(`Container ${scope} not found`);

  // 获取Remote共享的模块(如react)
  await container.init(__webpack_share_scopes__.default);

  // 加载指定模块
  const factory = await window[scope].get(module);
  // factory = () => require('./src/components/ProductList.jsx')

  const Module = factory();
  return Module.default; // 导出default
};

// 使用示例:
async function App() {
  const { ProductList } = await loadRemote('remote_app_a', './ProductList');
  return <ProductList />;
}

// 手动实现一个简化版容器加载器(理解原理):
const loadRemoteModule = async (url, scope, modulePath) => {
  // ① 加载remoteEntry.js
  await loadScript(url);

  // ② 注册共享模块
  await __webpack_init_sharing__(scope);

  const container = window[scope];
  await container.init(window.__webpack_share_scopes__[scope]);

  // ③ 获取模块
  const moduleFactory = await container.get(modulePath);
  return moduleFactory();
};

3.2 版本冲突解决策略

// 版本冲突解决的四种策略:
//
// 策略一:singleton(单例,最严格)
shared: {
  react: { singleton: true }
}
// → 强制使用最高版本(语义版本兼容)
// react@18.2.0 vs react@18.0.0 → 使用18.2.0
// react@17 vs react@18 → ❌ 版本冲突,无法加载

// 策略二:requiredVersion(范围兼容)
shared: {
  'react-dom': { requiredVersion: '^18.0.0' }
}
// → 18.1.0/18.2.0/18.9.0都兼容,17.x ❌

// 策略三:strictVersion(严格相等)
shared: {
  axios: { strictVersion: true }
}
// → 必须完全相同版本才共享,否则各自加载

// 策略四:单例+版本范围(生产推荐)
shared: {
  react: {
    singleton: true,
    requiredVersion: '^18.0.0',
    // 允许版本范围的"最宽"解析
  }
}

// ⚠️ 常见问题:host和remote的react版本不一致
// Host: react@18.2.0 (singleton)
// Remote: react@18.0.0
// → Module Federation会选择18.2.0(取最高兼容版本)
// → Remote会使用Host加载的react实例(共享成功)

// 调试版本冲突:
// 浏览器控制台执行:
window.__webpack_share_scopes__

四、生产环境部署架构

// 生产部署拓扑
//
// CDN / Nginx (反向代理)
// ├── /           → host_app
// ├── /remote-a/  → remote_app_a (独立部署)
// ├── /remote-b/  → remote_app_b (独立部署)
// └── /remote-c/  → remote_app_c (独立部署)
//
// 每个应用独立CI/CD,互不影响

// Host的nginx配置(支持多应用路由)
server {
    listen 80;
    server_name app.example.com;

    location / {
        proxy_pass http://host_app:3000;
    }

    # Remote应用的路径
    location /remote-a/ {
        proxy_pass http://remote_app_a:3001/;
        add_header Access-Control-Allow-Origin *;
    }

    location /remote-b/ {
        proxy_pass http://remote_app_b:3002/;
        add_header Access-Control-Allow-Origin *;
    }
}

// Host的webpack publicPath
output: {
  publicPath: 'auto',  // 自动适配当前域名
  // 或明确指定:
  // publicPath: 'https://cdn.example.com/host/',
}

// Remote的webpack filename(必须能被CDN访问)
plugins: [
  new ModuleFederationPlugin({
    filename: 'remoteEntry.js',  // 访问路径:/remote-a/remoteEntry.js
    // 在Nginx中需要配置跨域:
    // add_header Access-Control-Allow-Origin *;
  })
]

// ⚠️ 生产环境注意事项:
// 1. remoteEntry.js不应被浏览器长期缓存(更新版本时需刷新)
//    → 使用contenthash:filename: 'remoteEntry.[contenthash:8].js'
// 2. 所有应用使用相同的webpack版本(MF的兼容性保证)
// 3. 使用SSR时,Module Federation需要Node构建目标