iframe → wujie 迁移收益分析与子应用集成方案
一、背景
当前 CMCLink 平台存在两种微前端集成方案并行:
- 旧方案:原生 iframe 嵌入 + postMessage 通信(mkt、doc、ibs-manage 等子应用在用)
- 新方案:wujie iframe 沙箱 + bus 通信(template 子应用模板已验证,ibs-manage 已完成迁移试点)
本文档从决策层面和工程层面两个维度分析:
- 为什么要从 iframe 迁移到 wujie?投入产出比如何?
- 如何将子应用集成复杂度降到最低,让不同部门愿意迁移?
二、旧 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.ts | 15 分钟 | 替换为 createChildAppConfig
|
| 改 src/main.ts | 30 分钟 | 替换为 bootstrapMicroApp
|
| 改 src/App.vue | 2-4 小时 | wujie 适配(共享数据注入 + 路由恢复) |
| 改 src/router/index.ts | 15 分钟 | 删除旧通知逻辑 |
| 改 env + package.json | 15 分钟 | 端口 + 依赖 |
| 主应用配置 | 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 环境检测 — 3 行
document.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 对不同子应用类型的适配
| 子应用类型 | sharedData | onBeforeMount | 说明 |
|---|---|---|---|
| 新子应用(无动态路由) | ✅ 配置 | 不需要 | 最简单,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 小时),但能获得:
- 首屏性能提升 5-10 倍(预加载 + alive 保活)
- 消除 ~150 行/子应用的重复通信代码
- F5 刷新可靠恢复(wujie sync 机制,零代码)
- keep-alive 页面缓存(iframe 方案无法实现)
- 统一的通信协议(有类型约束,bug 减少)
不迁移的隐性成本(每次新功能都要在 postMessage 协议上打补丁、路由同步 bug 反复出现、无法实现缓存)远大于一次性迁移成本。
建议行动项
- 评审本方案,确认 v2 简化方案的 API 设计
-
实现 v2 简化,在 micro-bootstrap 中落地
sharedData+onBeforeMount - 用 doc 子应用验证 v2 简化方案的实际效果
- 发布迁移通知,各部门按优先级排期迁移
- 设定清理时间线,在所有子应用迁移完成后统一清理旧 iframe 代码
附录:术语表
| 术语 | 说明 |
|---|---|
| wujie | 腾讯开源的微前端框架,基于 iframe 沙箱 + WebComponent 容器 |
| alive 模式 | wujie 保活模式,切换子应用时不销毁 Vue 实例 |
| sync 模式 | wujie 路由同步模式,子应用路由写入主应用 URL query |
| prefix | wujie sync 短路径映射,压缩 URL query 长度 |
| micro-bootstrap | CMCLink 子应用统一启动器,封装 wujie 生命周期 |
| micro-bridge | CMCLink 通信桥接层,封装 wujie bus + 子应用注册表 |
| shared-provider | 主应用状态广播器,将 Pinia store 数据广播给子应用 |