Module Federation

下面,我们来系统的梳理关于 微前端:Module Federation 的基本知识点:


一、微前端与 Module Federation 概述

1.1 什么是微前端?

微前端是一种将前端应用分解为更小、更简单、可以独立开发、测试和部署的架构模式。

1.2 Module Federation 的核心价值

特性 传统微前端 Module Federation
部署独立性
技术栈无关
运行时集成
共享依赖 困难 内置支持
开发体验 复杂 优秀

1.3 架构对比

传统微前端 构建时集成 iframe 隔离 重复依赖 Module Federation 运行时集成 依赖共享 动态加载

二、Module Federation 核心概念

2.1 基本术语

  • Host(主机):消费其他应用模块的应用
  • Remote(远程):被其他应用消费的应用
  • Exposes(暴露):Remote 应用对外提供的模块
  • Remotes(远程模块):Host 应用从 Remote 消费的模块
  • Shared(共享):应用间共享的依赖

2.2 工作原理

Host 应用 Remote 应用 浏览器 加载 Host 应用 执行 Host 代码 动态请求 Remote 模块 返回 Remote 模块 集成 Remote 模块 Host 应用 Remote 应用 浏览器

三、Webpack 5 Module Federation 配置

3.1 基础配置结构

javascript 复制代码
// webpack.config.js (Host 应用)
const ModuleFederationPlugin = require('webpack/lib/container/ModuleFederationPlugin');

module.exports = {
  plugins: [
    new ModuleFederationPlugin({
      name: 'hostApp',
      remotes: {
        remoteApp: 'remoteApp@http://localhost:3001/remoteEntry.js',
      },
      shared: {
        react: { singleton: true, eager: true },
        'react-dom': { singleton: true, eager: true },
      },
    }),
  ],
};
javascript 复制代码
// webpack.config.js (Remote 应用)
const ModuleFederationPlugin = require('webpack/lib/container/ModuleFederationPlugin');

module.exports = {
  plugins: [
    new ModuleFederationPlugin({
      name: 'remoteApp',
      filename: 'remoteEntry.js',
      exposes: {
        './Button': './src/components/Button',
        './Header': './src/components/Header',
      },
      shared: {
        react: { singleton: true },
        'react-dom': { singleton: true },
      },
    }),
  ],
};

3.2 完整配置示例

javascript 复制代码
// host-app/webpack.config.js
const { ModuleFederationPlugin } = require('webpack').container;
const deps = require('./package.json').dependencies;

module.exports = {
  // ...其他配置
  plugins: [
    new ModuleFederationPlugin({
      name: 'host',
      remotes: {
        remoteApp: `remoteApp@${process.env.REMOTE_APP_URL || 'http://localhost:3001'}/remoteEntry.js`,
        authApp: `authApp@${process.env.AUTH_APP_URL || 'http://localhost:3002'}/remoteEntry.js`,
      },
      shared: {
        ...deps,
        react: {
          singleton: true,
          requiredVersion: deps.react,
          eager: true,
        },
        'react-dom': {
          singleton: true,
          requiredVersion: deps['react-dom'],
          eager: true,
        },
        'react-router-dom': {
          singleton: true,
          requiredVersion: deps['react-router-dom'],
        },
      },
    }),
  ],
};

四、React 应用集成实战

4.1 Host 应用配置

javascript 复制代码
// src/bootstrap.js (Host 入口)
import('./bootstrap');

// src/App.js
import React, { Suspense } from 'react';
import ErrorBoundary from './components/ErrorBoundary';

// 动态导入 Remote 组件
const RemoteButton = React.lazy(() => import('remoteApp/Button'));
const RemoteHeader = React.lazy(() => import('remoteApp/Header'));

function App() {
  return (
    <div>
      <ErrorBoundary>
        <Suspense fallback={<div>Loading Remote Components...</div>}>
          <RemoteHeader title="Host Application" />
        </Suspense>
      </ErrorBoundary>
      
      <main>
        <h1>Host App Content</h1>
        <ErrorBoundary>
          <Suspense fallback={<div>Loading Button...</div>}>
            <RemoteButton onClick={() => console.log('Clicked from remote')}>
              Remote Button
            </RemoteButton>
          </Suspense>
        </ErrorBoundary>
      </main>
    </div>
  );
}

export default App;

4.2 Remote 应用组件

javascript 复制代码
// remote-app/src/components/Button.js
import React from 'react';
import './Button.css';

const Button = ({ children, onClick, variant = 'primary' }) => {
  return (
    <button 
      className={`btn btn-${variant}`}
      onClick={onClick}
    >
      {children}
    </button>
  );
};

export default Button;

// remote-app/src/components/Header.js
import React from 'react';

const Header = ({ title }) => {
  return (
    <header style={{ padding: '1rem', background: '#f0f0f0' }}>
      <h1>{title}</h1>
      <nav>
        <a href="/">Home</a> | <a href="/about">About</a>
      </nav>
    </header>
  );
};

export default Header;

4.3 错误边界组件

javascript 复制代码
// src/components/ErrorBoundary.js
import React from 'react';

class ErrorBoundary extends React.Component {
  constructor(props) {
    super(props);
    this.state = { hasError: false, error: null };
  }

  static getDerivedStateFromError(error) {
    return { hasError: true, error };
  }

  componentDidCatch(error, errorInfo) {
    console.error('Remote component error:', error, errorInfo);
  }

  render() {
    if (this.state.hasError) {
      return (
        <div style={{ padding: '1rem', border: '1px solid #ff6b6b', color: '#ff6b6b' }}>
          <h3>Remote Component Failed to Load</h3>
          <p>{this.state.error?.message}</p>
          <button onClick={() => this.setState({ hasError: false, error: null })}>
            Retry
          </button>
        </div>
      );
    }

    return this.props.children;
  }
}

export default ErrorBoundary;

五、路由集成与状态管理

5.1 跨应用路由配置

javascript 复制代码
// host-app/src/App.js
import { BrowserRouter as Router, Routes, Route, Link } from 'react-router-dom';

const RemoteHome = React.lazy(() => import('remoteApp/HomePage'));
const RemoteAbout = React.lazy(() => import('remoteApp/AboutPage'));

function App() {
  return (
    <Router>
      <nav>
        <Link to="/">Home</Link> | 
        <Link to="/about">About</Link> |
        <Link to="/remote">Remote Home</Link> |
        <Link to="/remote/about">Remote About</Link>
      </nav>
      
      <Routes>
        <Route path="/" element={<HomePage />} />
        <Route path="/about" element={<AboutPage />} />
        <Route 
          path="/remote/*" 
          element={
            <Suspense fallback={<div>Loading Remote App...</div>}>
              <RemoteAppRouter />
            </Suspense>
          } 
        />
      </Routes>
    </Router>
  );
}

// Remote 应用的路由包装器
function RemoteAppRouter() {
  return (
    <Routes>
      <Route path="/" element={<RemoteHome />} />
      <Route path="/about" element={<RemoteAbout />} />
    </Routes>
  );
}

5.2 状态管理集成

javascript 复制代码
// shared-state/src/store.js (共享状态库)
import { createStore } from 'redux';

const initialState = {
  user: null,
  cart: [],
  theme: 'light'
};

function rootReducer(state = initialState, action) {
  switch (action.type) {
    case 'SET_USER':
      return { ...state, user: action.payload };
    case 'ADD_TO_CART':
      return { ...state, cart: [...state.cart, action.payload] };
    case 'SET_THEME':
      return { ...state, theme: action.payload };
    default:
      return state;
  }
}

export const store = createStore(rootReducer);

// 在各个应用的 webpack 配置中共享
shared: {
  'shared-state': {
    singleton: true,
    requiredVersion: require('../shared-state/package.json').version
  }
}

六、高级配置与优化

6.1 动态 Remote 配置

javascript 复制代码
// src/utils/dynamicRemotes.js
class DynamicRemoteLoader {
  constructor() {
    this.remotes = new Map();
  }

  async loadRemote(remoteName, remoteUrl) {
    if (this.remotes.has(remoteName)) {
      return this.remotes.get(remoteName);
    }

    try {
      await this.__loadRemoteEntry(remoteUrl);
      const container = window[remoteName];
      await container.init(__webpack_share_scopes__.default);
      this.remotes.set(remoteName, container);
      return container;
    } catch (error) {
      console.error(`Failed to load remote ${remoteName}:`, error);
      throw error;
    }
  }

  async __loadRemoteEntry(url) {
    return new Promise((resolve, reject) => {
      const script = document.createElement('script');
      script.src = url;
      script.onload = resolve;
      script.onerror = reject;
      document.head.appendChild(script);
    });
  }
}

export const remoteLoader = new DynamicRemoteLoader();

// 使用动态 Remote
const loadRemoteComponent = async (remoteName, modulePath) => {
  const remoteConfig = await fetch('/api/remotes-config').then(r => r.json());
  const remoteUrl = remoteConfig[remoteName];
  
  await remoteLoader.loadRemote(remoteName, remoteUrl);
  const container = window[remoteName];
  const factory = await container.get(modulePath);
  return factory();
};

6.2 性能优化配置

javascript 复制代码
// webpack.config.js 优化配置
new ModuleFederationPlugin({
  name: 'hostApp',
  remotes: {
    remoteApp: `promise new Promise(resolve => {
      const remoteUrl = new URL('./remoteEntry.js', import.meta.url);
      const script = document.createElement('script');
      script.src = remoteUrl;
      script.onload = () => {
        const proxy = {
          get: (request) => window.remoteApp.get(request),
          init: (arg) => {
            try {
              return window.remoteApp.init(arg);
            } catch (e) {
              console.log('Remote container already initialized');
            }
          }
        };
        resolve(proxy);
      };
      document.head.appendChild(script);
    })`,
  },
  shared: {
    react: {
      singleton: true,
      requiredVersion: deps.react,
      eager: false, // 延迟加载
    },
    'react-dom': {
      singleton: true,
      requiredVersion: deps['react-dom'],
      eager: false,
    },
  },
}),

七、开发环境与工具链

7.1 开发服务器配置

javascript 复制代码
// host-app/webpack.config.js
devServer: {
  port: 3000,
  historyApiFallback: true,
  headers: {
    'Access-Control-Allow-Origin': '*',
    'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, PATCH, OPTIONS',
    'Access-Control-Allow-Headers': 'X-Requested-With, content-type, Authorization',
  },
  allowedHosts: 'all',
},

// remote-app/webpack.config.js
devServer: {
  port: 3001,
  headers: {
    'Access-Control-Allow-Origin': '*',
    'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, PATCH, OPTIONS',
    'Access-Control-Allow-Headers': 'X-Requested-With, content-type, Authorization',
  },
},

7.2 TypeScript 支持

typescript 复制代码
// types/federation.d.ts
declare module '*.png';
declare module '*.jpg';
declare module '*.css';

declare module 'remoteApp/Button' {
  import { ComponentType } from 'react';
  interface ButtonProps {
    children: React.ReactNode;
    onClick?: () => void;
    variant?: 'primary' | 'secondary';
  }
  const Button: ComponentType<ButtonProps>;
  export default Button;
}

declare module 'remoteApp/Header' {
  import { ComponentType } from 'react';
  interface HeaderProps {
    title: string;
  }
  const Header: ComponentType<HeaderProps>;
  export default Header;
}

八、测试策略

8.1 单元测试配置

javascript 复制代码
// jest.config.js
module.exports = {
  moduleNameMapper: {
    '^remoteApp/(.*)$': '<rootDir>/__mocks__/remoteApp/$1',
  },
  testEnvironment: 'jsdom',
  setupFilesAfterEnv: ['<rootDir>/src/setupTests.js'],
};

// __mocks__/remoteApp/Button.js
import React from 'react';
const MockButton = ({ children, onClick }) => (
  <button onClick={onClick} data-testid="mock-button">
    {children}
  </button>
);
export default MockButton;

8.2 集成测试

javascript 复制代码
// src/__tests__/App.integration.js
import { render, screen, waitFor } from '@testing-library/react';
import * as RemoteApp from 'remoteApp/Button';

jest.mock('remoteApp/Button', () => ({
  __esModule: true,
  default: jest.fn(() => <button>Mock Remote Button</button>)
}));

describe('App Integration', () => {
  it('should load remote component', async () => {
    render(<App />);
    
    await waitFor(() => {
      expect(screen.getByText('Mock Remote Button')).toBeInTheDocument();
    });
    
    expect(RemoteApp.default).toHaveBeenCalled();
  });
});

九、部署与生产环境

9.1 生产环境配置

javascript 复制代码
// 环境特定的配置
const getRemoteUrl = (appName) => {
  const baseUrl = process.env.NODE_ENV === 'production' 
    ? 'https://cdn.yourcompany.com'
    : 'http://localhost';
  
  const ports = {
    remoteApp: process.env.NODE_ENV === 'production' ? '' : ':3001',
    authApp: process.env.NODE_ENV === 'production' ? '' : ':3002',
  };
  
  return `${baseUrl}${ports[appName]}/${appName}/remoteEntry.js`;
};

new ModuleFederationPlugin({
  remotes: {
    remoteApp: `remoteApp@${getRemoteUrl('remoteApp')}`,
    authApp: `authApp@${getRemoteUrl('authApp')}`,
  },
}),

9.2 CDN 部署策略

bash 复制代码
# 部署脚本示例
#!/bin/bash

# 构建各个应用
cd remote-app && npm run build && cd ..
cd host-app && npm run build && cd ..

# 上传到 CDN
aws s3 sync remote-app/dist/ s3://your-bucket/remote-app/ --delete
aws s3 sync host-app/dist/ s3://your-bucket/host-app/ --delete

# 刷新 CDN 缓存
aws cloudfront create-invalidation \
  --distribution-id YOUR_DISTRIBUTION_ID \
  --paths "/remote-app/*" "/host-app/*"

十、监控与错误处理

10.1 性能监控

javascript 复制代码
// src/utils/performanceMonitor.js
export const monitorRemoteLoading = () => {
  const originalGet = window.webpackChunkhostApp.get;
  
  window.webpackChunkhostApp.get = function(remoteName) {
    const startTime = performance.now();
    
    return originalGet.apply(this, arguments).then(container => {
      const loadTime = performance.now() - startTime;
      console.log(`Remote ${remoteName} loaded in ${loadTime}ms`);
      
      // 发送到监控系统
      sendMetricsToMonitoring({
        event: 'remote_loaded',
        remote: remoteName,
        duration: loadTime,
        timestamp: Date.now()
      });
      
      return container;
    });
  };
};

// 在应用启动时调用
monitorRemoteLoading();

10.2 错误追踪

javascript 复制代码
// src/utils/errorHandler.js
export const setupErrorHandling = () => {
  // 捕获远程模块加载错误
  window.addEventListener('error', (event) => {
    if (event.filename && event.filename.includes('remoteEntry.js')) {
      const remoteName = event.filename.split('/')[2];
      logError({
        type: 'REMOTE_LOAD_ERROR',
        remote: remoteName,
        message: event.error?.message,
        stack: event.error?.stack
      });
    }
  });

  // 捕获未处理的 Promise rejection
  window.addEventListener('unhandledrejection', (event) => {
    if (event.reason && event.reason.message.includes('Remote')) {
      logError({
        type: 'REMOTE_MODULE_ERROR',
        message: event.reason.message,
        stack: event.reason.stack
      });
    }
  });
};

// 初始化错误处理
setupErrorHandling();

十一、安全最佳实践

11.1 CSP 配置

html 复制代码
<!-- public/index.html -->
<meta http-equiv="Content-Security-Policy" 
      content="default-src 'self'; 
               script-src 'self' 'unsafe-inline' http://localhost:3001 http://localhost:3002;
               style-src 'self' 'unsafe-inline';
               connect-src 'self' http://localhost:3001 http://localhost:3002;">

11.2 安全头配置

javascript 复制代码
// Express 服务器配置
app.use(helmet({
  contentSecurityPolicy: {
    directives: {
      defaultSrc: ["'self'"],
      scriptSrc: ["'self'", "'unsafe-inline'", "trusted-cdn.com"],
      styleSrc: ["'self'", "'unsafe-inline'"],
      connectSrc: ["'self'", "api.yourdomain.com"],
    },
  },
  crossOriginEmbedderPolicy: false,
}));

十二、常见问题与解决方案

12.1 版本冲突解决

javascript 复制代码
// webpack.config.js
shared: {
  react: {
    singleton: true,
    requiredVersion: '^18.0.0',
    strictVersion: true,
    eager: true,
  },
  'react-dom': {
    singleton: true,
    requiredVersion: '^18.0.0',
    strictVersion: true,
    eager: true,
  },
}

12.2 循环依赖处理

javascript 复制代码
// 使用异步初始化避免循环依赖
new ModuleFederationPlugin({
  name: 'app1',
  exposes: { './Component': './src/Component' },
  shared: {
    react: { singleton: true, eager: false },
  },
}),

十三、总结

Module Federation 为微前端架构提供了强大的运行时模块共享能力,具有以下优势:

核心价值

  1. 真正的独立部署:各个应用可以独立开发、测试和部署
  2. 运行时集成:不需要重新构建整个应用
  3. 依赖共享:避免重复加载相同的依赖
  4. 技术栈无关:支持不同框架的应用集成
  5. 渐进式采用:可以逐步迁移到微前端架构

适用场景

  • 大型企业应用:多个团队协作开发的大型项目
  • 遗留系统迁移:逐步替换老旧系统
  • 多框架共存:React、Vue、Angular 混合开发
  • 独立部署需求:不同功能模块需要独立发布

决策考虑

项目需求 评估因素 团队结构 技术栈多样性 部署独立性需求 性能要求 多个独立团队 多种技术栈 独立部署需求 高性能要求 适合 Module Federation

相关推荐
安心不心安9 小时前
React Router 6 获取路由参数
前端·javascript·react.js
1024小神18 小时前
vue/react项目如何跳转到一个已经写好的html页面
vue.js·react.js·html
Maschera9621 小时前
扣子同款半固定输入模板的简单解决方案
前端·react.js
webKity1 天前
React 的基本概念介绍
javascript·react.js
YuspTLstar1 天前
从工作原理入手理解React一:React核心内容和基本使用
react.js
__M__1 天前
Zalo Mini App 初体验
前端·react.js
程序员小续1 天前
告别重复造轮子!看 ahooks 如何改变你的代码结构
前端·javascript·react.js
大力yy1 天前
从零到一:VS Code 扩展开发全流程简介(含 Webview 与 React 集成)
前端·javascript·react.js
OEC小胖胖1 天前
掌握表单:React中的受控组件与表单处理
前端·javascript·react.js·前端框架·react·web