9.微前端架构设计 - 标准答案

考察点:架构能力、技术选型、问题解决

场景描述: 您的公司有多个前端团队,分别负责不同的业务模块(订单、库存、财务、用户中心),技术栈混杂(Vue2、Vue3、React)。现在需要将这些模块整合到一个统一的后台管理系统中。

需求

  • 各模块独立开发、独立部署
  • 支持 Vue2、Vue3 混合运行
  • 共享公共依赖(如 Vue、Element UI)以减小包体积
  • 主应用负责路由、权限、布局

问题

  1. 技术方案:微前端的实现方案有哪些?(如 qiankun、Micro-App、Module Federation、iframe)您会选择哪个?为什么?
  2. 核心挑战:微前端架构的核心技术难点是什么?(如 JS 沙箱隔离、样式隔离、应用间通信)如何解决?
  3. 实战考量:Vue2 和 Vue3 共存时有什么注意事项?如何避免版本冲突?

主流方案对比

方案技术原理优点缺点适用场景
qiankun基于 single-spa + HTML Entry + JS 沙箱成熟稳定、开箱即用、社区活跃、完善的沙箱和样式隔离性能开销较大、对子应用侵入性中等企业级中后台系统、多团队协作
Micro-App基于 WebComponent + HTML Entry接入成本低、类似 iframe 的使用体验、性能较好社区相对小、浏览器兼容性依赖 polyfill快速接入、中小型项目
Module FederationWebpack 5 原生支持、模块联邦性能最优、可共享依赖、Webpack 生态需要 Webpack 5、配置复杂、依赖版本管理难同技术栈、深度集成场景
iframe浏览器原生隔离完全隔离、接入成本极低性能差、通信复杂、用户体验不佳(白屏、路由不同步)快速集成第三方系统、安全要求极高

推荐选型:qiankun

理由

  1. 成熟度高:阿里开源,经过蚂蚁集团大规模实践验证
  2. 多框架支持:原生支持 Vue2/Vue3/React 等任意框架混用
  3. 沙箱隔离:提供 Proxy 沙箱和快照沙箱,有效隔离 JS 运行环境
  4. 样式隔离:支持 Shadow DOM 和动态样式表隔离
  5. 开箱即用:提供完整的生命周期钩子和应用间通信方案
  6. 社区生态:文档完善,issue 响应快,插件丰富

对于 Vue2/Vue3 混用场景,qiankun 是目前最佳选择。


2.1 JS 沙箱隔离

挑战:多个子应用运行在同一个 window 上下文,可能互相污染全局变量。

qiankun 的解决方案

a) Proxy 沙箱 (推荐,适用于现代浏览器)
// qiankun 内部实现原理简化版
class ProxySandbox {
  constructor() {
    const fakeWindow = {}
    const proxy = new Proxy(fakeWindow, {
      get(target, prop) {
        // 优先从沙箱中获取
        if (prop in target) {
          return target[prop]
        }
        // 否则从真实 window 获取
        const value = window[prop]
        // 如果是函数,绑定到真实 window
        if (typeof value === 'function' && !value.prototype) {
          return value.bind(window)
        }
        return value
      },
      set(target, prop, value) {
        // 所有修改都记录在沙箱内
        target[prop] = value
        return true
      },
      has(target, prop) {
        return prop in target || prop in window
      }
    })
    
    this.proxy = proxy
  }
  
  // 激活沙箱
  active() {
    this.running = true
  }
  
  // 失活沙箱
  inactive() {
    this.running = false
  }
}

// 使用示例
const sandbox = new ProxySandbox()
sandbox.active()

// 子应用在沙箱中运行
;(function(window) {
  window.myApp = { name: 'App1' } // 只作用于沙箱
})(sandbox.proxy)

sandbox.inactive()

特性

  • 每个子应用独立 Proxy 沙箱
  • 不影响真实 window
  • 支持多应用同时运行
b) 快照沙箱 (兼容 IE,但不支持多应用并行)
class SnapshotSandbox {
  constructor() {
    this.modifyPropsMap = {} // 记录运行时修改的属性
  }
  
  active() {
    // 保存当前 window 快照
    this.windowSnapshot = {}
    for (const prop in window) {
      this.windowSnapshot[prop] = window[prop]
    }
    
    // 恢复上次记录的修改
    Object.keys(this.modifyPropsMap).forEach(prop => {
      window[prop] = this.modifyPropsMap[prop]
    })
  }
  
  inactive() {
    // 记录本次运行的修改
    for (const prop in window) {
      if (window[prop] !== this.windowSnapshot[prop]) {
        this.modifyPropsMap[prop] = window[prop]
        // 还原 window
        window[prop] = this.windowSnapshot[prop]
      }
    }
  }
}

2.2 样式隔离

挑战:不同子应用的样式可能相互覆盖(如都有 .btn 类)。

解决方案

a) Shadow DOM (严格隔离,但可能影响弹窗组件)
// qiankun 配置
import { start } from 'qiankun'

start({
  sandbox: {
    strictStyleIsolation: true // 启用 Shadow DOM
  }
})

原理:将子应用渲染在 Shadow DOM 内,样式天然隔离。

注意事项

  • Element UI / Ant Design 的弹窗组件默认挂载到 body,可能渲染到 Shadow DOM 外
  • 需要配置弹窗挂载点:
// 子应用配置 Element UI
import { Message } from 'element-ui'

export async function mount(props) {
  const { container } = props
  // 将弹窗挂载到子应用容器内
  Message.config({
    getContainer: () => container.querySelector('#app')
  })
}
b) 动态样式表作用域 (推荐,兼容性好)
start({
  sandbox: {
    experimentalStyleIsolation: true // 运行时添加样式前缀
  }
})

原理:qiankun 运行时动态给子应用的样式选择器添加 data-qiankun 属性前缀。

/* 原始样式 */
.btn { color: red; }

/* qiankun 运行时转换为 */
div[data-qiankun="app1"] .btn { color: red; }
c) CSS Modules / CSS-in-JS (源头隔离)
<!-- Vue 组件使用 scoped 样式 -->
<style scoped>
.btn {
  color: blue;
}
</style>

编译后自动添加唯一哈希:

.btn[data-v-f3f3eg9] {
  color: blue;
}

2.3 应用间通信

挑战:主应用和子应用、子应用之间需要共享数据(如用户信息、token)。

解决方案

a) qiankun 官方通信方案 - initGlobalState
// ===== 主应用 =====
import { initGlobalState } from 'qiankun'

// 初始化全局状态
const actions = initGlobalState({
  user: { name: 'Admin', token: 'xxx' },
  theme: 'dark'
})

// 监听变化
actions.onGlobalStateChange((state, prev) => {
  console.log('主应用监听到状态变化:', state)
})

// 修改状态
actions.setGlobalState({ theme: 'light' })

// ===== 子应用 =====
export async function mount(props) {
  // 接收主应用传递的通信方法
  props.onGlobalStateChange((state, prev) => {
    console.log('子应用监听到状态变化:', state)
    // 同步到 Vuex/Pinia
    store.commit('setUser', state.user)
  })
  
  // 子应用也可以修改全局状态
  props.setGlobalState({ theme: 'blue' })
}
b) 自定义事件总线 (更灵活)
// ===== 主应用 =====
class EventBus {
  constructor() {
    this.events = {}
  }
  
  on(event, callback) {
    if (!this.events[event]) {
      this.events[event] = []
    }
    this.events[event].push(callback)
  }
  
  emit(event, data) {
    if (this.events[event]) {
      this.events[event].forEach(cb => cb(data))
    }
  }
  
  off(event, callback) {
    if (this.events[event]) {
      this.events[event] = this.events[event].filter(cb => cb !== callback)
    }
  }
}

const eventBus = new EventBus()

registerMicroApps([
  {
    name: 'app1',
    entry: '//localhost:8081',
    container: '#container',
    props: { eventBus } // 传递给子应用
  }
])

// ===== 子应用 =====
export async function mount(props) {
  const { eventBus } = props
  
  // 监听事件
  eventBus.on('user:login', (user) => {
    console.log('用户登录:', user)
  })
  
  // 触发事件
  eventBus.emit('order:create', { orderId: 123 })
}
c) 共享 Pinia Store (Vue 专属,推荐)
// ===== 主应用 =====
import { createPinia } from 'pinia'

const pinia = createPinia()
app.use(pinia)

registerMicroApps([
  {
    name: 'app1',
    entry: '//localhost:8081',
    container: '#container',
    props: { pinia } // 传递 Pinia 实例
  }
])

// ===== 子应用 (Vue3) =====
import { defineStore } from 'pinia'

export async function mount(props) {
  const { pinia } = props
  
  // 使用主应用的 Pinia 实例
  const useUserStore = defineStore('user', {
    state: () => ({ name: '', token: '' })
  })
  
  app.use(pinia) // 注入主应用的 Pinia
  
  const userStore = useUserStore(pinia)
  console.log(userStore.name) // 可访问主应用的状态
}

3.1 核心挑战

  1. 全局 API 冲突:Vue2 和 Vue3 都会在 window 上挂载 window.Vue
  2. 依赖版本冲突:Element UI (Vue2) vs Element Plus (Vue3)
  3. 路由冲突:Vue Router 3 vs Vue Router 4

3.2 解决方案

a) 通过 qiankun 沙箱隔离
// 主应用使用 Vue3
import { createApp } from 'vue'
import App from './App.vue'

createApp(App).mount('#main-app')

// 注册 Vue2 子应用
registerMicroApps([
  {
    name: 'vue2-app',
    entry: '//localhost:8081',
    container: '#subapp-container',
    activeRule: '/vue2'
  }
])

// 注册 Vue3 子应用
registerMicroApps([
  {
    name: 'vue3-app',
    entry: '//localhost:8082',
    container: '#subapp-container',
    activeRule: '/vue3'
  }
])

start({
  sandbox: {
    strictStyleIsolation: false,
    experimentalStyleIsolation: true
  }
})

qiankun 的沙箱会自动隔离

  • Vue2 子应用的 window.Vue 不会影响 Vue3
  • 两个子应用的路由实例互不干扰
b) 子应用配置 (Vue2 子应用示例)
// ===== Vue2 子应用入口文件 =====
import Vue from 'vue'
import VueRouter from 'vue-router'
import ElementUI from 'element-ui'
import 'element-ui/lib/theme-chalk/index.css'
import App from './App.vue'

Vue.use(VueRouter)
Vue.use(ElementUI)

let instance = null
let router = null

// 导出 qiankun 生命周期
export async function bootstrap() {
  console.log('Vue2 子应用 bootstrap')
}

export async function mount(props) {
  console.log('Vue2 子应用 mount', props)
  
  // 创建路由实例
  router = new VueRouter({
    mode: 'history',
    base: window.__POWERED_BY_QIANKUN__ ? '/vue2' : '/',
    routes: [
      { path: '/order', component: () => import('./views/Order.vue') }
    ]
  })
  
  // 创建 Vue 实例
  instance = new Vue({
    router,
    render: h => h(App)
  }).$mount(props.container ? props.container.querySelector('#app') : '#app')
}

export async function unmount() {
  instance.$destroy()
  instance.$el.innerHTML = ''
  instance = null
  router = null
}

// 独立运行时直接挂载
if (!window.__POWERED_BY_QIANKUN__) {
  mount({})
}
c) 依赖共享与隔离

推荐策略

  1. 公共依赖不共享:Vue2 和 Vue3 各自打包,避免版本冲突
  2. 工具库可共享:如 axioslodash 可通过 externals 配置共享
// Vue2 子应用 vue.config.js
module.exports = {
  configureWebpack: {
    externals: {
      // axios 共享,从主应用加载
      axios: 'axios'
    },
    output: {
      library: 'vue2App',
      libraryTarget: 'umd',
      jsonpFunction: `webpackJsonp_vue2App` // 避免 chunk 冲突
    }
  }
}
// 主应用 index.html 提供共享依赖
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>

3.3 最佳实践清单

实践说明重要性
避免全局污染子应用不要在 window 上挂载变量⭐⭐⭐⭐⭐
路由 base 配置子应用路由 base 需根据运行环境动态设置⭐⭐⭐⭐⭐
静态资源 publicPath配置 __webpack_public_path__ 为动态值⭐⭐⭐⭐⭐
弹窗组件挂载点确保弹窗渲染在子应用容器内⭐⭐⭐⭐
生命周期规范必须导出 bootstrap/mount/unmount⭐⭐⭐⭐⭐
独立运行能力子应用需支持脱离 qiankun 独立运行⭐⭐⭐⭐
// 动态 publicPath 配置
if (window.__POWERED_BY_QIANKUN__) {
  __webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__
}

┌─────────────────────────────────────────────────┐
│               主应用 (Vue3)                      │
│  - 路由控制、权限管理、全局布局                    │
│  - qiankun 加载器                                │
│  - 公共组件库、工具函数                            │
└──────────────┬──────────────────────────────────┘
               │
      ┌────────┼────────┬────────┐
      │        │        │        │
┌─────▼───┐ ┌─▼─────┐ ┌▼──────┐ ┌▼──────────┐
│ Vue2    │ │ Vue3  │ │ React │ │ Angular   │
│ 订单模块 │ │ 库存  │ │ 财务  │ │ 用户中心  │
│         │ │ 模块  │ │ 模块  │ │           │
│ Element │ │ Elem+ │ │ AntD  │ │ Material  │
│ UI      │ │       │ │       │ │           │
└─────────┘ └───────┘ └───────┘ └───────────┘

主应用配置

// main.ts
import { registerMicroApps, start, initGlobalState } from 'qiankun'
import { createPinia } from 'pinia'

const pinia = createPinia()

// 初始化全局状态
const actions = initGlobalState({
  user: null,
  token: localStorage.getItem('token')
})

// 注册子应用
registerMicroApps([
  {
    name: 'order-app',
    entry: '//localhost:8081', // Vue2
    container: '#subapp-viewport',
    activeRule: '/order',
    props: { pinia, actions }
  },
  {
    name: 'stock-app',
    entry: '//localhost:8082', // Vue3
    container: '#subapp-viewport',
    activeRule: '/stock',
    props: { pinia, actions }
  },
  {
    name: 'finance-app',
    entry: '//localhost:8083', // React
    container: '#subapp-viewport',
    activeRule: '/finance',
    props: { actions }
  }
], {
  beforeLoad: app => console.log('加载前', app.name),
  beforeMount: app => console.log('挂载前', app.name),
  afterMount: app => console.log('挂载后', app.name),
  beforeUnmount: app => console.log('卸载前', app.name),
  afterUnmount: app => console.log('卸载后', app.name)
})

// 启动 qiankun
start({
  sandbox: {
    strictStyleIsolation: false,
    experimentalStyleIsolation: true
  },
  prefetch: 'all', // 预加载所有子应用
  fetch: (url, ...args) => {
    // 自定义 fetch,可添加 token
    return window.fetch(url, {
      ...args,
      headers: {
        ...args[0]?.headers,
        'Authorization': `Bearer ${localStorage.getItem('token')}`
      }
    })
  }
})

场景推荐方案原因
多团队、多技术栈qiankun成熟、支持任意框架混用
同技术栈、深度集成Module Federation性能最优、原生支持依赖共享
快速接入、中小项目Micro-App使用简单、接入成本低
集成第三方系统iframe完全隔离、安全性高
  1. 渐进式改造:先从边缘模块试点,积累经验后再推广
  2. 文档先行:制定《微前端接入规范》,明确生命周期、通信方式、样式规范
  3. 基建工具:提供子应用脚手架、公共组件库、部署脚本
  4. 监控告警:接入性能监控(子应用加载时间、错误上报)
  5. 灰度发布:支持子应用独立灰度,降低上线风险
问题原因解决方案
路由切换后子应用白屏未正确实现 unmount确保 unmount 中销毁实例、清空 DOM
弹窗显示在错误位置弹窗挂载到 body 外配置弹窗 getContainer 指向子应用容器
样式丢失publicPath 配置错误动态设置 __webpack_public_path__
子应用间数据不同步未使用全局状态管理使用 qiankun 的 initGlobalState 或共享 Pinia
Vue2/Vue3 冲突沙箱未生效检查 qiankun 版本、确保启用沙箱