Vue 3 微前端架构实践:从 Module Federation 到生产部署

约 22 分钟阅读

引言:微前端时代的到来

随着企业应用规模的不断扩大,传统的单体前端架构已经难以满足多团队协作、技术栈多样化和快速迭代的需求。微前端架构作为解决方案,让我们能够将大型应用拆分为多个独立的、可独立开发和部署的前端应用。

本文将以 Vue 3 为核心,全面介绍微前端架构的设计原理、技术实现和生产级部署实践。

1. 微前端架构设计原则

1.1 核心设计理念

typescript
// types/micro-frontend.ts
interface MicroFrontendConfig {
  name: string
  entry: string
  container: string
  activeRule: string | ((location: Location) => boolean)
  props?: Record<string, any>
}

interface MicroFrontendApp {
  // 应用元信息
  metadata: {
    name: string
    version: string
    description: string
    maintainers: string[]
    dependencies: Record<string, string>
  }
  
  // 生命周期钩子
  lifecycle: {
    bootstrap: () => Promise<void>
    mount: (props: any) => Promise<void>
    unmount: (props: any) => Promise<void>
    update?: (props: any) => Promise<void>
  }
  
  // 路由配置
  routes: {
    base: string
    routes: Array<{
      path: string
      component: any
      meta?: Record<string, any>
    }>
  }
  
  // 共享资源
  shared: {
    dependencies: string[]
    components: Record<string, any>
    utils: Record<string, any>
  }
}

// 微前端架构原则
export const MICRO_FRONTEND_PRINCIPLES = {
  技术栈无关: '每个子应用可以使用不同的技术栈',
  独立开发: '团队可以独立开发、测试和部署',
  增量升级: '可以逐步迁移和升级各个子应用',
  运行时集成: '在浏览器运行时动态加载和集成',
  样式隔离: '避免子应用间的样式冲突',
  状态隔离: '子应用状态相互独立,通过明确接口通信'
} as const

1.2 架构模式选择

typescript
// config/architecture-patterns.ts
export const ARCHITECTURE_PATTERNS = {
  // 1. 路由分发模式
  routeDistribution: {
    description: '基于路由规则分发到不同子应用',
    适用场景: '功能边界清晰的大型应用',
    优点: ['简单易理解', '性能好', 'SEO友好'],
    缺点: ['页面级集成', '共享状态复杂']
  },
  
  // 2. 组件集成模式  
  componentIntegration: {
    description: '将子应用作为组件集成到主应用',
    适用场景: '需要细粒度集成的应用',
    优点: ['灵活性高', '组件级复用', '集成度高'],
    缺点: ['复杂度高', '性能开销', '版本管理难']
  },
  
  // 3. iframe 模式
  iframeBased: {
    description: '使用 iframe 隔离子应用',
    适用场景: '强隔离需求、遗留系统集成',
    优点: ['完全隔离', '简单实现', '安全性好'],
    缺点: ['用户体验差', '通信复杂', 'SEO不友好']
  }
}

2. Module Federation 实现方案

2.1 主应用配置

typescript
// main-app/vite.config.ts
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import { federation } from '@originjs/vite-plugin-federation'

export default defineConfig({
  plugins: [
    vue(),
    federation({
      name: 'main-app',
      remotes: {
        'user-app': 'http://localhost:3001/assets/remoteEntry.js',
        'product-app': 'http://localhost:3002/assets/remoteEntry.js',
        'order-app': 'http://localhost:3003/assets/remoteEntry.js'
      },
      shared: {
        vue: {
          singleton: true,
          eager: true,
          requiredVersion: '^3.3.0'
        },
        'vue-router': {
          singleton: true,
          eager: true
        },
        pinia: {
          singleton: true,
          eager: true
        },
        'element-plus': {
          singleton: true
        }
      }
    })
  ],
  
  build: {
    target: 'esnext',
    minify: false,
    cssCodeSplit: false
  }
})
vue
<!-- main-app/src/App.vue -->
<script setup lang="ts">
import { ref, onMounted, defineAsyncComponent } from 'vue'
import { useRouter } from 'vue-router'
import { useMicroFrontend } from '@/composables/useMicroFrontend'

// 动态加载微前端应用
const UserApp = defineAsyncComponent(() => import('user-app/App'))
const ProductApp = defineAsyncComponent(() => import('product-app/App'))
const OrderApp = defineAsyncComponent(() => import('order-app/App'))

const router = useRouter()
const { registerApp, getSharedStore, broadcastMessage } = useMicroFrontend()

// 应用注册配置
const microApps = [
  {
    name: 'user-app',
    component: UserApp,
    route: '/users',
    props: { apiBase: '/api/users' }
  },
  {
    name: 'product-app', 
    component: ProductApp,
    route: '/products',
    props: { apiBase: '/api/products' }
  },
  {
    name: 'order-app',
    component: OrderApp,
    route: '/orders', 
    props: { apiBase: '/api/orders' }
  }
]

onMounted(() => {
  // 注册所有微前端应用
  microApps.forEach(app => {
    registerApp(app.name, {
      component: app.component,
      route: app.route,
      props: app.props
    })
  })
})

// 全局事件处理
const handleGlobalEvent = (event: CustomEvent) => {
  const { type, data, source } = event.detail
  
  switch (type) {
    case 'NAVIGATE':
      router.push(data.path)
      break
    case 'UPDATE_USER':
      broadcastMessage('USER_UPDATED', data, source)
      break
    case 'LOGOUT':
      // 通知所有子应用用户已登出
      broadcastMessage('USER_LOGOUT', {}, source)
      router.push('/login')
      break
  }
}

onMounted(() => {
  window.addEventListener('micro-frontend-event', handleGlobalEvent)
})

onUnmounted(() => {
  window.removeEventListener('micro-frontend-event', handleGlobalEvent)
})
</script>

<template>
  <div id="main-app">
    <header class="main-header">
      <nav class="main-nav">
        <router-link to="/dashboard">首页</router-link>
        <router-link to="/users">用户管理</router-link>
        <router-link to="/products">产品管理</router-link>
        <router-link to="/orders">订单管理</router-link>
      </nav>
    </header>
    
    <main class="main-content">
      <router-view />
    </main>
    
    <footer class="main-footer">
      <p>&copy; 2024 企业管理系统</p>
    </footer>
  </div>
</template>

<style scoped>
.main-app {
  display: flex;
  flex-direction: column;
  min-height: 100vh;
}

.main-header {
  background: #2c3e50;
  color: white;
  padding: 1rem;
}

.main-nav {
  display: flex;
  gap: 1rem;
}

.main-nav a {
  color: white;
  text-decoration: none;
  padding: 0.5rem 1rem;
  border-radius: 4px;
}

.main-nav a:hover,
.main-nav a.router-link-active {
  background: #34495e;
}

.main-content {
  flex: 1;
  padding: 2rem;
}
</style>

2.2 子应用配置

typescript
// user-app/vite.config.ts
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import { federation } from '@originjs/vite-plugin-federation'

export default defineConfig({
  plugins: [
    vue(),
    federation({
      name: 'user-app',
      filename: 'remoteEntry.js',
      exposes: {
        './App': './src/App.vue',
        './UserList': './src/components/UserList.vue',
        './UserDetail': './src/components/UserDetail.vue'
      },
      shared: {
        vue: {
          singleton: true,
          eager: true,
          requiredVersion: '^3.3.0'
        },
        'vue-router': {
          singleton: true,
          eager: true
        },
        pinia: {
          singleton: true,
          eager: true
        }
      }
    })
  ],
  
  server: {
    port: 3001,
    cors: true
  },
  
  build: {
    target: 'esnext',
    minify: false,
    cssCodeSplit: false
  }
})
vue
<!-- user-app/src/App.vue -->
<script setup lang="ts">
import { provide, onMounted, onUnmounted } from 'vue'
import { createRouter, createWebHistory } from 'vue-router'
import { createPinia } from 'pinia'
import UserList from './components/UserList.vue'
import UserDetail from './components/UserDetail.vue'
import { useMicroFrontendBridge } from './composables/useMicroFrontendBridge'

// 接收主应用传递的 props
interface AppProps {
  apiBase?: string
  theme?: string
  permissions?: string[]
}

const props = withDefaults(defineProps<AppProps>(), {
  apiBase: '/api/users',
  theme: 'default',
  permissions: () => []
})

// 创建子应用路由
const router = createRouter({
  history: createWebHistory('/users'),
  routes: [
    { 
      path: '/', 
      name: 'UserList',
      component: UserList 
    },
    { 
      path: '/:id', 
      name: 'UserDetail',
      component: UserDetail,
      props: true
    }
  ]
})

// 创建状态管理
const pinia = createPinia()

// 微前端通信桥梁
const { emitToMain, subscribeFromMain, unsubscribe } = useMicroFrontendBridge()

// 提供全局配置
provide('appConfig', {
  apiBase: props.apiBase,
  theme: props.theme,
  permissions: props.permissions
})

// 监听主应用消息
const handleMainMessage = (type: string, data: any) => {
  switch (type) {
    case 'USER_LOGOUT':
      // 清理用户相关状态
      router.push('/')
      break
    case 'THEME_CHANGE':
      // 更新主题
      document.documentElement.setAttribute('data-theme', data.theme)
      break
  }
}

onMounted(() => {
  subscribeFromMain(handleMainMessage)
  
  // 通知主应用子应用已加载
  emitToMain('APP_LOADED', { 
    name: 'user-app',
    version: '1.0.0',
    routes: router.getRoutes().map(r => r.path)
  })
})

onUnmounted(() => {
  unsubscribe()
  emitToMain('APP_UNLOADED', { name: 'user-app' })
})
</script>

<template>
  <div class="user-app" :data-theme="theme">
    <div class="user-app-header">
      <h2>用户管理系统</h2>
      <div class="user-app-actions">
        <button @click="emitToMain('NAVIGATE', { path: '/dashboard' })">
          返回首页
        </button>
      </div>
    </div>
    
    <div class="user-app-content">
      <router-view />
    </div>
  </div>
</template>

<style scoped>
.user-app {
  border: 2px solid #e1e8ed;
  border-radius: 8px;
  padding: 1rem;
  background: white;
}

.user-app-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 1rem;
  padding-bottom: 1rem;
  border-bottom: 1px solid #e1e8ed;
}

.user-app-content {
  min-height: 400px;
}

/* 主题样式 */
.user-app[data-theme="dark"] {
  background: #1a1a1a;
  color: white;
  border-color: #333;
}
</style>

3. Single-SPA 集成方案

3.1 主应用 Single-SPA 配置

typescript
// main-app/src/main.ts
import { createApp } from 'vue'
import { registerApplication, start } from 'single-spa'
import App from './App.vue'
import { createSingleSpaVue } from 'single-spa-vue'

// 主应用启动
const app = createApp(App)
app.mount('#main-app')

// 注册微前端应用
registerApplication({
  name: 'user-management',
  app: () => System.import('user-app'),
  activeWhen: ['/users'],
  customProps: {
    apiBase: '/api/users',
    mountParcel: true
  }
})

registerApplication({
  name: 'product-management', 
  app: () => System.import('product-app'),
  activeWhen: ['/products'],
  customProps: {
    apiBase: '/api/products'
  }
})

registerApplication({
  name: 'order-management',
  app: () => System.import('order-app'), 
  activeWhen: (location) => location.pathname.startsWith('/orders'),
  customProps: {
    apiBase: '/api/orders'
  }
})

// 启动 single-spa
start({
  urlRerouteOnly: true
})

3.2 子应用 Single-SPA 适配

typescript
// user-app/src/main.ts
import { createApp, type App as VueApp } from 'vue'
import { createRouter, createWebHistory } from 'vue-router'
import { createPinia } from 'pinia'
import singleSpaVue from 'single-spa-vue'
import App from './App.vue'
import { routes } from './router'

const vueLifecycles = singleSpaVue({
  createApp,
  appOptions: {
    render() {
      return h(App, {
        // 接收 single-spa 传递的 props
        ...this.$props
      })
    }
  },
  handleInstance(app: VueApp, props: any) {
    // 创建路由实例
    const router = createRouter({
      history: createWebHistory(props.base || '/users'),
      routes
    })
    
    // 创建状态管理
    const pinia = createPinia()
    
    app.use(router)
    app.use(pinia)
    
    // 设置全局属性
    app.config.globalProperties.$props = props
  }
})

export const { bootstrap, mount, unmount } = vueLifecycles

// 独立运行模式(开发环境)
if (!window.singleSpaNavigate) {
  delete window.singleSpaNavigate
  
  const app = createApp(App)
  const router = createRouter({
    history: createWebHistory('/users'),
    routes
  })
  const pinia = createPinia()
  
  app.use(router)
  app.use(pinia)
  app.mount('#app')
}

4. 跨应用通信机制

4.1 事件总线模式

typescript
// shared/event-bus.ts
interface EventPayload {
  type: string
  data: any
  source: string
  timestamp: number
  id: string
}

class MicroFrontendEventBus {
  private listeners = new Map<string, Set<Function>>()
  private history: EventPayload[] = []
  private maxHistorySize = 100

  // 发布事件
  emit(type: string, data: any, source: string = 'unknown') {
    const payload: EventPayload = {
      type,
      data,
      source,
      timestamp: Date.now(),
      id: this.generateId()
    }

    // 存储到历史记录
    this.addToHistory(payload)

    // 通知监听器
    const typeListeners = this.listeners.get(type)
    if (typeListeners) {
      typeListeners.forEach(listener => {
        try {
          listener(payload)
        } catch (error) {
          console.error(`Error in event listener for ${type}:`, error)
        }
      })
    }

    // 通知通用监听器
    const allListeners = this.listeners.get('*')
    if (allListeners) {
      allListeners.forEach(listener => {
        try {
          listener(payload)
        } catch (error) {
          console.error('Error in universal event listener:', error)
        }
      })
    }

    // 发送自定义事件到 window
    window.dispatchEvent(new CustomEvent('micro-frontend-event', {
      detail: payload
    }))
  }

  // 订阅事件
  on(type: string, listener: (payload: EventPayload) => void) {
    if (!this.listeners.has(type)) {
      this.listeners.set(type, new Set())
    }
    this.listeners.get(type)!.add(listener)

    // 返回取消订阅函数
    return () => {
      const typeListeners = this.listeners.get(type)
      if (typeListeners) {
        typeListeners.delete(listener)
        if (typeListeners.size === 0) {
          this.listeners.delete(type)
        }
      }
    }
  }

  // 一次性监听
  once(type: string, listener: (payload: EventPayload) => void) {
    const unsubscribe = this.on(type, (payload) => {
      listener(payload)
      unsubscribe()
    })
    return unsubscribe
  }

  // 移除监听器
  off(type: string, listener?: Function) {
    if (!listener) {
      this.listeners.delete(type)
    } else {
      const typeListeners = this.listeners.get(type)
      if (typeListeners) {
        typeListeners.delete(listener)
      }
    }
  }

  // 获取事件历史
  getHistory(type?: string, limit: number = 10): EventPayload[] {
    let filtered = this.history
    
    if (type) {
      filtered = this.history.filter(event => event.type === type)
    }
    
    return filtered.slice(-limit)
  }

  // 清空历史
  clearHistory() {
    this.history = []
  }

  private addToHistory(payload: EventPayload) {
    this.history.push(payload)
    if (this.history.length > this.maxHistorySize) {
      this.history = this.history.slice(-this.maxHistorySize)
    }
  }

  private generateId(): string {
    return `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`
  }
}

export const eventBus = new MicroFrontendEventBus()

// Vue 组合式函数
export function useEventBus() {
  const emit = (type: string, data: any) => {
    eventBus.emit(type, data, getCurrentInstance()?.appContext.app.config.globalProperties.$appName || 'unknown')
  }

  const on = (type: string, listener: (payload: EventPayload) => void) => {
    return eventBus.on(type, listener)
  }

  const once = (type: string, listener: (payload: EventPayload) => void) => {
    return eventBus.once(type, listener)
  }

  return {
    emit,
    on,
    once,
    off: eventBus.off.bind(eventBus),
    getHistory: eventBus.getHistory.bind(eventBus)
  }
}

4.2 共享状态管理

typescript
// shared/global-store.ts
import { reactive, readonly } from 'vue'
import { defineStore } from 'pinia'

// 全局共享状态接口
interface GlobalState {
  user: {
    id: string | null
    name: string
    email: string
    permissions: string[]
    isAuthenticated: boolean
  }
  
  theme: {
    mode: 'light' | 'dark'
    primaryColor: string
    language: string
  }
  
  navigation: {
    currentApp: string
    breadcrumb: Array<{ label: string; path: string }>
    activeMenus: string[]
  }
  
  notifications: Array<{
    id: string
    type: 'info' | 'success' | 'warning' | 'error'
    title: string
    message: string
    timestamp: number
    read: boolean
  }>
}

// 创建全局状态
const globalState = reactive<GlobalState>({
  user: {
    id: null,
    name: '',
    email: '',
    permissions: [],
    isAuthenticated: false
  },
  
  theme: {
    mode: 'light',
    primaryColor: '#409eff',
    language: 'zh-CN'
  },
  
  navigation: {
    currentApp: '',
    breadcrumb: [],
    activeMenus: []
  },
  
  notifications: []
})

// 全局状态管理器
class GlobalStateManager {
  private state = globalState
  private subscribers = new Set<(state: GlobalState) => void>()

  // 获取只读状态
  getState() {
    return readonly(this.state)
  }

  // 更新用户信息
  updateUser(userData: Partial<GlobalState['user']>) {
    Object.assign(this.state.user, userData)
    this.notify()
  }

  // 更新主题
  updateTheme(themeData: Partial<GlobalState['theme']>) {
    Object.assign(this.state.theme, themeData)
    this.notify()
    
    // 通知所有子应用主题变更
    eventBus.emit('THEME_CHANGE', themeData)
  }

  // 更新导航状态
  updateNavigation(navData: Partial<GlobalState['navigation']>) {
    Object.assign(this.state.navigation, navData)
    this.notify()
  }

  // 添加通知
  addNotification(notification: Omit<GlobalState['notifications'][0], 'id' | 'timestamp' | 'read'>) {
    const newNotification = {
      ...notification,
      id: `notification-${Date.now()}`,
      timestamp: Date.now(),
      read: false
    }
    
    this.state.notifications.unshift(newNotification)
    this.notify()
    
    return newNotification.id
  }

  // 标记通知为已读
  markNotificationRead(id: string) {
    const notification = this.state.notifications.find(n => n.id === id)
    if (notification) {
      notification.read = true
      this.notify()
    }
  }

  // 清空通知
  clearNotifications() {
    this.state.notifications = []
    this.notify()
  }

  // 订阅状态变化
  subscribe(callback: (state: GlobalState) => void) {
    this.subscribers.add(callback)
    
    return () => {
      this.subscribers.delete(callback)
    }
  }

  // 通知订阅者
  private notify() {
    this.subscribers.forEach(callback => {
      try {
        callback(this.state)
      } catch (error) {
        console.error('Error in global state subscriber:', error)
      }
    })
  }
}

export const globalStateManager = new GlobalStateManager()

// Pinia Store
export const useGlobalStore = defineStore('global', () => {
  const state = globalStateManager.getState()

  const updateUser = (userData: Partial<GlobalState['user']>) => {
    globalStateManager.updateUser(userData)
  }

  const updateTheme = (themeData: Partial<GlobalState['theme']>) => {
    globalStateManager.updateTheme(themeData)
  }

  const updateNavigation = (navData: Partial<GlobalState['navigation']>) => {
    globalStateManager.updateNavigation(navData)
  }

  const addNotification = (notification: Omit<GlobalState['notifications'][0], 'id' | 'timestamp' | 'read'>) => {
    return globalStateManager.addNotification(notification)
  }

  return {
    state,
    updateUser,
    updateTheme,
    updateNavigation,
    addNotification,
    markNotificationRead: globalStateManager.markNotificationRead.bind(globalStateManager),
    clearNotifications: globalStateManager.clearNotifications.bind(globalStateManager)
  }
})

5. 样式隔离方案

5.1 CSS-in-JS 方案

typescript
// utils/styled-components.ts
import { computed, type Ref } from 'vue'

interface StyleConfig {
  prefix: string
  theme: Ref<any>
  scoped: boolean
}

class StyledComponentsManager {
  private prefix: string
  private theme: Ref<any>
  private scoped: boolean
  private styleCache = new Map<string, string>()

  constructor(config: StyleConfig) {
    this.prefix = config.prefix
    this.theme = config.theme
    this.scoped = config.scoped
  }

  // 创建样式类
  createStyles(styles: Record<string, any>) {
    const classNames: Record<string, string> = {}
    
    Object.entries(styles).forEach(([key, value]) => {
      const className = this.generateClassName(key)
      const cssText = this.processCSSText(value, className)
      
      this.injectStyles(cssText)
      classNames[key] = className
    })
    
    return classNames
  }

  // 主题感知样式
  createThemedStyles(styleFactory: (theme: any) => Record<string, any>) {
    return computed(() => {
      const styles = styleFactory(this.theme.value)
      return this.createStyles(styles)
    })
  }

  // 响应式样式
  createResponsiveStyles(breakpoints: Record<string, string>, styles: Record<string, Record<string, any>>) {
    const mediaQueries = Object.entries(breakpoints).map(([name, query]) => {
      const mediaStyles = styles[name]
      if (!mediaStyles) return ''
      
      const cssRules = Object.entries(mediaStyles).map(([selector, rules]) => {
        const className = this.generateClassName(selector)
        const cssText = this.processCSSText(rules, className)
        return cssText
      }).join('\n')
      
      return `@media ${query} {\n${cssRules}\n}`
    }).join('\n')
    
    this.injectStyles(mediaQueries)
  }

  private generateClassName(key: string): string {
    const hash = this.hashCode(key + this.prefix)
    return `${this.prefix}-${key}-${Math.abs(hash).toString(36)}`
  }

  private processCSSText(styles: any, className: string): string {
    const cssProperties = Object.entries(styles).map(([prop, value]) => {
      const kebabProp = prop.replace(/([A-Z])/g, '-$1').toLowerCase()
      return `  ${kebabProp}: ${value};`
    }).join('\n')
    
    return `.${className} {\n${cssProperties}\n}`
  }

  private injectStyles(cssText: string) {
    if (this.styleCache.has(cssText)) return
    
    const styleElement = document.createElement('style')
    styleElement.textContent = cssText
    
    if (this.scoped) {
      styleElement.setAttribute('data-app', this.prefix)
    }
    
    document.head.appendChild(styleElement)
    this.styleCache.set(cssText, 'injected')
  }

  private hashCode(str: string): number {
    let hash = 0
    for (let i = 0; i < str.length; i++) {
      const char = str.charCodeAt(i)
      hash = ((hash << 5) - hash) + char
      hash = hash & hash // Convert to 32bit integer
    }
    return hash
  }
}

// Vue 组合式函数
export function useStyledComponents(appName: string) {
  const theme = inject('theme', ref({}))
  
  const manager = new StyledComponentsManager({
    prefix: `mf-${appName}`,
    theme,
    scoped: true
  })

  const createStyles = (styles: Record<string, any>) => {
    return manager.createStyles(styles)
  }

  const createThemedStyles = (styleFactory: (theme: any) => Record<string, any>) => {
    return manager.createThemedStyles(styleFactory)
  }

  return {
    createStyles,
    createThemedStyles,
    createResponsiveStyles: manager.createResponsiveStyles.bind(manager)
  }
}

5.2 CSS Module 隔离

vue
<!-- components/IsolatedComponent.vue -->
<script setup lang="ts">
import { computed } from 'vue'
import styles from './IsolatedComponent.module.css'

interface ComponentProps {
  variant?: 'primary' | 'secondary' | 'danger'
  size?: 'small' | 'medium' | 'large'
  disabled?: boolean
}

const props = withDefaults(defineProps<ComponentProps>(), {
  variant: 'primary',
  size: 'medium',
  disabled: false
})

// 动态类名组合
const componentClasses = computed(() => [
  styles.component,
  styles[`variant-${props.variant}`],
  styles[`size-${props.size}`],
  {
    [styles.disabled]: props.disabled
  }
])
</script>

<template>
  <div :class="componentClasses">
    <slot />
  </div>
</template>

<style module>
.component {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border: none;
  border-radius: 4px;
  cursor: pointer;
  font-weight: 500;
  transition: all 0.2s ease;
}

.variant-primary {
  background-color: #409eff;
  color: white;
}

.variant-primary:hover {
  background-color: #66b1ff;
}

.variant-secondary {
  background-color: #909399;
  color: white;
}

.variant-danger {
  background-color: #f56c6c;
  color: white;
}

.size-small {
  padding: 4px 8px;
  font-size: 12px;
}

.size-medium {
  padding: 8px 16px;
  font-size: 14px;
}

.size-large {
  padding: 12px 24px;
  font-size: 16px;
}

.disabled {
  opacity: 0.5;
  cursor: not-allowed;
}
</style>

6. 部署与运维

6.1 Docker 容器化部署

dockerfile
# Dockerfile.main-app
FROM node:18-alpine as builder

WORKDIR /app

# 复制依赖文件
COPY package*.json ./
COPY pnpm-lock.yaml ./

# 安装依赖
RUN npm install -g pnpm
RUN pnpm install

# 复制源代码
COPY . .

# 构建应用
RUN pnpm build

# 生产镜像
FROM nginx:alpine

# 复制构建产物
COPY --from=builder /app/dist /usr/share/nginx/html

# 复制 nginx 配置
COPY nginx.conf /etc/nginx/nginx.conf

EXPOSE 80

CMD ["nginx", "-g", "daemon off;"]
yaml
# docker-compose.yml
version: '3.8'

services:
  # 主应用
  main-app:
    build:
      context: ./main-app
      dockerfile: Dockerfile
    ports:
      - "3000:80"
    environment:
      - NODE_ENV=production
    networks:
      - micro-frontend-network

  # 用户管理微应用
  user-app:
    build:
      context: ./user-app  
      dockerfile: Dockerfile
    ports:
      - "3001:80"
    environment:
      - NODE_ENV=production
      - API_BASE_URL=http://api-gateway:8080/api/users
    networks:
      - micro-frontend-network

  # 产品管理微应用
  product-app:
    build:
      context: ./product-app
      dockerfile: Dockerfile
    ports:
      - "3002:80"
    environment:
      - NODE_ENV=production
      - API_BASE_URL=http://api-gateway:8080/api/products
    networks:
      - micro-frontend-network

  # API 网关
  api-gateway:
    image: nginx:alpine
    ports:
      - "8080:80"
    volumes:
      - ./gateway/nginx.conf:/etc/nginx/nginx.conf
    networks:
      - micro-frontend-network

  # Redis (用于共享会话)
  redis:
    image: redis:alpine
    ports:
      - "6379:6379"
    networks:
      - micro-frontend-network

networks:
  micro-frontend-network:
    driver: bridge

6.2 Kubernetes 部署配置

yaml
# k8s/main-app-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: main-app
  namespace: micro-frontend
spec:
  replicas: 3
  selector:
    matchLabels:
      app: main-app
  template:
    metadata:
      labels:
        app: main-app
    spec:
      containers:
      - name: main-app
        image: micro-frontend/main-app:latest
        ports:
        - containerPort: 80
        env:
        - name: NODE_ENV
          value: "production"
        resources:
          limits:
            cpu: 500m
            memory: 512Mi
          requests:
            cpu: 250m
            memory: 256Mi

---
apiVersion: v1
kind: Service
metadata:
  name: main-app-service
  namespace: micro-frontend
spec:
  selector:
    app: main-app
  ports:
  - port: 80
    targetPort: 80
  type: ClusterIP

---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: main-app-ingress
  namespace: micro-frontend
  annotations:
    nginx.ingress.kubernetes.io/rewrite-target: /
spec:
  rules:
  - host: app.example.com
    http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: main-app-service
            port:
              number: 80

6.3 CI/CD 流水线

yaml
# .github/workflows/deploy.yml
name: Micro Frontend Deploy

on:
  push:
    branches: [ main ]
  pull_request:
    branches: [ main ]

jobs:
  build-and-test:
    runs-on: ubuntu-latest
    
    strategy:
      matrix:
        app: [main-app, user-app, product-app, order-app]
    
    steps:
    - uses: actions/checkout@v3
    
    - name: Setup Node.js
      uses: actions/setup-node@v3
      with:
        node-version: '18'
        cache: 'pnpm'
    
    - name: Install dependencies
      run: |
        cd ${{ matrix.app }}
        pnpm install
    
    - name: Run tests
      run: |
        cd ${{ matrix.app }}
        pnpm test
    
    - name: Build application
      run: |
        cd ${{ matrix.app }}
        pnpm build
    
    - name: Build Docker image
      run: |
        cd ${{ matrix.app }}
        docker build -t micro-frontend/${{ matrix.app }}:${{ github.sha }} .
    
    - name: Push to registry
      if: github.ref == 'refs/heads/main'
      run: |
        echo ${{ secrets.DOCKER_PASSWORD }} | docker login -u ${{ secrets.DOCKER_USERNAME }} --password-stdin
        docker push micro-frontend/${{ matrix.app }}:${{ github.sha }}

  deploy:
    needs: build-and-test
    runs-on: ubuntu-latest
    if: github.ref == 'refs/heads/main'
    
    steps:
    - uses: actions/checkout@v3
    
    - name: Deploy to Kubernetes
      run: |
        # 更新 K8s 部署配置中的镜像标签
        sed -i 's/latest/${{ github.sha }}/g' k8s/*.yaml
        
        # 应用配置
        kubectl apply -f k8s/
        
        # 等待部署完成
        kubectl rollout status deployment/main-app -n micro-frontend
        kubectl rollout status deployment/user-app -n micro-frontend
        kubectl rollout status deployment/product-app -n micro-frontend

7. 监控与日志

7.1 应用性能监控

typescript
// utils/monitoring.ts
interface PerformanceMetric {
  name: string
  value: number
  unit: string
  timestamp: number
  tags: Record<string, string>
}

class MicroFrontendMonitor {
  private metrics: PerformanceMetric[] = []
  private endpoint: string

  constructor(endpoint: string) {
    this.endpoint = endpoint
    this.setupPerformanceObserver()
    this.setupErrorTracking()
  }

  // 性能指标收集
  private setupPerformanceObserver() {
    // 监控页面加载性能
    new PerformanceObserver((list) => {
      list.getEntries().forEach((entry) => {
        this.recordMetric({
          name: 'page_load_time',
          value: entry.duration,
          unit: 'ms',
          timestamp: Date.now(),
          tags: {
            entryType: entry.entryType,
            name: entry.name
          }
        })
      })
    }).observe({ entryTypes: ['navigation', 'resource'] })

    // 监控用户交互
    new PerformanceObserver((list) => {
      list.getEntries().forEach((entry: any) => {
        this.recordMetric({
          name: 'user_interaction',
          value: entry.processingStart - entry.startTime,
          unit: 'ms',
          timestamp: Date.now(),
          tags: {
            interactionType: entry.name
          }
        })
      })
    }).observe({ entryTypes: ['first-input'] })
  }

  // 错误追踪
  private setupErrorTracking() {
    window.addEventListener('error', (event) => {
      this.recordError({
        type: 'javascript_error',
        message: event.message,
        filename: event.filename,
        lineno: event.lineno,
        colno: event.colno,
        stack: event.error?.stack
      })
    })

    window.addEventListener('unhandledrejection', (event) => {
      this.recordError({
        type: 'unhandled_promise_rejection',
        message: event.reason?.message || String(event.reason),
        stack: event.reason?.stack
      })
    })
  }

  // 记录自定义指标
  recordMetric(metric: PerformanceMetric) {
    this.metrics.push(metric)
    
    // 批量发送(避免频繁请求)
    if (this.metrics.length >= 10) {
      this.flushMetrics()
    }
  }

  // 记录错误
  recordError(error: any) {
    this.recordMetric({
      name: 'error_count',
      value: 1,
      unit: 'count',
      timestamp: Date.now(),
      tags: {
        type: error.type,
        message: error.message
      }
    })
  }

  // 发送指标数据
  private async flushMetrics() {
    if (this.metrics.length === 0) return

    try {
      await fetch(this.endpoint, {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json'
        },
        body: JSON.stringify({
          metrics: this.metrics,
          app: window.__MICRO_FRONTEND_APP_NAME__ || 'unknown',
          version: window.__MICRO_FRONTEND_APP_VERSION__ || 'unknown',
          timestamp: Date.now()
        })
      })
      
      this.metrics = []
    } catch (error) {
      console.error('Failed to send metrics:', error)
    }
  }
}

export const monitor = new MicroFrontendMonitor('/api/metrics')

// Vue 插件
export function createMonitoringPlugin(endpoint: string) {
  return {
    install(app: any) {
      const monitor = new MicroFrontendMonitor(endpoint)
      
      app.config.globalProperties.$monitor = monitor
      app.provide('monitor', monitor)
      
      // 组件渲染性能监控
      app.mixin({
        beforeCreate() {
          this._renderStart = performance.now()
        },
        mounted() {
          const renderTime = performance.now() - this._renderStart
          monitor.recordMetric({
            name: 'component_render_time',
            value: renderTime,
            unit: 'ms',
            timestamp: Date.now(),
            tags: {
              component: this.$options.name || 'anonymous'
            }
          })
        }
      })
    }
  }
}

总结

微前端架构为大型企业应用提供了强大的解决方案,通过本文的实践指南,我们掌握了:

🏗️ 核心技术栈

  • Module Federation - 运行时模块共享
  • Single-SPA - 应用注册与生命周期管理
  • 事件总线 - 跨应用通信机制
  • 样式隔离 - CSS Module 与 CSS-in-JS

🔧 工程化实践

  • Docker 容器化 - 标准化部署环境
  • Kubernetes 编排 - 高可用集群部署
  • CI/CD 流水线 - 自动化构建与部署
  • 监控与日志 - 完整的可观测性体系

💡 最佳实践

  • 渐进式迁移 - 平滑过渡到微前端架构
  • 团队自治 - 独立开发、测试、部署
  • 性能优化 - 懒加载、缓存策略、资源共享
  • 错误隔离 - 单个应用故障不影响整体系统

微前端架构不仅解决了技术债务和团队协作问题,更为企业数字化转型提供了可扩展的技术底座。通过合理的架构设计和工程化实践,能够构建出高性能、高可用、易维护的现代化企业应用。

相关文章