返回笔记首页

qiankun 深度应用(下)- 关键问题解决与实战

主题配置

三、关键问题解决

3.1 路由劫持与同步

问题描述

主应用和子应用都有自己的路由系统,如何保证路由同步?浏览器前进后退如何处理?

解决方案

1. 主应用路由劫持
javascript
// 主应用 main.js
import { registerMicroApps, start } from 'qiankun'
import { createRouter, createWebHistory } from 'vue-router'

const router = createRouter({
  history: createWebHistory(),
  routes: [
    {
      path: '/',
      component: Home
    },
    // 子应用路由占位
    {
      path: '/app1/:pathMatch(.*)*',
      name: 'app1',
      component: () => import('./views/SubAppContainer.vue')
    }
  ]
})

// qiankun 会自动劫持 history API
registerMicroApps([
  {
    name: 'app1',
    entry: '//localhost:8081',
    container: '#subapp-container',
    activeRule: '/app1',
    props: {
      // 传递路由 base
      routerBase: '/app1'
    }
  }
])

start()
2. 子应用路由配置
javascript
// 子应用 main.js
import { createRouter, createWebHistory } from 'vue-router'

function render(props = {}) {
  const { container, routerBase } = props

  // 使用主应用传递的 routerBase
  const router = createRouter({
    history: createWebHistory(routerBase || '/app1'),
    routes: [
      {
        path: '/',
        component: Home
      },
      {
        path: '/list',
        component: List
      },
      {
        path: '/detail/:id',
        component: Detail
      }
    ]
  })

  const app = createApp(App)
  app.use(router)
  app.mount(container ? container.querySelector('#app') : '#app')
}
3. 路由监听与转发
javascript
// 主应用监听路由变化
import { useRouter } from 'vue-router'

const router = useRouter()

router.afterEach((to, from) => {
  console.log('主应用路由变化', to.path)

  // 可以在这里做一些全局处理
  // 比如记录路由历史、埋点等
})

// 子应用路由变化会自动同步到浏览器地址栏
// 因为 qiankun 劫持了 history API
4. 处理浏览器前进后退
javascript
// qiankun 自动处理,无需额外配置
// 原理:劫持了 pushState、replaceState、popstate

// 如果需要自定义处理
window.addEventListener('popstate', (event) => {
  console.log('浏览器前进/后退', event.state)
})
5. 路由守卫处理
javascript
// 主应用全局路由守卫
router.beforeEach((to, from, next) => {
  // 检查用户权限
  const token = localStorage.getItem('token')

  if (!token && to.path !== '/login') {
    next('/login')
    return
  }

  // 检查子应用权限
  if (to.path.startsWith('/app1')) {
    const hasApp1Permission = checkPermission('app1')
    if (!hasApp1Permission) {
      next('/403')
      return
    }
  }

  next()
})

// 子应用路由守卫
const router = createRouter({...})

router.beforeEach((to, from, next) => {
  // 子应用内部路由守卫
  console.log('子应用路由守卫', to.path)
  next()
})

完整示例代码

vue
<!-- 主应用 App.vue -->
<template>
  <div id="main-app">
    <nav>
      <router-link to="/">首页</router-link>
      <router-link to="/app1">子应用1</router-link>
      <router-link to="/app1/list">子应用1-列表</router-link>
      <router-link to="/app1/detail/123">子应用1-详情</router-link>
    </nav>

    <div class="content">
      <router-view v-if="!isSubApp"></router-view>
      <div id="subapp-container" v-show="isSubApp"></div>
    </div>

    <div class="route-info">
      <p>当前路由: {{ currentRoute }}</p>
      <p>路由历史: {{ routeHistory.join(' -> ') }}</p>
    </div>
  </div>
</template>

<script setup>
import { ref, computed, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router'

const route = useRoute()
const router = useRouter()

const currentRoute = computed(() => route.path)
const routeHistory = ref([route.path])

const isSubApp = computed(() => {
  return route.path.startsWith('/app1') ||
         route.path.startsWith('/app2')
})

// 记录路由历史
watch(() => route.path, (newPath) => {
  if (routeHistory.value[routeHistory.value.length - 1] !== newPath) {
    routeHistory.value.push(newPath)
    if (routeHistory.value.length > 10) {
      routeHistory.value.shift()
    }
  }
})
</script>

3.2 全局状态共享 (initGlobalState)

问题描述

主应用和子应用之间如何共享状态?如用户信息、权限数据、主题配置等。

解决方案

1. 初始化全局状态
javascript
// 主应用 main.js
import { initGlobalState } from 'qiankun'

// 初始化状态
const initialState = {
  user: {
    name: 'Admin',
    role: 'admin',
    avatar: 'https://xxx.com/avatar.jpg'
  },
  theme: 'light',
  token: localStorage.getItem('token'),
  permissions: ['user:read', 'user:write']
}

const actions = initGlobalState(initialState)

// 监听全局状态变化
actions.onGlobalStateChange((state, prev) => {
  console.log('全局状态变化', state, prev)

  // 状态变化时的业务逻辑
  if (state.token !== prev.token) {
    if (state.token) {
      console.log('用户登录')
    } else {
      console.log('用户登出')
      router.push('/login')
    }
  }
})

// 修改全局状态
actions.setGlobalState({
  user: { name: 'New User' }
})

// 获取全局状态
const currentState = actions.getGlobalState()

// 注销监听
// actions.offGlobalStateChange()
2. 传递给子应用
javascript
// 主应用注册子应用
registerMicroApps([
  {
    name: 'app1',
    entry: '//localhost:8081',
    container: '#subapp-container',
    activeRule: '/app1',
    props: {
      // 传递全局状态的方法
      getGlobalState: actions.getGlobalState,
      setGlobalState: actions.setGlobalState,
      onGlobalStateChange: actions.onGlobalStateChange,
      offGlobalStateChange: actions.offGlobalStateChange
    }
  }
])
3. 子应用使用全局状态
javascript
// 子应用 main.js
let globalStateActions = null

export async function mount(props) {
  const {
    getGlobalState,
    setGlobalState,
    onGlobalStateChange,
    offGlobalStateChange
  } = props

  // 保存全局状态操作方法
  globalStateActions = {
    getGlobalState,
    setGlobalState,
    onGlobalStateChange,
    offGlobalStateChange
  }

  // 获取全局状态
  const globalState = getGlobalState()
  console.log('子应用获取全局状态', globalState)

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

    // 可以同步到子应用的 store
    if (window.__APP_STORE__) {
      window.__APP_STORE__.commit('updateUser', state.user)
    }
  }, true) // true 表示立即执行一次

  render(props)
}

export async function unmount() {
  // 卸载时注销监听
  if (globalStateActions) {
    globalStateActions.offGlobalStateChange()
    globalStateActions = null
  }

  instance.unmount()
}

// 子应用内修改全局状态
function updateUserInfo(newUser) {
  if (globalStateActions) {
    globalStateActions.setGlobalState({
      user: newUser
    })
  }
}
4. 结合 Pinia/Vuex 使用
javascript
// 主应用 store
import { createPinia } from 'pinia'
import { initGlobalState } from 'qiankun'

const pinia = createPinia()

// 用户 store
export const useUserStore = defineStore('user', {
  state: () => ({
    user: null,
    token: null
  }),
  actions: {
    setUser(user) {
      this.user = user
      // 同步到全局状态
      if (window.__QIANKUN_ACTIONS__) {
        window.__QIANKUN_ACTIONS__.setGlobalState({ user })
      }
    },
    setToken(token) {
      this.token = token
      localStorage.setItem('token', token)
      // 同步到全局状态
      if (window.__QIANKUN_ACTIONS__) {
        window.__QIANKUN_ACTIONS__.setGlobalState({ token })
      }
    }
  }
})

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

// 保存到 window 供其他地方使用
window.__QIANKUN_ACTIONS__ = actions

// 监听全局状态变化,同步到 pinia
actions.onGlobalStateChange((state) => {
  const userStore = useUserStore()
  if (state.user) userStore.user = state.user
  if (state.token) userStore.token = state.token
})
5. 完整通信示例
vue
<!-- 主应用组件 -->
<template>
  <div class="main-app">
    <div class="user-panel">
      <h3>用户信息</h3>
      <p>姓名: {{ globalState.user?.name }}</p>
      <p>角色: {{ globalState.user?.role }}</p>
      <button @click="updateUser">更新用户</button>
      <button @click="logout">退出登录</button>
    </div>

    <div class="theme-switch">
      <label>主题:</label>
      <select v-model="globalState.theme" @change="changeTheme">
        <option value="light">浅色</option>
        <option value="dark">深色</option>
      </select>
    </div>
  </div>
</template>

<script setup>
import { reactive, onMounted } from 'vue'

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

onMounted(() => {
  // 监听全局状态
  if (window.__QIANKUN_ACTIONS__) {
    const state = window.__QIANKUN_ACTIONS__.getGlobalState()
    Object.assign(globalState, state)

    window.__QIANKUN_ACTIONS__.onGlobalStateChange((state) => {
      Object.assign(globalState, state)
    })
  }
})

const updateUser = () => {
  window.__QIANKUN_ACTIONS__?.setGlobalState({
    user: {
      name: 'Updated User',
      role: 'admin'
    }
  })
}

const changeTheme = () => {
  window.__QIANKUN_ACTIONS__?.setGlobalState({
    theme: globalState.theme
  })
  document.body.setAttribute('data-theme', globalState.theme)
}

const logout = () => {
  window.__QIANKUN_ACTIONS__?.setGlobalState({
    user: null,
    token: null
  })
  localStorage.removeItem('token')
}
</script>

3.3 样式隔离方案

问题描述

主应用和子应用的样式可能冲突,如何实现样式隔离?

三种方案对比

方案1: Shadow DOM (严格隔离)
javascript
// 主应用启动配置
start({
  sandbox: {
    strictStyleIsolation: true
  }
})

// 优点:完全隔离,最彻底
// 缺点:某些组件库不支持(如 antd Modal)
解决 Modal 等弹窗问题
javascript
// 子应用中
import { Modal } from 'ant-design-vue'

// 方法1: 修改 getContainer
Modal.confirm({
  title: '提示',
  content: '确认操作?',
  getContainer: () => document.getElementById('app') // 挂载到子应用容器内
})

// 方法2: 全局配置
import { ConfigProvider } from 'ant-design-vue'

app.use(ConfigProvider, {
  getPopupContainer: (node) => {
    return node?.parentNode || document.getElementById('app')
  }
})
方案2: Scoped CSS (宽松隔离)
javascript
// 主应用启动配置
start({
  sandbox: {
    experimentalStyleIsolation: true
  }
})

// 原理:自动添加属性选择器
// 原始: .title { color: red; }
// 处理后: div[data-qiankun-app1] .title { color: red; }

// 优点:兼容性好,性能好
// 缺点:隔离不彻底,可能有样式泄露
方案3: 手动添加命名空间
vue
<!-- 子应用根组件 -->
<template>
  <div class="app1-container">
    <!-- 所有样式都加前缀 -->
    <div class="app1-title">标题</div>
    <div class="app1-content">内容</div>
  </div>
</template>

<style scoped>
/* 使用 scoped 或手动加前缀 */
.app1-container {
  /* 样式 */
}

.app1-title {
  color: #333;
}

.app1-content {
  font-size: 14px;
}
</style>
方案4: CSS Modules
vue
<template>
  <div :class="$style.container">
    <h1 :class="$style.title">标题</h1>
  </div>
</template>

<style module>
.container {
  padding: 20px;
}

.title {
  font-size: 24px;
}
</style>
方案5: 动态样式加载/卸载
javascript
// 子应用 mount 时加载样式
export async function mount(props) {
  // 创建样式节点
  const styleNodes = []

  // 加载样式
  const styles = ['style1.css', 'style2.css']
  styles.forEach(href => {
    const link = document.createElement('link')
    link.rel = 'stylesheet'
    link.href = href
    document.head.appendChild(link)
    styleNodes.push(link)
  })

  // 保存样式节点引用
  window.__APP_STYLE_NODES__ = styleNodes

  render(props)
}

// 子应用 unmount 时卸载样式
export async function unmount() {
  // 移除样式节点
  if (window.__APP_STYLE_NODES__) {
    window.__APP_STYLE_NODES__.forEach(node => {
      document.head.removeChild(node)
    })
    window.__APP_STYLE_NODES__ = null
  }

  instance.unmount()
}

推荐方案

javascript
// 组合使用效果最佳

// 1. 主应用:开启实验性样式隔离
start({
  sandbox: {
    experimentalStyleIsolation: true
  }
})

// 2. 子应用:使用 scoped 或 CSS Modules
<style scoped>
/* 子应用样式 */
</style>

// 3. 公共样式:放在主应用,子应用可以继承
// 主应用 global.css
:root {
  --primary-color: #1890ff;
  --font-size: 14px;
}

// 4. 弹窗组件:指定挂载容器
Modal.config({
  getContainer: () => document.getElementById('app')
})

3.4 静态资源路径问题 (publicPath)

问题描述

子应用部署后,图片、字体等静态资源 404,路径不对。

解决方案

1. 配置 publicPath
javascript
// 子应用 public-path.js
if (window.__POWERED_BY_QIANKUN__) {
  // qiankun 环境,使用动态 publicPath
  __webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__
} else {
  // 独立运行,使用固定 publicPath
  __webpack_public_path__ = '/'
}

// main.js 顶部引入
import './public-path'
2. Webpack 配置
javascript
// vue.config.js
module.exports = {
  publicPath: process.env.NODE_ENV === 'production'
    ? '/app1/' // 生产环境使用子应用的部署路径
    : '/', // 开发环境使用根路径

  devServer: {
    port: 8081,
    headers: {
      'Access-Control-Allow-Origin': '*'
    }
  },

  configureWebpack: {
    output: {
      library: `app1-[name]`,
      libraryTarget: 'umd',
      jsonpFunction: `webpackJsonp_app1`
    }
  }
}
3. 运行时设置
javascript
// 子应用 main.js
export async function mount(props) {
  // 方式1: 从 props 获取
  const { publicPath } = props
  if (publicPath) {
    __webpack_public_path__ = publicPath
  }

  // 方式2: 从主应用配置获取
  if (window.__QIANKUN_PUBLIC_PATH__) {
    __webpack_public_path__ = window.__QIANKUN_PUBLIC_PATH__
  }

  render(props)
}
4. 主应用配置
javascript
// 主应用注册子应用
registerMicroApps([
  {
    name: 'app1',
    entry: process.env.NODE_ENV === 'production'
      ? 'https://cdn.example.com/app1/' // 生产环境 CDN
      : '//localhost:8081', // 开发环境本地
    container: '#subapp-container',
    activeRule: '/app1',
    props: {
      publicPath: 'https://cdn.example.com/app1/'
    }
  }
])
5. 图片路径处理
vue
<template>
  <div>
    <!-- 方式1: 使用 require 动态引入 -->
    <img :src="require('@/assets/logo.png')" />

    <!-- 方式2: 放在 public 目录,使用绝对路径 -->
    <img :src="`${publicPath}logo.png`" />

    <!-- 方式3: 使用 CDN -->
    <img src="https://cdn.example.com/logo.png" />
  </div>
</template>

<script setup>
import { ref } from 'vue'

const publicPath = ref(window.__QIANKUN_PUBLIC_PATH__ || '/')
</script>
6. 字体文件处理
css
/* 方式1: 使用相对路径 */
@font-face {
  font-family: 'MyFont';
  src: url('./fonts/myfont.woff2') format('woff2');
}

/* 方式2: 使用 CDN */
@font-face {
  font-family: 'MyFont';
  src: url('https://cdn.example.com/fonts/myfont.woff2') format('woff2');
}

3.5 子应用独立运行配置

问题描述

子应用既要能在微前端环境运行,也要能独立运行和开发。

完整解决方案

javascript
// 子应用 main.js
import { createApp } from 'vue'
import { createRouter, createWebHistory } from 'vue-router'
import App from './App.vue'
import routes from './router'
import './public-path'

let instance = null
let router = null

// 渲染函数
function render(props = {}) {
  const { container, routerBase } = props

  // 创建路由
  router = createRouter({
    // qiankun 环境使用主应用传入的 base,独立运行使用根路径
    history: createWebHistory(
      window.__POWERED_BY_QIANKUN__ ? routerBase : '/'
    ),
    routes
  })

  // 创建应用
  instance = createApp(App)
  instance.use(router)

  // 如果有 pinia/vuex,也要在这里注册
  // instance.use(pinia)

  // 挂载
  const containerEl = container
    ? container.querySelector('#app')
    : document.querySelector('#app')
  instance.mount(containerEl)
}

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

// qiankun 生命周期
export async function bootstrap() {
  console.log('[app1] bootstrap')
}

export async function mount(props) {
  console.log('[app1] mount', props)
  render(props)
}

export async function unmount() {
  console.log('[app1] unmount')
  instance.unmount()
  instance = null
  router = null
}

export async function update(props) {
  console.log('[app1] update', props)
}
环境判断逻辑
javascript
// utils/env.js
export const isQiankun = () => {
  return window.__POWERED_BY_QIANKUN__
}

export const getBaseURL = () => {
  if (isQiankun()) {
    return window.__QIANKUN_BASE_URL__ || '/api'
  }
  return process.env.VUE_APP_BASE_URL || '/api'
}

export const getPublicPath = () => {
  if (isQiankun()) {
    return window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__
  }
  return '/'
}

// 使用
import { isQiankun, getBaseURL } from '@/utils/env'

const apiClient = axios.create({
  baseURL: getBaseURL()
})

if (isQiankun()) {
  // 微前端环境特殊处理
  console.log('运行在微前端环境')
} else {
  // 独立运行
  console.log('独立运行')
}
开发环境配置
javascript
// vue.config.js
module.exports = {
  devServer: {
    port: 8081,
    headers: {
      'Access-Control-Allow-Origin': '*'
    },
    // 独立开发时的代理配置
    proxy: {
      '/api': {
        target: 'http://localhost:3000',
        changeOrigin: true
      }
    }
  },

  configureWebpack: {
    output: {
      // 开发环境也配置 library,方便调试
      library: `app1-[name]`,
      libraryTarget: 'umd',
      jsonpFunction: `webpackJsonp_app1`
    }
  }
}
独立运行时的入口 HTML
html
<!-- public/index.html -->
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>子应用1 - 独立运行</title>
</head>
<body>
  <div id="app"></div>

  <!-- 独立运行时可以加载一些全局依赖 -->
  <script>
    // 标记独立运行环境
    window.__INDEPENDENT_RUN__ = true
  </script>
</body>
</html>

四、简历撰写指南

4.1 项目经验描述模板

项目名称: qiankun 微前端架构落地实践

项目时间: 2024.06 - 2024.12

项目描述: 负责公司核心业务系统的微前端改造,采用 qiankun 框架将单体应用拆分为主应用 + 8 个子应用。实现了各业务线的技术独立和团队自治,解决了大型单体应用的开发、部署、维护难题。

核心职责

  1. qiankun 技术选型与架构设计
    • 深入研究 qiankun 核心原理:single-spa、import-html-entry、沙箱隔离机制
    • 设计主子应用通信方案、路由管理方案、状态共享方案
    • 制定代码规范、开发流程、部署策略
  2. 主应用架构实现
    • 实现应用注册、生命周期管理、全局状态管理(initGlobalState)
    • 开发统一的 Layout、导航菜单、权限控制、错误边界
    • 实现子应用预加载、缓存策略,首屏加载优化
  3. 关键技术问题解决
    • JS 沙箱隔离:启用 Proxy 沙箱,解决全局变量污染,支持多实例运行
    • CSS 样式隔离:采用 experimentalStyleIsolation + scoped 组合方案
    • 路由劫持与同步:实现主子应用路由无缝切换,支持浏览器前进后退
    • 静态资源路径:通过 publicPath 动态配置解决资源 404 问题
    • 全局状态共享:基于 initGlobalState 实现主子应用数据通信
  4. 子应用改造指导
    • 制定生命周期规范(bootstrap/mount/unmount)
    • 规范 webpack 配置(library/libraryTarget/publicPath)
    • 实现子应用独立运行和微前端运行双模式
  5. 工程化建设
    • 开发脚手架工具,快速创建符合规范的子应用
    • 建立 CI/CD 流程,实现子应用独立部署
    • 实现监控系统,监控子应用加载性能、错误率

技术栈: qiankun、single-spa、Vue3、Webpack、import-html-entry、Proxy

项目成果
  • 构建时间从 15 分钟降至单应用 3 分钟,效率提升 80%
  • 各业务线独立开发部署,团队协作效率提升 50%
  • 发布频率从月发布提升到周发布,迭代速度提升 4 倍
  • 故障影响面降低 80%,单个子应用故障不影响其他模块
  • 支持技术栈逐步升级,已有 3 个子应用从 Vue2 升级到 Vue3
  • 新人上手时间从 2 周缩短到 3 天,培训成本降低 70%

4.2 SOP 标准回答话术

面试官:详细说说 qiankun 的沙箱隔离原理

回答话术: "好的。qiankun 的沙箱隔离是它的核心能力之一,主要解决不同子应用的 JS 执行环境隔离问题。

qiankun 提供了三种沙箱方案:

第一种是 ProxySandbox,这是默认方案也是最推荐的。它的原理是用 Proxy 代理 window 对象。具体来说,qiankun 会为每个子应用创建一个 fakeWindow 对象,然后用 Proxy 代理这个对象。当子应用访问 window.xxx 时,实际访问的是代理对象。如果这个属性是子应用自己设置的,就从 fakeWindow 取;如果不是,就从真实 window 取。这样子应用的全局变量都记录在 fakeWindow 里,不会污染真实 window,实现了完全隔离。而且这个方案支持多实例,性能也很好。

第二种是 SnapshotSandbox,快照沙箱。它的原理比较简单:应用激活时,遍历 window 对象做一个快照;应用失活时,对比快照和当前 window,把变化的属性恢复回去。这个方案的优势是兼容性好,支持 IE11,但缺点是性能差(需要遍历 window)、不支持多实例、而且会暂时污染全局 window。

第三种是 LegacySandbox,遗留沙箱,是 Proxy 和快照的结合。它用 Proxy 记录所有变更,同时还是会修改真实 window,只不过在失活时会还原。这个方案主要是为了兼容一些特殊场景。

我们项目用的是 ProxySandbox,因为我们不需要兼容 IE,而且需要支持多个子应用同时运行的场景。实测下来隔离效果很好,没有出现全局变量污染的问题。

不过沙箱也不是万能的,比如一些直接操作 DOM 的第三方库,或者往 document 上绑定事件的,沙箱是管不到的。这种情况需要在子应用 unmount 时手动清理。"

面试官:qiankun 的样式隔离是如何实现的?

回答话术: "qiankun 提供了两种样式隔离方案:strictStyleIsolation 和 experimentalStyleIsolation。

strictStyleIsolation 是严格样式隔离,用的是 Shadow DOM。原理是把子应用的内容挂载到一个 Shadow Root 里面,Shadow DOM 有天然的样式隔离能力,里面的样式不会泄露出来,外面的样式也进不去。这是最彻底的隔离方案。

但 Shadow DOM 也有缺点。最大的问题是某些组件库不支持,比如 antd 的 Modal、Tooltip 这些弹窗组件,默认会挂载到 document.body,在 Shadow DOM 里就找不到样式了。解决办法是修改组件的 getContainer,让它挂载到子应用容器里面。但这需要改很多代码,比较麻烦。

所以我们项目用的是 experimentalStyleIsolation,实验性样式隔离。它的原理是给子应用的所有样式选择器加一个属性前缀。比如原来是 .title { color: red },处理后变成 div[data-qiankun-app1] .title { color: red }。同时给子应用的容器加上这个属性 data-qiankun-app1。这样样式就只对子应用内的元素生效了。

这个方案兼容性好,性能也好,大部分场景都够用。但它不是完全隔离,如果子应用写了很宽泛的选择器,比如 * { margin: 0 },还是可能影响主应用。

所以我们的最佳实践是:

  1. 开启 experimentalStyleIsolation
  2. 子应用使用 scoped 或 CSS Modules
  3. 公共样式放主应用
  4. 弹窗组件指定挂载容器

这样组合使用,既保证了隔离性,又有很好的兼容性。"

4.3 难点与亮点分析

难点1:子应用间通信

问题描述: 两个子应用之间需要通信,比如子应用 A 创建了一个订单,子应用 B 需要刷新订单列表。

解决方案
javascript
// 方案1: 通过主应用中转
// 主应用
const actions = initGlobalState({ events: {} })

actions.onGlobalStateChange((state, prev) => {
  if (state.events.orderCreated) {
    console.log('订单创建事件', state.events.orderCreated)
    // 可以在这里做统一的事件分发
  }
})

// 子应用 A 触发事件
actions.setGlobalState({
  events: {
    orderCreated: { orderId: '123', timestamp: Date.now() }
  }
})

// 子应用 B 监听事件
actions.onGlobalStateChange((state) => {
  if (state.events.orderCreated) {
    refreshOrderList()
  }
})

// 方案2: 使用 EventBus
// 主应用提供 EventBus
class EventBus {
  constructor() {
    this.events = {}
  }

  on(event, handler) {
    if (!this.events[event]) {
      this.events[event] = []
    }
    this.events[event].push(handler)
  }

  emit(event, data) {
    if (this.events[event]) {
      this.events[event].forEach(handler => handler(data))
    }
  }

  off(event, handler) {
    if (this.events[event]) {
      this.events[event] = this.events[event].filter(h => h !== handler)
    }
  }
}

window.__EVENT_BUS__ = new EventBus()

// 子应用 A
window.__EVENT_BUS__.emit('orderCreated', { orderId: '123' })

// 子应用 B
window.__EVENT_BUS__.on('orderCreated', (data) => {
  console.log('订单创建', data)
  refreshOrderList()
})

难点2:公共依赖处理

问题描述: Vue、Vue Router、Axios 等公共库,每个子应用都打包会很大。

解决方案
javascript
// 主应用 index.html 引入公共依赖
<script src="https://cdn.jsdelivr.net/npm/vue@3"></script>
<script src="https://cdn.jsdelivr.net/npm/vue-router@4"></script>
<script src="https://cdn.jsdelivr.net/npm/axios@1"></script>

// 子应用 webpack 配置
configureWebpack: {
  externals: {
    vue: 'Vue',
    'vue-router': 'VueRouter',
    axios: 'axios'
  }
}

// 版本管理
// 建立公共依赖版本表
{
  "vue": "3.3.4",
  "vue-router": "4.2.4",
  "axios": "1.5.0"
}

// 子应用必须遵守版本约定

难点3:首屏加载优化

问题描述: 微前端架构下,首屏需要加载主应用 + 子应用,时间长。

解决方案
javascript
// 1. 预加载
start({
  prefetch: true, // 自动预加载
  // 或自定义预加载策略
  prefetch: [
    { name: 'app1', force: true }, // 强制预加载
    'app2' // 空闲时预加载
  ]
})

// 2. 手动预加载高频子应用
import { prefetchApps } from 'qiankun'

prefetchApps([
  { name: 'app1', entry: '//localhost:8081' }
])

// 3. 缓存机制
// qiankun 默认会缓存子应用资源
// 可以通过 import-html-entry 的 cache 参数控制

// 4. 代码分割
// 子应用使用路由懒加载
const routes = [
  {
    path: '/list',
    component: () => import('./views/List.vue')
  }
]

// 5. 关键路径优化
// 主应用只加载必要的代码
// 非关键功能延迟加载

亮点1:渐进式改造

不是一次性重构,而是渐进式迁移:

  1. 先抽离最独立的模块作为子应用
  2. 主应用保留核心功能继续运行
  3. 验证方案可行性后逐步迁移
  4. 最后清理旧代码

这种策略风险可控,业务不中断。

亮点2:完整的监控体系

javascript
// 子应用加载监控
registerMicroApps(apps, {
  beforeLoad: [(app) => {
    console.time(`${app.name}-load`)
    return Promise.resolve()
  }],
  afterMount: [(app) => {
    console.timeEnd(`${app.name}-load`)
    // 上报加载时间
    reportMetrics({
      appName: app.name,
      loadTime: performance.now()
    })
    return Promise.resolve()
  }]
})

// 错误捕获
window.addEventListener('error', (event) => {
  if (event.filename.includes('app1')) {
    console.error('子应用1错误', event)
    reportError({
      app: 'app1',
      error: event.message
    })
  }
})

亮点3:开发工具链

bash
# 脚手架快速创建子应用
npm create micro-app my-app

# 自动配置 webpack
# 自动添加生命周期
# 自动配置 publicPath

# 一键启动所有应用
npm run dev:all

# 一键部署
npm run deploy:app1

4.4 完整简历示例

plain
【qiankun 微前端架构落地实践】2024.06 - 2024.12

项目背景:
公司核心业务系统代码量 30 万行,涉及 6 个业务线,3 个团队协作。存在构建慢、协作难、技术债重、发布风险高等问题。

技术选型:
对比 qiankun、Wujie、Micro-App 后选择 qiankun,原因:社区最活跃、生态最完善、适合企业级长期项目。

核心工作:
1. 技术方案设计
   - 深入研究 qiankun 核心原理:single-spa 生命周期、import-html-entry 加载机制、Proxy 沙箱隔离
   - 设计主应用架构:应用注册、路由管理、状态共享、权限控制
   - 制定拆分策略:按业务域拆分为 1 主应用 + 8 子应用

2. 关键技术攻坚
   - JS 沙箱隔离:启用 ProxySandbox,支持多实例,完全隔离全局变量
   - CSS 样式隔离:experimentalStyleIsolation + scoped 组合方案,兼容 antd 等组件库
   - 路由劫持同步:实现主子应用路由无缝切换,支持浏览器前进后退
   - 全局状态共享:基于 initGlobalState 实现主子应用通信,开发 EventBus 增强通信能力
   - 静态资源路径:通过动态 publicPath 解决部署后资源 404 问题

3. 工程化建设
   - 开发脚手架工具,快速创建符合规范的子应用,降低 80% 接入成本
   - 建立 CI/CD 流程,实现子应用独立构建部署
   - 实现监控系统:子应用加载性能、错误捕获、用户行为分析

4. 团队赋能
   - 编写技术文档和最佳实践指南
   - 组织技术分享和培训
   - Code Review 保证代码质量

技术栈:
qiankun、single-spa、Vue3、Webpack、import-html-entry、Proxy

项目成果:
- 构建时间:从 15 分钟降至单应用 3 分钟(提升 80%)
- 协作效率:各业务线独立开发部署,协作效率提升 50%,代码冲突减少 90%
- 迭代速度:发布频率从月发布提升到周发布(提升 4 倍)
- 故障影响:单个子应用故障不影响其他模块(故障面降低 80%)
- 技术升级:支持渐进式升级,已有 3 个子应用从 Vue2 升级到 Vue3
- 培训成本:新人上手时间从 2 周缩短到 3 天(降低 70%)
- 系统稳定性:线上故障率下降 60%,平均修复时间缩短 70%

五、总结

qiankun 是目前最成熟的微前端解决方案,核心优势:

  1. 基于 single-spa,生态完善
  2. 完善的沙箱隔离机制
  3. 灵活的样式隔离方案
  4. 强大的全局状态管理
  5. 活跃的社区支持

关键要点:

  1. 理解核心原理(single-spa + import-html-entry + 沙箱)
  2. 解决关键问题(路由、状态、样式、资源)
  3. 制定开发规范
  4. 建立监控体系
  5. 持续优化改进