微前端架构设计与实践

深入探索微前端架构的设计理念、Module Federation技术实现、应用隔离和通信机制,以及实际落地的最佳实践

2024年2月18日
10 min read
微前端架构设计模块联邦前端工程化

前言

随着前端应用规模的增长,单体应用的维护成本越来越高。微前端架构通过将大型应用拆分为多个独立的小应用,实现了团队自治、技术栈灵活和独立部署。本文将深入探讨微前端的设计理念和实践方案。

微前端核心概念

什么是微前端

微前端是一种将多个可独立交付的前端应用组合成一个整体的架构风格。每个前端应用可以:

  • 独立开发和部署
  • 使用不同的技术栈
  • 由不同团队维护
  • 在运行时组合

适用场景

// ✅ 适合使用微前端的场景
const goodScenarios = [
  '大型企业应用,多团队协作',
  '需要渐进式重构遗留系统',
  '不同模块使用不同技术栈',
  '希望独立部署各个模块',
  '组织结构分散,团队自治'
]
 
// ❌ 不适合使用微前端的场景
const badScenarios = [
  '小型应用,单一团队',
  '团队技术栈统一',
  '模块间高度耦合',
  '性能要求极高',
  '团队缺乏微服务经验'
]

Module Federation详解

Webpack 5 Module Federation

Module Federation是Webpack 5引入的革命性功能,允许多个独立构建的应用共享代码。

// host/webpack.config.js(主应用配置)
const { ModuleFederationPlugin } = require('webpack').container
 
module.exports = {
  plugins: [
    new ModuleFederationPlugin({
      name: 'host',
      remotes: {
        // 远程应用配置
        app1: 'app1@http://localhost:3001/remoteEntry.js',
        app2: 'app2@http://localhost:3002/remoteEntry.js',
      },
      shared: {
        // 共享依赖
        react: { singleton: true, requiredVersion: '^18.0.0' },
        'react-dom': { singleton: true, requiredVersion: '^18.0.0' },
      },
    }),
  ],
}
 
// app1/webpack.config.js(子应用配置)
module.exports = {
  plugins: [
    new ModuleFederationPlugin({
      name: 'app1',
      filename: 'remoteEntry.js',
      exposes: {
        // 暴露的模块
        './App': './src/App',
        './Dashboard': './src/pages/Dashboard',
      },
      shared: {
        react: { singleton: true },
        'react-dom': { singleton: true },
      },
    }),
  },
}

动态加载远程模块

// host/src/App.tsx
import React, { Suspense, lazy } from 'react'
 
// 动态导入远程模块
const RemoteDashboard = lazy(() => import('app1/Dashboard'))
const RemoteSettings = lazy(() => import('app2/Settings'))
 
function App() {
  return (
    <div>
      <h1>主应用</h1>
 
      <Suspense fallback={<div>加载中...</div>}>
        <RemoteDashboard />
      </Suspense>
 
      <Suspense fallback={<div>加载中...</div>}>
        <RemoteSettings />
      </Suspense>
    </div>
  )
}
 
export default App

错误边界处理

// components/RemoteAppErrorBoundary.tsx
import React, { Component, ReactNode } from 'react'
 
interface Props {
  children: ReactNode
  fallback?: ReactNode
}
 
interface State {
  hasError: boolean
  error?: Error
}
 
export class RemoteAppErrorBoundary extends Component<Props, State> {
  constructor(props: Props) {
    super(props)
    this.state = { hasError: false }
  }
 
  static getDerivedStateFromError(error: Error): State {
    return { hasError: true, error }
  }
 
  componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
    console.error('远程模块加载失败:', error, errorInfo)
  }
 
  render() {
    if (this.state.hasError) {
      return (
        this.props.fallback || (
          <div className="error-container">
            <h2>模块加载失败</h2>
            <p>{this.state.error?.message}</p>
            <button onClick={() => window.location.reload()}>
              重新加载
            </button>
          </div>
        )
      )
    }
 
    return this.props.children
  }
}
 
// 使用
function App() {
  return (
    <RemoteAppErrorBoundary>
      <Suspense fallback={<Loading />}>
        <RemoteDashboard />
      </Suspense>
    </RemoteAppErrorBoundary>
  )
}

应用隔离和通信

1. 样式隔离

// 方案1:CSS Modules
// app1/src/Dashboard.module.css
.container {
  padding: 20px;
}
 
// app1/src/Dashboard.tsx
import styles from './Dashboard.module.css'
 
export function Dashboard() {
  return <div className={styles.container}>Dashboard</div>
}
 
// 方案2:CSS-in-JS
import styled from 'styled-components'
 
const Container = styled.div`
  padding: 20px;
  background: #fff;
`
 
// 方案3:Shadow DOM
class MicroApp extends HTMLElement {
  connectedCallback() {
    const shadow = this.attachShadow({ mode: 'open' })
    const style = document.createElement('style')
    style.textContent = `
      .container { padding: 20px; }
    `
    shadow.appendChild(style)
  }
}
 
customElements.define('micro-app', MicroApp)

2. JavaScript隔离

// utils/sandbox.ts
export class Sandbox {
  private proxy: WindowProxy
  private fakeWindow: any
 
  constructor() {
    this.fakeWindow = {}
    this.proxy = new Proxy(this.fakeWindow, {
      get: (target, prop) => {
        // 优先从沙箱获取
        if (prop in target) {
          return target[prop]
        }
        // 其次从全局获取
        return (window as any)[prop]
      },
      set: (target, prop, value) => {
        // 所有设置都在沙箱内
        target[prop] = value
        return true
      },
    })
  }
 
  exec(code: string) {
    // 在沙箱环境中执行代码
    const func = new Function('window', code)
    return func.call(this.proxy, this.proxy)
  }
 
  destroy() {
    this.fakeWindow = {}
  }
}
 
// 使用沙箱
const sandbox = new Sandbox()
sandbox.exec(`
  window.myVar = 'isolated'
  console.log(window.myVar)
`)

3. 应用间通信

// shared/eventBus.ts
type EventCallback = (...args: any[]) => void
 
class EventBus {
  private events: Map<string, EventCallback[]> = new Map()
 
  // 订阅事件
  on(event: string, callback: EventCallback) {
    if (!this.events.has(event)) {
      this.events.set(event, [])
    }
    this.events.get(event)!.push(callback)
 
    // 返回取消订阅函数
    return () => this.off(event, callback)
  }
 
  // 取消订阅
  off(event: string, callback: EventCallback) {
    const callbacks = this.events.get(event)
    if (callbacks) {
      const index = callbacks.indexOf(callback)
      if (index > -1) {
        callbacks.splice(index, 1)
      }
    }
  }
 
  // 发布事件
  emit(event: string, ...args: any[]) {
    const callbacks = this.events.get(event)
    if (callbacks) {
      callbacks.forEach((callback) => callback(...args))
    }
  }
 
  // 清空所有事件
  clear() {
    this.events.clear()
  }
}
 
export const eventBus = new EventBus()
 
// app1使用
import { eventBus } from '@shared/eventBus'
 
eventBus.on('user:login', (user) => {
  console.log('用户登录:', user)
})
 
eventBus.emit('user:login', { id: 1, name: 'John' })
 
// app2使用
import { eventBus } from '@shared/eventBus'
 
eventBus.on('user:login', (user) => {
  // 更新用户信息显示
  updateUserProfile(user)
})

4. 共享状态管理

// shared/store.ts
import create from 'zustand'
 
interface GlobalStore {
  user: User | null
  theme: 'light' | 'dark'
  setUser: (user: User | null) => void
  setTheme: (theme: 'light' | 'dark') => void
}
 
// 创建全局共享store
export const useGlobalStore = create<GlobalStore>((set) => ({
  user: null,
  theme: 'light',
  setUser: (user) => set({ user }),
  setTheme: (theme) => set({ theme }),
}))
 
// 在主应用中使用
// host/src/App.tsx
import { useGlobalStore } from '@shared/store'
 
function Host() {
  const { user, setUser } = useGlobalStore()
 
  return (
    <div>
      <button onClick={() => setUser({ id: 1, name: 'John' })}>
        设置用户
      </button>
    </div>
  )
}
 
// 在子应用中使用
// app1/src/Dashboard.tsx
import { useGlobalStore } from '@shared/store'
 
function Dashboard() {
  const user = useGlobalStore((state) => state.user)
 
  return <div>欢迎, {user?.name}</div>
}

路由管理

主应用路由配置

// host/src/App.tsx
import { BrowserRouter, Routes, Route, Link } from 'react-router-dom'
import { lazy, Suspense } from 'react'
 
const App1 = lazy(() => import('app1/App'))
const App2 = lazy(() => import('app2/App'))
 
function App() {
  return (
    <BrowserRouter>
      <nav>
        <Link to="/app1">应用1</Link>
        <Link to="/app2">应用2</Link>
      </nav>
 
      <Suspense fallback={<div>加载中...</div>}>
        <Routes>
          <Route path="/app1/*" element={<App1 />} />
          <Route path="/app2/*" element={<App2 />} />
          <Route path="/" element={<Home />} />
        </Routes>
      </Suspense>
    </BrowserRouter>
  )
}

子应用路由配置

// app1/src/App.tsx
import { Routes, Route, useNavigate } from 'react-router-dom'
 
function App() {
  return (
    <Routes>
      <Route path="/" element={<Dashboard />} />
      <Route path="/settings" element={<Settings />} />
      <Route path="/profile" element={<Profile />} />
    </Routes>
  )
}
 
export default App

部署和CI/CD

独立部署配置

# app1/.github/workflows/deploy.yml
name: Deploy App1
 
on:
  push:
    branches: [main]
    paths:
      - 'apps/app1/**'
 
jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
 
      - name: Install dependencies
        run: npm install
        working-directory: ./apps/app1
 
      - name: Build
        run: npm run build
        working-directory: ./apps/app1
        env:
          PUBLIC_URL: https://cdn.example.com/app1
 
      - name: Deploy to CDN
        run: |
          aws s3 sync ./dist s3://micro-frontend/app1
          aws cloudfront create-invalidation --distribution-id XXX --paths "/app1/*"
        working-directory: ./apps/app1

Nginx配置

# nginx.conf
server {
    listen 80;
    server_name example.com;
 
    # 主应用
    location / {
        root /var/www/host;
        try_files $uri $uri/ /index.html;
    }
 
    # 子应用1
    location /app1/ {
        proxy_pass http://app1-service:3001/;
    }
 
    # 子应用2
    location /app2/ {
        proxy_pass http://app2-service:3002/;
    }
 
    # 静态资源
    location /static/ {
        alias /var/www/static/;
        expires 1y;
        add_header Cache-Control "public, immutable";
    }
}

性能优化

1. 共享依赖优化

// webpack.config.js
new ModuleFederationPlugin({
  shared: {
    react: {
      singleton: true,
      requiredVersion: '^18.0.0',
      eager: false, // 懒加载
    },
    'react-dom': {
      singleton: true,
      requiredVersion: '^18.0.0',
      eager: false,
    },
    // 共享工具库
    lodash: {
      singleton: true,
      eager: false,
    },
  },
})

2. 预加载策略

// utils/prefetch.ts
export function prefetchApp(appName: string, moduleUrl: string) {
  const link = document.createElement('link')
  link.rel = 'prefetch'
  link.href = moduleUrl
  document.head.appendChild(link)
}
 
// 预加载子应用
useEffect(() => {
  // 在用户可能访问时预加载
  prefetchApp('app1', 'http://localhost:3001/remoteEntry.js')
}, [])

3. 按需加载

// 使用路由懒加载
const Dashboard = lazy(() =>
  import('app1/Dashboard').catch(() => {
    return { default: () => <div>加载失败</div> }
  })
)

监控和调试

// utils/monitoring.ts
export function trackModuleLoad(moduleName: string) {
  const startTime = performance.now()
 
  return {
    success: () => {
      const loadTime = performance.now() - startTime
      console.log(`模块 ${moduleName} 加载成功,耗时: ${loadTime}ms`)
 
      // 上报性能数据
      sendMetrics({
        type: 'module_load',
        module: moduleName,
        duration: loadTime,
        success: true,
      })
    },
    error: (error: Error) => {
      console.error(`模块 ${moduleName} 加载失败:`, error)
 
      sendMetrics({
        type: 'module_load',
        module: moduleName,
        success: false,
        error: error.message,
      })
    },
  }
}
 
// 使用
const tracker = trackModuleLoad('app1/Dashboard')
import('app1/Dashboard')
  .then(() => tracker.success())
  .catch((error) => tracker.error(error))

最佳实践

  1. 明确边界:清晰定义各应用职责,避免功能重叠
  2. 版本管理:使用语义化版本,确保兼容性
  3. 依赖共享:合理共享公共依赖,减少重复加载
  4. 独立部署:确保各应用可独立部署和回滚
  5. 监控告警:建立完善的监控体系
  6. 文档规范:维护清晰的接口文档和使用指南

总结

微前端架构为大型前端应用提供了有效的解决方案。通过Module Federation等技术,我们可以实现真正的应用隔离和独立部署。但同时也要注意,微前端会带来额外的复杂度,需要根据实际情况权衡利弊。关键是找到适合团队和项目的实施方案,不要为了微前端而微前端。