引言

随着前端应用规模的增长,单体应用面临着维护困难、部署低效、技术栈固化等问题。微前端架构通过将应用拆分为多个独立的子应用,实现了应用级别的模块化。本文将全面介绍微前端架构的设计与实现。

一、微前端基础概念

1.1 什么是微前端

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// 单体应用 vs 微前端

// 单体应用
const MonolithicApp = () => {
  return (
    <div>
      <Header />        {/* 用户团队维护 */}
      <Dashboard />     {/* 数据团队维护 */}
      <Settings />      {/* 配置团队维护 */}
      <Billing />       {/* 财务团队维护 */}
    </div>
  )
}

// 微前端架构
const MicroFrontendApp = () => {
  return (
    <div>
      <MicroApp name="header" />       {/* 独立部署 */}
      <MicroApp name="dashboard" />    {/* 独立部署 */}
      <MicroApp name="settings" />     {/* 独立部署 */}
      <MicroApp name="billing" />      {/* 独立部署 */}
    </div>
  )
}

1.2 微前端核心价值

优势说明收益
技术栈无关各子应用可选择不同技术团队自治、技术灵活性
独立开发部署子应用独立版本控制提高发布频率、降低风险
增量升级逐步迁移旧系统降低迁移成本
团队自治独立开发测试部署提高团队效率

1.3 微前端技术选型对比

方案实现复杂度样式隔离JS隔离适用场景
qiankun中支持支持Vue/React/Angular混用
single-spa高手动处理手动处理高度定制需求
Module Federation低需配置需配置Webpack 5项目
iframe低天然隔离天然隔离快速隔离、跨域限制

二、qiankun微前端实战

2.1 主应用配置

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
// 主应用入口 main.ts
import { registerMicroApps, start, initGlobalState } from 'qiankun'

// 1. 注册子应用
const apps = [
  {
    name: 'dashboard',
    entry: '//localhost:7101',
    container: '#subapp-container',
    activeRule: '/dashboard',
    props: {
      routerBase: '/dashboard',
      getGlobalState: () => store.getState()
    }
  },
  {
    name: 'settings',
    entry: '//localhost:7102',
    container: '#subapp-container',
    activeRule: '/settings',
    props: {
      routerBase: '/settings'
    }
  },
  {
    name: 'billing',
    entry: '//localhost:7103',
    container: '#subapp-container',
    activeRule: '/billing'
  }
]

// 2. 配置全局状态
const initialState = {
  user: null,
  token: null,
  theme: 'light'
}

const actions = initGlobalState(initialState)

// 3. 监听状态变化
actions.onGlobalStateChange((state, prev) => {
  console.log('Global state changed:', state, prev)
})

// 4. 启动qiankun
registerMicroApps(apps, {
  beforeLoad: [
    app => {
      console.log('Loading app:', app.name)
      return Promise.resolve()
    }
  ],
  beforeMount: [
    app => {
      console.log('Mounting app:', app.name)
      return Promise.resolve()
    }
  ],
  afterMount: [
    app => {
      console.log('Mounted app:', app.name)
      return Promise.resolve()
    }
  ],
  beforeUnmount: [
    app => {
      console.log('Unmounting app:', app.name)
      return Promise.resolve()
    }
  ],
  afterUnmount: [
    app => {
      console.log('Unmounted app:', app.name)
      return Promise.resolve()
    }
  ]
})

start({
  sandbox: {
    strictStyleIsolation: true,  // 严格样式隔离
    experimentalStyleIsolation: true  // 实验性样式隔离
  },
  prefetch: true,  // 预加载子应用资源
  singular: false,  // 是否单实例
  fetch: window.fetch  // 自定义fetch方法
})

2.2 子应用配置(Vue 3)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
// 子应用入口 main.ts
import { createApp } from 'vue'
import { createRouter, createWebHistory } from 'vue-router'
import App from './App.vue'

let app: any
let router: any
let history: any

// 渲染函数
function render(props: any = {}) {
  const { container } = props

  history = createWebHistory(props.routerBase || '/')
  router = createRouter({
    history,
    routes: [
      { path: '/', component: () => import('./views/Home.vue') },
      { path: '/about', component: () => import('./views/About.vue') }
    ]
  })

  app = createApp(App)
  app.use(router)
  app.mount(container ? container.querySelector('#app') : '#app')
}

// 独立运行
if (!(window as any).__POWERED_BY_QIANKUN__) {
  render()
}

// qiankun生命周期
export async function bootstrap() {
  console.log('Dashboard app bootstrapped')
}

export async function mount(props: any) {
  console.log('Dashboard app mounted with props:', props)
  render(props)

  // 接收主应用传递的状态
  props.onGlobalStateChange &&
    props.onGlobalStateChange((state: any) => {
      console.log('Global state:', state)
    }, true)
}

export async function unmount() {
  app?.unmount()
  history?.destroy()
  app = null
  router = null
  history = null
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
// vue.config.js
const { name } = require('./package.json')

module.exports = {
  devServer: {
    port: 7101,
    headers: {
      'Access-Control-Allow-Origin': '*'  // 允许跨域
    }
  },
  configureWebpack: {
    output: {
      library: `${name}-[name]`,
      libraryTarget: 'umd',
      globalObject: 'window'
    }
  }
}

2.3 应用间通信

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
// 1. 基于props通信(推荐)
// 主应用
registerMicroApps([
  {
    name: 'dashboard',
    entry: '//localhost:7101',
    container: '#subapp-container',
    activeRule: '/dashboard',
    props: {
      // 传递方法
      onNavigate: (to: string) => {
        console.log('Navigate to:', to)
      },
      // 传递数据
      sharedData: {
        apiEndpoint: 'https://api.example.com'
      },
      // 传递状态管理
      getGlobalState: () => store.getState()
    }
  }
])

// 子应用接收
export async function mount(props: any) {
  const { onNavigate, sharedData, getGlobalState } = props

  // 使用传递的方法
  onNavigate('/settings')

  // 访问共享数据
  console.log('API endpoint:', sharedData.apiEndpoint)

  // 访问全局状态
  const globalState = getGlobalState()
}

// 2. 全局状态管理
// 主应用
import { initGlobalState } from 'qiankun'

const initialState = {
  user: null,
  theme: 'light',
  locale: 'zh-CN'
}

const actions = initGlobalState(initialState)

// 主应用更新状态
actions.setGlobalState({
  user: { id: 1, name: 'Alice' }
})

// 子应用监听状态
export async function mount(props: any) {
  props.onGlobalStateChange((state: any, prev: any) => {
    console.log('State changed from', prev, 'to', state)

    // 响应状态变化
    if (state.theme !== prev.theme) {
      updateTheme(state.theme)
    }
  }, true)
}

// 3. 自定义事件总线
// 创建事件总线
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)
  }

  emit(event: string, ...args: any[]) {
    const callbacks = this.events.get(event) || []
    callbacks.forEach(callback => callback(...args))
  }

  off(event: string, callback: Function) {
    const callbacks = this.events.get(event) || []
    const index = callbacks.indexOf(callback)
    if (index > -1) {
      callbacks.splice(index, 1)
    }
  }
}

// 挂载到window
window.__EVENT_BUS__ = new EventBus()

// 使用
window.__EVENT_BUS__.on('user-login', (user) => {
  console.log('User logged in:', user)
})

window.__EVENT_BUS__.emit('user-login', { id: 1, name: 'Alice' })

三、Module Federation实战

3.1 Webpack配置

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
// 主应用 webpack.config.js
const ModuleFederationPlugin = require('webpack/lib/container/ModuleFederationPlugin')

module.exports = {
  plugins: [
    new ModuleFederationPlugin({
      name: 'host',
      remotes: {
        dashboard: 'dashboard@http://localhost:7101/remoteEntry.js',
        settings: 'settings@http://localhost:7102/remoteEntry.js'
      },
      shared: {
        react: { singleton: true, requiredVersion: '^18.0.0' },
        'react-dom': { singleton: true, requiredVersion: '^18.0.0' },
        vue: { singleton: true, requiredVersion: '^3.0.0' }
      }
    })
  ]
}

// 子应用 webpack.config.js
module.exports = {
  plugins: [
    new ModuleFederationPlugin({
      name: 'dashboard',
      filename: 'remoteEntry.js',
      exposes: {
        './Dashboard': './src/Dashboard.vue',
        './router': './src/router'
      },
      shared: {
        vue: { singleton: true, requiredVersion: '^3.0.0' },
        'vue-router': { singleton: true }
      }
    })
  ]
}

3.2 动态加载远程模块

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// 主应用中动态加载
import { defineAsyncComponent } from 'vue'

// 方式1:使用defineAsyncComponent
const Dashboard = defineAsyncComponent(() =>
  import('dashboard/Dashboard')
)

// 方式2:手动加载
async function loadRemoteModule() {
  const module = await import('dashboard/Dashboard')
  return module.default
}

// 方式3:错误边界
const SafeDashboard = defineAsyncComponent({
  loader: () => import('dashboard/Dashboard'),
  loadingComponent: LoadingSpinner,
  errorComponent: ErrorDisplay,
  delay: 200,
  timeout: 5000
})

// 使用
<template>
  <Dashboard v-if="loaded" />
  <Loading v-else />
</template>

四、样式隔离方案

4.1 CSS Modules

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
/* Dashboard.module.css */
.container {
  padding: 20px;
  background: #f0f0f0;
}

.title {
  font-size: 24px;
  color: #333;
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
<!-- Dashboard.vue -->
<template>
  <div :class="$style.container">
    <h1 :class="$style.title">Dashboard</h1>
  </div>
</template>

<script setup>
import styles from './Dashboard.module.css'
</script>

4.2 Shadow DOM

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// 使用Shadow DOM实现完全隔离
class ShadowComponent extends HTMLElement {
  constructor() {
    super()

    // 创建Shadow DOM
    const shadow = this.attachShadow({ mode: 'open' })

    // 添加样式
    const style = document.createElement('style')
    style.textContent = `
      .container {
        padding: 20px;
        background: #f0f0f0;
      }
    `
    shadow.appendChild(style)

    // 添加内容
    const container = document.createElement('div')
    container.className = 'container'
    container.innerHTML = '<h1>Shadow DOM Content</h1>'
    shadow.appendChild(container)
  }
}

customElements.define('shadow-component', ShadowComponent)

4.3 动态CSS隔离

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
// qiankun的样式隔离
start({
  sandbox: {
    strictStyleIsolation: true  // 为每个子应用创建Shadow DOM
  }
})

// 或使用实验性CSS作用域
start({
  sandbox: {
    experimentalStyleIsolation: true  // 添加作用域选择器
  }
})

五、共享依赖管理

5.1 依赖共享配置

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
// webpack.config.js
new ModuleFederationPlugin({
  shared: {
    // 单例模式共享
    react: {
      singleton: true,  // 只加载一个版本
      requiredVersion: '^18.0.0',
      eager: true  // 初始时加载
    },
    // 版本控制
    'lodash': {
      singleton: false,  // 允许多版本
      version: '^4.17.0'
    },
    // 自定义共享
    'shared-utils': {
      import: './src/utils',
      shareScope: 'default'
    }
  }
})

5.2 公共组件库

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// 创建共享组件库
// shared-components/src/index.ts
export { Button } from './Button'
export { Input } from './Input'
export { Modal } from './Modal'

// 主应用暴露共享组件
// webpack.config.js
new ModuleFederationPlugin({
  name: 'host',
  exposes: {
    './shared': './shared-components/src'
  }
})

// 子应用使用共享组件
// webpack.config.js
new ModuleFederationPlugin({
  name: 'dashboard',
  remotes: {
    host: 'host@http://localhost:3000/remoteEntry.js'
  },
  shared: {
    vue: { singleton: true }
  }
})

// 子应用中使用
import { Button, Input } from 'host/shared'

六、部署策略

6.1 独立部署

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# 主应用部署配置
# .github/workflows/deploy-host.yml
name: Deploy Host App

on:
  push:
    branches: [main]
    paths:
      - 'packages/host/**'

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3

      - name: Build
        run: |
          cd packages/host
          npm install
          npm run build

      - name: Deploy to CDN
        run: |
          aws s3 sync packages/host/dist s3://cdn.example.com/host
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# 子应用部署配置
# .github/workflows/deploy-dashboard.yml
name: Deploy Dashboard App

on:
  push:
    branches: [main]
    paths:
      - 'packages/dashboard/**'

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3

      - name: Build
        run: |
          cd packages/dashboard
          npm install
          npm run build

      - name: Deploy to CDN
        run: |
          aws s3 sync packages/dashboard/dist s3://cdn.example.com/dashboard

6.2 版本管理策略

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 应用版本管理
const appVersions = {
  host: '1.2.0',
  dashboard: '2.1.0',
  settings: '1.5.0',
  billing: '1.0.0'
}

// A/B测试部署
const abTestConfig = {
  '/dashboard': {
    control: '//cdn.example.com/dashboard/2.1.0/remoteEntry.js',
    experiment: '//cdn.example.com/dashboard/2.2.0/remoteEntry.js',
    traffic: 0.1  // 10%流量到新版本
  }
}

function getAppEntry(appName: string): string {
  const config = abTestConfig[`/${appName}`]
  if (!config) return `//cdn.example.com/${appName}/latest/remoteEntry.js`

  const traffic = Math.random()
  return traffic < config.traffic ? config.experiment : config.control
}

七、监控与调试

7.1 性能监控

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
// 微前端性能监控
class MicroFrontendMonitor {
  trackAppLoad(appName: string) {
    const startTime = performance.now()

    return {
      end: () => {
        const duration = performance.now() - startTime
        this.sendMetrics({
          app: appName,
          type: 'load',
          duration
        })
      }
    }
  }

  trackAppError(appName: string, error: Error) {
    this.sendMetrics({
      app: appName,
      type: 'error',
      message: error.message,
      stack: error.stack
    })
  }

  private sendMetrics(data: any) {
    fetch('/api/metrics', {
      method: 'POST',
      body: JSON.stringify(data)
    })
  }
}

// 使用qiankun生命周期
const monitor = new MicroFrontendMonitor()

registerMicroApps(apps, {
  beforeMount: [app => monitor.trackAppLoad(app.name)],
  error: [app => monitor.trackAppError(app.name, app.error)]
})

7.2 错误边界

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<!-- ErrorBoundary.vue -->
<template>
  <div v-if="error" class="error-boundary">
    <h2>Something went wrong</h2>
    <p>{{ error.message }}</p>
    <button @click="retry">Retry</button>
  </div>
  <slot v-else />
</template>

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

const error = ref<Error | null>(null)

onErrorCaptured((err: Error) => {
  error.value = err
  // 上报错误
  console.error('Micro frontend error:', err)
  return false  // 阻止错误继续传播
})

function retry() {
  error.value = null
}
</script>

总结

微前端架构实施要点:

  1. 技术选型 - 根据项目情况选择合适的微前端框架
  2. 应用隔离 - 确保样式、JS执行环境的隔离
  3. 通信机制 - 建立清晰的应用间通信规范
  4. 依赖管理 - 合理配置共享依赖避免重复加载
  5. 独立部署 - 实现子应用的独立版本控制和部署
  6. 监控体系 - 建立完善的监控和错误处理机制

微前端不是银弹,需要根据团队规模、业务特点谨慎选择。适合大中型团队和复杂业务场景,小型项目可能反而增加复杂度。

相关工具推荐