前端架构

微前端架构深度指南:构建可扩展的大型应用

深入学习微前端的核心概念、Module Federation、应用隔离、数据通信等,打造可扩展的企业级应用架构

16 分钟阅读
#微前端 #Module Federation #应用隔离 #企业级架构

📖 文章概述

微前端是将大型前端应用拆分成多个独立、自治的小应用,然后再组合起来的架构模式。本文深入讲解微前端的核心概念、实现方案和实战应用。


🎯 微前端核心概念

什么是微前端?

微前端是一种架构风格,用于在前端应用中独立地交付功能的技术、策略和方法。

传统单体应用          微前端应用
┌─────────────┐      ┌──────┐ ┌──────┐ ┌──────┐
│             │      │ 微应 │ │ 微应 │ │ 微应 │
│  单一代码库  │  →   │ 用 1 │ │ 用 2 │ │ 用 3 │
│             │      └──────┘ └──────┘ └──────┘
│             │           ↓       ↓       ↓
│  单一部署   │      ┌───────────────────────┐
│             │      │   容器应用/基座应用   │
└─────────────┘      └───────────────────────┘

微前端的优势与劣势

特性优势劣势
独立开发团队自主、技术栈自由重复依赖、包体积增大
独立部署快速迭代、灰度发布运维复杂度高
隔离性故障隔离、样式隔离通信成本、调试困难
可扩展性支持增量开发、动态加载学习曲线陡峭

适用场景

✅ 适合:
├─ 大型团队协作项目
├─ 需要快速迭代的应用
├─ 涉及多个技术栈的项目
├─ 需要独立部署的功能模块
└─ 需要灰度更新的关键应用

❌ 不适合:
├─ 小型项目(过度设计)
├─ 对性能有极端要求
├─ 实时性要求很高
└─ 团队规模小(维护成本高)

🚀 微前端实现方案

1. 基于 URL 的方案

// 最简单的微前端方案:根据 URL 路由加载不同应用

// main.js(容器应用)
function loadMicroApp(path) {
  const apps = {
    '/dashboard': 'https://app1.example.com/dist/index.js',
    '/user': 'https://app2.example.com/dist/index.js',
    '/settings': 'https://app3.example.com/dist/index.js'
  }
  
  const scriptUrl = apps[path]
  if (scriptUrl) {
    loadScript(scriptUrl)
  }
}

function loadScript(src) {
  const script = document.createElement('script')
  script.src = src
  document.body.appendChild(script)
}

// 路由变化时加载对应的微应用
window.addEventListener('hashchange', () => {
  const path = window.location.hash.slice(1)
  loadMicroApp(path)
})

2. Webpack 5 Module Federation

// webpack.config.js - 容器应用(Host)
module.exports = {
  mode: 'development',
  entry: './src/index',
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: '[name].[contenthash].js'
  },
  plugins: [
    new ModuleFederationPlugin({
      name: 'container',
      filename: 'remoteEntry.js',
      remotes: {
        // 远程应用
        app1: 'app1@http://localhost:3001/remoteEntry.js',
        app2: 'app2@http://localhost:3002/remoteEntry.js',
        app3: 'app3@http://localhost:3003/remoteEntry.js'
      },
      shared: {
        react: { singleton: true, requiredVersion: false },
        'react-dom': { singleton: true, requiredVersion: false },
        vue: { singleton: true, requiredVersion: false }
      }
    })
  ]
}

// webpack.config.js - 微应用(Remote)
module.exports = {
  plugins: [
    new ModuleFederationPlugin({
      name: 'app1',
      filename: 'remoteEntry.js',
      exposes: {
        './App': './src/App.vue'
      },
      shared: {
        react: { singleton: true },
        'react-dom': { singleton: true },
        vue: { singleton: true }
      }
    })
  ]
}

// 使用远程应用
import React, { Suspense, lazy } from 'react'

const RemoteApp1 = lazy(() => import('app1/App'))

export default function Container() {
  return (
    <Suspense fallback={<div>Loading...</div>}>
      <RemoteApp1 />
    </Suspense>
  )
}

3. Vite Module Federation(experimental)

// 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: 'container',
      remotes: {
        app1: 'http://localhost:3001/dist/remoteEntry.js',
        app2: 'http://localhost:3002/dist/remoteEntry.js'
      },
      exposes: {
        './App': './src/App.vue'
      },
      shared: ['vue', 'vue-router']
    })
  ]
})

4. 基于 iframe 的方案

<template>
  <div class="micro-container">
    <iframe
      :src="iframeSrc"
      :title="appName"
      class="micro-iframe"
      @load="handleLoad"
    ></iframe>
  </div>
</template>

<script setup lang="ts">
import { ref } from 'vue'

interface Props {
  appUrl: string
  appName: string
}

const props = withDefaults(defineProps<Props>(), {})
const iframeSrc = ref(props.appUrl)

const handleLoad = () => {
  // iframe 加载完成后的处理
  console.log(`${props.appName} 加载完成`)
}

// iframe 通信
const sendMessage = (data: any) => {
  const iframe = document.querySelector('iframe') as HTMLIFrameElement
  iframe.contentWindow?.postMessage(data, '*')
}

// 监听来自 iframe 的消息
window.addEventListener('message', (event) => {
  if (event.origin !== 'http://trusted-site.example.com') return
  console.log('收到 iframe 消息:', event.data)
})
</script>

<style scoped>
.micro-container {
  width: 100%;
  height: 100%;
}

.micro-iframe {
  width: 100%;
  height: 100%;
  border: none;
}
</style>

🔌 qiankun 框架(生产级方案)

5. qiankun 基础设置

// main.ts(容器应用)
import { registerMicroApps, start } from 'qiankun'

// 注册微应用
registerMicroApps([
  {
    name: '@org/app1',
    entry: '//localhost:3001',
    container: '#app1-container',
    activeRule: '/app1'
  },
  {
    name: '@org/app2',
    entry: '//localhost:3002',
    container: '#app2-container',
    activeRule: '/app2'
  },
  {
    name: '@org/app3',
    entry: '//localhost:3003',
    container: '#app3-container',
    activeRule: '/app3'
  }
])

// 全局错误处理
qiankun.onGlobalStateChange((state, prev) => {
  console.log('全局状态变化:', state, prev)
})

// 启动应用
start()

// 容器应用的根组件
export default {
  template: `
    <div id="root">
      <nav>
        <a href="#/app1">App 1</a>
        <a href="#/app2">App 2</a>
        <a href="#/app3">App 3</a>
      </nav>
      <div id="app1-container"></div>
      <div id="app2-container"></div>
      <div id="app3-container"></div>
    </div>
  `
}

6. qiankun 微应用接入

// src/main.ts(微应用)
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'

let instance: any = null

// 导出必要的生命周期钩子
export async function bootstrap() {
  console.log('[vue app] vue app bootstraped')
}

export async function mount(props: any) {
  console.log('[vue app] props from container:', props)
  instance = createApp(App)
  instance.use(router)
  instance.mount(props.container || '#app')
}

export async function unmount() {
  instance?.unmount()
  instance = null
}

// 单独开发时
if (!window.__POWERED_BY_QIANKUN__) {
  const app = createApp(App)
  app.use(router)
  app.mount('#app')
}

7. 全局状态管理

// shared-store.ts(容器应用)
import { initGlobalState } from 'qiankun'

const initialState = {
  user: null,
  token: '',
  theme: 'light'
}

const actions = initGlobalState(initialState)

// 监听全局状态
actions.onGlobalStateChange((state, prev) => {
  console.log('全局状态更新:', state)
  
  // 同步状态到本地存储
  localStorage.setItem('globalState', JSON.stringify(state))
})

// 设置全局状态
export function setGlobalState(newState: any) {
  actions.setGlobalState(newState)
}

export default actions

// 微应用中使用
import { getGlobalState } from 'qiankun'

export async function mount(props: any) {
  // 从容器应用获取全局状态
  const globalState = props.onGlobalStateChange?.getState?.()
  console.log('获取全局状态:', globalState)
  
  // 监听全局状态变化
  props.onGlobalStateChange?.((state: any) => {
    console.log('全局状态变化:', state)
    // 更新应用内的状态
    updateAppState(state)
  })
  
  // 修改全局状态
  props.setGlobalState?.({
    user: { id: 1, name: 'John' }
  })
}

🛡️ 应用隔离和样式隔离

8. CSS 隔离方案

// 方案 1: CSS Modules
// app1/src/styles/index.module.css
.container {
  padding: 20px;
  color: #333;
}

// app1/src/App.vue
<style module>
@import './styles/index.module.css';
</style>

// 方案 2: BEM 命名规范
<template>
  <div class="app1__container">
    <div class="app1__header">Header</div>
    <div class="app1__content">Content</div>
  </div>
</template>

<style scoped>
.app1__container { /* ... */ }
.app1__header { /* ... */ }
.app1__content { /* ... */ }
</style>

// 方案 3: Shadow DOM(最隔离)
class MicroApp extends HTMLElement {
  connectedCallback() {
    const shadow = this.attachShadow({ mode: 'open' })
    shadow.innerHTML = `
      <style>
        :host {
          display: block;
          padding: 20px;
        }
        .container {
          color: blue;
        }
      </style>
      <div class="container">
        <h1>Isolated App</h1>
      </div>
    `
  }
}

customElements.define('micro-app', MicroApp)

9. JavaScript 隔离(沙箱)

// 简单沙箱实现
class Sandbox {
  private proxy: any
  private globalContext: any = {}

  constructor() {
    // 创建代理上下文
    this.globalContext = Object.create(null)
    
    // 预设全局对象(白名单)
    const whiteList = ['console', 'Math', 'Date', 'JSON']
    whiteList.forEach(name => {
      this.globalContext[name] = window[name as any]
    })
    
    // 使用 Proxy 拦截访问
    this.proxy = new Proxy(this.globalContext, {
      get: (target, prop) => {
        if (prop in target) {
          return target[prop]
        }
        return undefined
      },
      set: (target, prop, value) => {
        target[prop] = value
        return true
      }
    })
  }

  // 在沙箱中执行代码
  execute(code: string) {
    try {
      const fn = new Function('sandbox', code)
      fn(this.proxy)
    } catch (error) {
      console.error('沙箱执行错误:', error)
    }
  }
}

// 使用
const sandbox = new Sandbox()
sandbox.execute(`
  console.log('运行在沙箱中')
  const data = { id: 1 }
  // 无法访问 window.location 等危险属性
`)

📡 微应用间通信

10. 事件总线通信

// EventBus.ts(共享库)
class EventBus {
  private events: Map<string, Function[]> = new Map()

  // 订阅事件
  on(event: string, callback: Function) {
    if (!this.events.has(event)) {
      this.events.set(event, [])
    }
    this.events.get(event)!.push(callback)
    
    // 返回取消订阅函数
    return () => {
      const callbacks = this.events.get(event)!
      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(cb => cb(...args))
    }
  }

  // 一次性监听
  once(event: string, callback: Function) {
    const unsubscribe = this.on(event, (...args: any[]) => {
      callback(...args)
      unsubscribe()
    })
  }

  // 清除事件
  off(event: string) {
    this.events.delete(event)
  }

  // 清除所有事件
  clear() {
    this.events.clear()
  }
}

export const eventBus = new EventBus()

// 微应用 1 中发送事件
import { eventBus } from '@shared/event-bus'

export function notifyUserUpdate(user: any) {
  eventBus.emit('user-updated', user)
}

// 微应用 2 中监听事件
import { eventBus } from '@shared/event-bus'

onMounted(() => {
  eventBus.on('user-updated', (user: any) => {
    console.log('用户已更新:', user)
    updateUserInfo(user)
  })
})

11. 基于 URL 的通信

// 通过 URL 参数传递数据
export function navigateToApp(appName: string, params: any) {
  const queryString = new URLSearchParams(params).toString()
  window.location.hash = `#/${appName}?${queryString}`
}

// 在微应用中读取参数
import { useRoute } from 'vue-router'

export default {
  setup() {
    const route = useRoute()
    const userId = route.query.userId
    const action = route.query.action
    
    return { userId, action }
  }
}

🎯 实战:完整微前端应用示例

12. 容器应用完整实现

<!-- Container.vue -->
<template>
  <div class="container">
    <!-- 导航 -->
    <header class="header">
      <div class="logo">Micro App Platform</div>
      <nav class="nav">
        <router-link to="/dashboard">Dashboard</router-link>
        <router-link to="/profile">Profile</router-link>
        <router-link to="/settings">Settings</router-link>
      </nav>
      <div class="user-info">{{ globalState.user?.name }}</div>
    </header>

    <!-- 微应用容器 -->
    <main class="main">
      <div id="micro-app-container"></div>
    </main>

    <!-- 全局加载提示 -->
    <div v-if="loading" class="loading">
      <span>加载中...</span>
    </div>
  </div>
</template>

<script setup lang="ts">
import { ref, reactive, onMounted } from 'vue'
import { registerMicroApps, start, setDefaultMountApp } from 'qiankun'

const loading = ref(false)

const globalState = reactive({
  user: null,
  token: '',
  theme: 'light'
})

// 注册微应用
const registerApps = () => {
  registerMicroApps([
    {
      name: '@app/dashboard',
      entry: 'http://localhost:3001',
      container: '#micro-app-container',
      activeRule: '/dashboard',
      props: { globalState }
    },
    {
      name: '@app/profile',
      entry: 'http://localhost:3002',
      container: '#micro-app-container',
      activeRule: '/profile',
      props: { globalState }
    },
    {
      name: '@app/settings',
      entry: 'http://localhost:3003',
      container: '#micro-app-container',
      activeRule: '/settings',
      props: { globalState }
    }
  ], {
    beforeLoad: [
      async () => {
        loading.value = true
      }
    ],
    afterMount: [
      async () => {
        loading.value = false
      }
    ]
  })

  // 设置默认应用
  setDefaultMountApp('@app/dashboard')
}

onMounted(() => {
  registerApps()
  start()
})

// 导出全局状态更新方法
export function setGlobalState(newState: any) {
  Object.assign(globalState, newState)
}
</script>

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

.header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 20px;
  background: #333;
  color: white;
}

.nav {
  display: flex;
  gap: 20px;
}

.nav a {
  color: white;
  text-decoration: none;
}

.main {
  flex: 1;
  overflow: auto;
}

.loading {
  position: fixed;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  background: white;
  padding: 20px;
  border-radius: 8px;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
}
</style>

📊 性能优化建议

优化方案效果实施难度
预加载微应用⭐⭐⭐
共享依赖⭐⭐⭐⭐
懒加载模块⭐⭐⭐
CDN 加速⭐⭐⭐
增量更新⭐⭐⭐⭐⭐

🐛 常见问题解决

问题 1:样式污染

// ❌ 问题:全局样式相互覆盖
// app1 定义 .title { color: blue }
// app2 定义 .title { color: red }

// ✅ 解决方案:使用 BEM 或 CSS Modules
// app1: .app1__title { color: blue }
// app2: .app2__title { color: red }

// 或使用 Shadow DOM
const shadow = this.attachShadow({ mode: 'open' })
shadow.innerHTML = `
  <style>
    .title { color: blue } /* 完全隔离 */
  </style>
`

问题 2:全局变量冲突

// ❌ 问题:多个应用使用相同的全局变量
window.config = { ... }  // app1 设置
window.config = { ... }  // app2 覆盖

// ✅ 解决方案:使用命名空间
window.__APP1__ = { config: { ... } }
window.__APP2__ = { config: { ... } }

// 或使用事件总线
eventBus.emit('config-update', newConfig)

问题 3:加载缓慢

// ✅ 优化方案:预加载微应用
const preloadMicroApp = (name: string, entry: string) => {
  const script = document.createElement('script')
  script.src = `${entry}/remoteEntry.js`
  script.async = true
  document.head.appendChild(script)
}

// 应用启动时预加载
preloadMicroApp('app1', 'http://localhost:3001')
preloadMicroApp('app2', 'http://localhost:3002')

🎓 最佳实践总结

DO ✅

// 1. 清晰的应用边界
registerMicroApps([
  {
    name: '@org/app1',
    entry: 'http://localhost:3001',
    activeRule: '/app1'  // 明确的激活规则
  }
])

// 2. 共享必要的依赖
shared: {
  vue: { singleton: true },
  'vue-router': { singleton: true }
}

// 3. 隔离样式和脚本
// 使用 CSS Modules 或 BEM
// 使用沙箱或 Web Components

// 4. 健全的通信机制
eventBus.on('event-name', callback)

// 5. 优雅的错误处理
try {
  await loadMicroApp(...)
} catch (error) {
  showErrorMessage(error)
}

DON'T ❌

// 1. 不要过度分割应用
// 应该按业务域划分,不要过细

// 2. 不要共享所有依赖
// 会导致版本冲突

// 3. 不要忽视隔离
// 必须实现样式和脚本隔离

// 4. 不要直接操作 DOM
// 应该通过事件总线通信

// 5. 不要让微应用依赖特定顺序加载
// 应该支持独立加载

📚 扩展资源


总结

微前端架构核心要点:

  1. 正确选型:根据项目规模选择合适方案
  2. 应用隔离:样式、脚本、上下文的完全隔离
  3. 有效通信:事件总线、全局状态管理
  4. 性能优化:预加载、共享依赖、增量更新
  5. 团队协作:明确的应用边界、规范的通信协议
  6. 运维成本:监控、日志、灰度部署

掌握微前端,能够构建可扩展、易维护的大型前端应用!