iframe → wujie 迁移收益分析与子应用集成方案

一、背景

当前 CMCLink 平台存在两种微前端集成方案并行:

  • 旧方案:原生 iframe 嵌入 + postMessage 通信(mkt、doc、ibs-manage 等子应用在用)
  • 新方案:wujie iframe 沙箱 + bus 通信(template 子应用模板已验证,ibs-manage 已完成迁移试点)

本文档从决策层面工程层面两个维度分析:

  1. 为什么要从 iframe 迁移到 wujie?投入产出比如何?
  2. 如何将子应用集成复杂度降到最低,让不同部门愿意迁移?

二、旧 iframe 方案的真实痛点

以下痛点均来自 ibs-manage 子应用迁移前的实际代码,非理论推演。

2.1 通信机制:postMessage 的脆弱性

旧方案代码(子应用 App.vue):

// 子应用监听主应用消息
window.addEventListener('message', (event: MessageEvent) => {
  if (typeof event.data?.type === 'string') {
    if (event.data.type === 'router-change') {
      router.push({ path: event.data.payload.route })
    } else if (event.data.type === 'close-all-tab') {
      tagsViewStore.delAllViews()
    }
  }
})

// 主应用通知子应用
window.parent.postMessage({ type: 'router-change', payload: { route } }, origin)

问题

问题影响
消息类型是字符串魔法值,无 TypeScript 类型约束拼写错误不会编译报错,只能运行时排查
数据需要序列化(不支持函数、循环引用)复杂数据传递受限
跨域时 origin 校验容易出错安全隐患或消息丢失
每个子应用独立实现消息协议协议不统一,新增事件需要双端同步修改
无法追踪消息链路调试困难,console.log 满天飞

wujie 方案

// 统一的 EventEmitter 模式,有类型约束
bus.$emit('CHILD_ROUTE_CHANGE', { appName, path, name, query })
bus.$on('ROUTE_CHANGE_TO_CHILD', (data) => { router.push(data.path) })

2.2 路由同步:hack 堆叠

旧方案代码(子应用 App.vue):

// 路由恢复:3 层 fallback
const restoreRoute = () => {
  let targetPath = ''
  // 1. 尝试从父页面 URL 参数获取
  try {
    if (window.parent !== window && window.parent.location.hostname === window.location.hostname) {
      targetPath = new URLSearchParams(window.parent.location.search).get('childPath') || ''
    }
  } catch { /* 跨域失败 */ }
  // 2. 回退到 localStorage
  if (!targetPath) {
    targetPath = localStorage.getItem('ibs-manage-latest-path') || ''
  }
  // 3. 兜底空路径
  router.replace(targetPath || '')
}

问题

问题影响
跨域部署时 parent.location 不可访问路由恢复完全失效
localStorage 在多 Tab 场景下互相覆盖Tab A 刷新可能恢复到 Tab B 的路由
主应用 URL 不反映子应用当前路由无法通过 URL 分享/收藏具体页面
每个子应用独立实现恢复逻辑代码重复,bug 各异

wujie 方案

# 主应用 URL 自动同步子应用路由(sync 模式)
http://localhost:3000/ibs-manage/operation/xxx?ibs-manage={~}/operation/xxx

# F5 刷新时 wujie 自动从 URL query 恢复子应用路由,零代码

2.3 状态共享:Token 传递的安全隐患

旧方案

  • Token 通过 URL 参数传递给 iframe → 明文暴露在浏览器历史记录和服务器日志中
  • 或依赖同域 Cookie → 跨域部署时失效
  • 或通过 postMessage 传递 → 需要手动管理刷新/过期同步

wujie 方案

// 主应用通过 props 注入,子应用通过 __WUJIE.props 读取
// 内存传递,不经过 URL/Cookie,不序列化
const sharedAuth = (window as any).__WUJIE?.props?.$shared?.auth
// token、userInfo、permissions、menus 一次性获取

2.4 性能:首屏加载体验

指标iframe 方案wujie 方案
首次打开子应用2-5 秒白屏(iframe 从零加载 HTML + JS + CSS)<500ms(preloadApp 预加载 + alive 保活)
切换已访问子应用1-3 秒(iframe 重新加载或从 bfcache 恢复)瞬切(alive 模式保持 Vue 实例不销毁)
子应用内部路由切换正常正常
keep-alive 页面缓存❌ 不支持(iframe 销毁即丢失)✅ 支持(alive 模式 + 自动缓存同步)

2.5 开发体验

维度iframe 方案wujie 方案
DevTools 调试需切换 iframe context同一 DevTools 窗口
HMR 热更新正常正常
独立运行✅ 支持✅ 支持
联调成本高(需同时启动主应用 + 子应用,调试 postMessage 链路)低(bus 事件可在 DevTools 中直接观察)

三、迁移投入成本

3.1 一次性投入(已完成)

项目工时状态
@cmclink/micro-bootstrap 子应用启动器2 人天✅ 已完成
@cmclink/micro-bridge 通信桥接 + 注册表2 人天✅ 已完成
@cmclink/vite-config 统一构建配置1 人天✅ 已完成
主应用 AuthenticatedLayout WujieVue 容器1 人天✅ 已完成
主应用 shared-provider 状态广播1 人天✅ 已完成
template 子应用模板验证0.5 人天✅ 已完成
ibs-manage 迁移试点1 人天✅ 已完成
合计~8.5 人天已完成

3.2 单个子应用迁移成本

基于 ibs-manage 实际迁移数据:

步骤工时说明
改 vite.config.ts15 分钟替换为 createChildAppConfig
改 src/main.ts30 分钟替换为 bootstrapMicroApp
改 src/App.vue2-4 小时wujie 适配(共享数据注入 + 路由恢复)
改 src/router/index.ts15 分钟删除旧通知逻辑
改 env + package.json15 分钟端口 + 依赖
主应用配置15 分钟env + 路由 + 注册表
联调验证2-4 小时路由 + 状态 + Tab + 刷新
合计0.5-1.5 人天视子应用复杂度而定

3.3 长期维护成本对比

场景iframe 方案wujie 方案
新增通信事件双端各加 postMessage 处理(~30 行/事件)注册表加类型 + bus.$on(~5 行/事件)
新增子应用复制粘贴 ~150 行通信代码 + 调试适配bootstrapMicroApp 一行启动
修复路由同步 bug每个子应用独立排查统一在 micro-bridge 修复,所有子应用受益
升级 Vue/Router 版本每个子应用独立处理micro-bootstrap 统一兼容

四、风险评估

4.1 迁移风险

风险概率影响缓解措施
wujie 框架停止维护低(GitHub 活跃,腾讯开源)wujie 核心代码量小(~3000 行),可 fork 自维护
子应用版本不兼容micro-bootstrap 已放宽类型约束,支持 vue@3.4/3.5 共存
样式隔离不完美WebComponent shadowDOM 隔离 + teleported=false 弹窗隔离
迁移期间两套方案并存确定App.vue 中 isWujie / isInIframe 分支兼容,可渐进迁移

4.2 不迁移的风险

风险概率影响
postMessage 协议碎片化加剧高 — 新子应用接入成本持续增加
路由同步 bug 反复出现中 — 用户体验差,开发疲于修补
无法实现 keep-alive 缓存确定中 — 表单填写中途切换 Tab 数据丢失
首屏性能无法优化确定中 — 每次切换子应用白屏 2-5 秒

五、子应用集成简化方案(v2 提案)

5.1 问题:当前集成仍然太重

ibs-manage 迁移后,App.vue 中仍有 ~80 行 wujie 适配代码

injectSharedDataFromWujie()     — 25 行(从 props.$shared 注入 token/user/menus)
wujie 路由恢复                   — 15 行(await generateRoutes + router.replace)
isWujie 环境检测                 — 3document.title / localStorage 分支 — 10 行
旧 iframe 兼容逻辑               — 30

这些代码对每个子应用都是几乎相同的模板代码。如果其他部门(mkt、doc、finance 等)迁移时都要手动写这些,集成意愿度会很低。

5.2 目标:子应用 App.vue 零 wujie 感知

理想状态:子应用开发者完全不需要知道 wujie 的存在。App.vue 只写业务逻辑,所有微前端适配在 bootstrapMicroApp 中自动完成。

5.3 方案:micro-bootstrap 新增 sharedData 配置

BootstrapOptions 中新增一个配置项,让 micro-bootstrap 自动完成共享数据注入:

// ===== 子应用 main.ts(简化后)=====
bootstrapMicroApp({
  app: App,
  router,
  pinia: store,
  appId: '#ibs-manage',
  appName: 'ibs-manage',
  tagsViewStore: () => useTagsViewStore(store),
  plugins: [setupI18n, setupElementPlus, setupGlobCom],

  // 🆕 共享数据注入配置(micro-bootstrap 自动处理 wujie props → 本地缓存)
  sharedData: {
    // 缓存适配器:告诉 bootstrap 如何读写子应用的本地缓存
    cache: {
      get: (key: string) => wsCache.get(key),
      set: (key: string, value: any) => wsCache.set(key, value),
    },
    // 缓存 key 映射
    keys: {
      accessToken: 'ACCESS_TOKEN',
      refreshToken: 'REFRESH_TOKEN',
      user: 'USER',  // 对应 CACHE_KEY.USER
    },
  },

  // 🆕 动态路由注册回调(可选,有动态路由的子应用才需要)
  onBeforeMount: async ({ router, cache }) => {
    const userInfo = cache.get('USER')
    if (userInfo?.menus) {
      userStore.menus = userInfo.menus
      await generateRoutes()
    }
  },
})

子应用 App.vue 变化

  // ========== 应用初始化 ==========
  const init = async () => {
-   // wujie 环境:从主应用共享数据注入 token 和用户信息
-   if (isWujie) {
-     injectSharedDataFromWujie()
-   }
-
-   // 从缓存加载用户信息并生成动态路由
-   const userInfo = wsCache.get(CACHE_KEY.USER)
-   if (userInfo) {
-     if (userInfo.menus) {
-       userStore.menus = userInfo.menus
-       await generateRoutes()
-     }
-     if (userInfo.user) {
-       userInfoRef.value = userInfo.user
-     }
-   }
+   // 共享数据注入 + 动态路由注册已由 micro-bootstrap 自动处理
+   // 此处只需读取缓存中的用户信息用于 UI 显示
+   const userInfo = wsCache.get(CACHE_KEY.USER)
+   if (userInfo?.user) {
+     userInfoRef.value = userInfo.user
+   }

    if (getAccessToken()) {
      userStore.setUserInfoAction()
    }

-   // wujie 环境:动态路由注册后重新匹配当前路径
-   if (isWujie) {
-     const currentPath = router.currentRoute.value.fullPath
-     if (currentPath && currentPath !== '/') {
-       await router.replace(currentPath)
-     }
-   }
-
-   // 非 wujie 环境:从 localStorage / 父页面恢复路由
-   restoreRouteForLegacyMode()
+   // 路由恢复已由 micro-bootstrap 自动处理(wujie sync / localStorage fallback)
  }

减少 ~50 行 wujie 适配代码,App.vue 只剩纯业务逻辑。

5.4 micro-bootstrap 内部实现要点

// micro-bootstrap 内部(伪代码)
async function bootstrapMicroApp(options: BootstrapOptions) {
  const isWujie = !!(window as any).__WUJIE

  // ... 创建 app、安装插件 ...

  // 🆕 自动注入共享数据(mount 之前)
  if (isWujie && options.sharedData) {
    injectSharedData(options.sharedData)
  }

  // 🆕 执行用户自定义的 mount 前回调(动态路由注册等)
  if (options.onBeforeMount) {
    await options.onBeforeMount({
      router,
      cache: options.sharedData?.cache,
    })
  }

  // 挂载应用
  mount()

  // 🆕 wujie 环境:动态路由注册后自动恢复路由
  if (isWujie) {
    const currentPath = router.currentRoute.value.fullPath
    if (currentPath && currentPath !== '/') {
      await router.replace(currentPath)
    }
  }
}

function injectSharedData(config: SharedDataConfig) {
  const sharedAuth = (window as any).__WUJIE?.props?.$shared?.auth
  if (!sharedAuth) return

  const { cache, keys } = config
  if (sharedAuth.token) cache.set(keys.accessToken, sharedAuth.token)
  if (sharedAuth.refreshToken) cache.set(keys.refreshToken, sharedAuth.refreshToken)
  if (sharedAuth.userInfo) {
    const cachedUser = cache.get(keys.user) || {}
    cachedUser.user = sharedAuth.userInfo
    cachedUser.permissions = sharedAuth.permissions || []
    cachedUser.roles = sharedAuth.roles || []
    if (sharedAuth.menus) cachedUser.menus = sharedAuth.menus
    cache.set(keys.user, cachedUser)
  }
}

5.5 集成复杂度对比

维度旧 iframe 方案wujie 当前(v1)wujie 简化后(v2 提案)
main.ts~185 行(手动生命周期)~53 行(bootstrapMicroApp)~53 行(不变)
App.vue wujie 代码0(但有 ~150 行 postMessage 代码)~80 行~10 行(仅读缓存用于 UI)
router/index.ts~71 行(含旧通知)~52 行~52 行(不变)
子应用需要理解的概念postMessage 协议、origin 校验、序列化wujie props、bus、sync、prefix只需知道 bootstrapMicroApp 的配置项
新增子应用工时1-2 人天0.5-1.5 人天2-4 小时

5.6 对不同子应用类型的适配

子应用类型sharedDataonBeforeMount说明
新子应用(无动态路由)✅ 配置不需要最简单,2 小时搞定
存量子应用(有动态路由)✅ 配置✅ 提供回调需要在回调中注册动态路由
存量子应用(有旧 iframe 兼容)✅ 配置✅ 提供回调App.vue 保留 isInIframe 分支,渐进清理

六、推荐迁移策略

6.1 渐进式迁移路线

阶段一(已完成):基础设施 + ibs-manage 试点
    ↓
阶段二(当前):实现 v2 简化方案 + 迁移 doc 子应用验证
    ↓
阶段三:推广到 mkt、finance 等子应用(各部门自行迁移,提供文档 + 模板)
    ↓
阶段四:清理旧 iframe 兼容代码 + 统一 vue/vue-router 版本

6.2 并行兼容期

迁移期间,子应用 App.vue 通过 isWujie / isInIframe 分支同时支持两种模式:

const isWujie = !!(window as any).__WUJIE
const isInIframe = !isWujie && window.parent !== window

// wujie 环境:由 micro-bootstrap 自动处理
// 旧 iframe 环境:保留原有 postMessage 逻辑
// 独立运行:正常启动

旧 iframe 方案不需要立即下线,可以在所有子应用迁移完成后统一清理。

6.3 各部门迁移支持

支持项内容
迁移文档docs/migration/ibs-manage-wujie-集成迁移指南.md(已产出)
子应用模板apps/template/(可直接 copy 作为新子应用骨架)
排错指南迁移文档第 7 章(8 个常见问题 + 排查步骤)
培训文档docs/training/(5 章,覆盖 L1-L3 三个梯队)
Code Review首个迁移子应用由架构组 review,后续自行迁移

七、决策建议

迁移的核心论点

基础设施投入(8.5 人天)已完成且沉没。单个子应用迁移成本仅 0.5-1.5 人天(v2 简化后降至 2-4 小时),但能获得:

  1. 首屏性能提升 5-10 倍(预加载 + alive 保活)
  2. 消除 ~150 行/子应用的重复通信代码
  3. F5 刷新可靠恢复(wujie sync 机制,零代码)
  4. keep-alive 页面缓存(iframe 方案无法实现)
  5. 统一的通信协议(有类型约束,bug 减少)

不迁移的隐性成本(每次新功能都要在 postMessage 协议上打补丁、路由同步 bug 反复出现、无法实现缓存)远大于一次性迁移成本。

建议行动项

  1. 评审本方案,确认 v2 简化方案的 API 设计
  2. 实现 v2 简化,在 micro-bootstrap 中落地 sharedData + onBeforeMount
  3. 用 doc 子应用验证 v2 简化方案的实际效果
  4. 发布迁移通知,各部门按优先级排期迁移
  5. 设定清理时间线,在所有子应用迁移完成后统一清理旧 iframe 代码

附录:术语表

术语说明
wujie腾讯开源的微前端框架,基于 iframe 沙箱 + WebComponent 容器
alive 模式wujie 保活模式,切换子应用时不销毁 Vue 实例
sync 模式wujie 路由同步模式,子应用路由写入主应用 URL query
prefixwujie sync 短路径映射,压缩 URL query 长度
micro-bootstrapCMCLink 子应用统一启动器,封装 wujie 生命周期
micro-bridgeCMCLink 通信桥接层,封装 wujie bus + 子应用注册表
shared-provider主应用状态广播器,将 Pinia store 数据广播给子应用