主应用子应用globalData通信
This commit is contained in:
@@ -671,6 +671,591 @@ microApp.setData('app', {
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
### Q12-2:如何实现「主→子」「子→主」「子→子」完整的跨应用通信?给出实际代码。
|
||||||
|
|
||||||
|
**答:**
|
||||||
|
|
||||||
|
以下代码基于本项目真实实现,覆盖三种通信方向。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**一、架构总览**
|
||||||
|
|
||||||
|
```
|
||||||
|
┌────────────────────────────────────────┐
|
||||||
|
│ 主应用 (消息中枢) │
|
||||||
|
│ │
|
||||||
|
│ microApp.addDataListener('vue2', cb) │ ← 监听子→主
|
||||||
|
│ microApp.addDataListener('vue3', cb) │
|
||||||
|
│ │
|
||||||
|
│ 转发逻辑: │
|
||||||
|
│ 收到 vue2 → setData('vue3', ...) │ ← 子→子中转
|
||||||
|
│ 收到 vue3 → setData('vue2', ...) │
|
||||||
|
└────┬──────────────────────┬───────────┘
|
||||||
|
│ setData() │ setData()
|
||||||
|
↓ ↓
|
||||||
|
┌──────────────────┐ ┌──────────────────┐
|
||||||
|
│ vue2-app │ │ vue3-app │
|
||||||
|
│ │ │ │
|
||||||
|
│ addDataListener() │ │ addDataListener() │ ← 监听主→子
|
||||||
|
│ dispatch() │ │ dispatch() │ ← 子→主
|
||||||
|
└──────────────────┘ └──────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**二、主→子通信(主应用推送数据给子应用)**
|
||||||
|
|
||||||
|
主应用:
|
||||||
|
```ts
|
||||||
|
// src/main.ts — 在 lifeCycles.mounted 中自动推送
|
||||||
|
mounted(_e, appName) {
|
||||||
|
microApp.setData(appName, {
|
||||||
|
type: 'countUpdate',
|
||||||
|
count: sharedCount.value,
|
||||||
|
from: 'main',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// src/views/Home.vue — 手动推送
|
||||||
|
function pushToVue2() {
|
||||||
|
microApp.setData('vue2-app', { type: 'countUpdate', count: sharedCount.value, from: 'main' })
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
子应用接收:
|
||||||
|
```ts
|
||||||
|
// vue2-app / vue3-app — onMounted 中监听
|
||||||
|
onMounted(() => {
|
||||||
|
window.microApp?.addDataListener((data: any) => {
|
||||||
|
if (data.type === 'countUpdate') {
|
||||||
|
count.value = data.count // 更新本地计数器
|
||||||
|
addLog(`收到来自 ${data.from} 的计数: ${data.count}`)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**三、子→主通信(子应用通知主应用)**
|
||||||
|
|
||||||
|
子应用:
|
||||||
|
```ts
|
||||||
|
function sendToMain() {
|
||||||
|
count.value++
|
||||||
|
window.microApp?.dispatch({
|
||||||
|
type: 'countUpdate',
|
||||||
|
count: count.value,
|
||||||
|
from: 'vue2-app', // 标识来源
|
||||||
|
})
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
主应用(在 `main.ts` 中全局注册,避免组件重复挂载时重复监听):
|
||||||
|
```ts
|
||||||
|
// src/main.ts — 全局一次性注册
|
||||||
|
microApp.addDataListener('vue2-app', (data: any) => {
|
||||||
|
if (data.type === 'countUpdate') {
|
||||||
|
sharedCount.value = data.count // 更新全局计数
|
||||||
|
addLog(`vue2-app → 主: 收到计数 ${data.count}`, 'child')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**四、子→子通信(子应用 A 通知子应用 B,主应用中转)**
|
||||||
|
|
||||||
|
子应用 A:
|
||||||
|
```ts
|
||||||
|
function sendToVue3() {
|
||||||
|
count.value++
|
||||||
|
// 仍然是 dispatch 给主应用,由主应用中转
|
||||||
|
window.microApp?.dispatch({
|
||||||
|
type: 'countUpdate',
|
||||||
|
count: count.value,
|
||||||
|
from: 'vue2-app',
|
||||||
|
target: 'vue3-app', // 标注目标
|
||||||
|
})
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
主应用中转:
|
||||||
|
```ts
|
||||||
|
// src/main.ts — 在 addDataListener 回调中转发
|
||||||
|
microApp.addDataListener('vue2-app', (data: any) => {
|
||||||
|
if (data.type === 'countUpdate') {
|
||||||
|
sharedCount.value = data.count
|
||||||
|
|
||||||
|
// 转发给另一个子应用
|
||||||
|
microApp.setData('vue3-app', {
|
||||||
|
type: 'countUpdate',
|
||||||
|
count: data.count,
|
||||||
|
from: 'vue2-app', // 保留原始来源
|
||||||
|
})
|
||||||
|
addLog(`主 → vue3-app: 转发计数 ${data.count}`, 'relay')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
**关键:micro-app 不提供子→子直接通信 API,必须通过主应用中转。**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**五、状态持久化(解决路由切换后计数器归零)**
|
||||||
|
|
||||||
|
**问题**:主应用 `Home.vue` 中的 `ref(0)` 是组件局部状态,路由切走(组件卸载)后销毁,切回来时重新初始化为 0。
|
||||||
|
|
||||||
|
**解决方案:模块级单例 Store**
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// src/stores/counterStore.ts
|
||||||
|
import { ref } from 'vue'
|
||||||
|
|
||||||
|
// ✅ 模块级 ref — 绑定到模块单例,不随组件卸载而销毁
|
||||||
|
export const sharedCount = ref(0)
|
||||||
|
export const counterLogs = ref<LogEntry[]>([])
|
||||||
|
|
||||||
|
export function addLog(msg: string, type: 'main' | 'child' | 'relay' = 'main') {
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
主应用组件使用:
|
||||||
|
```ts
|
||||||
|
// src/views/Home.vue
|
||||||
|
import { sharedCount, counterLogs, addLog } from '@/stores/counterStore'
|
||||||
|
// sharedCount 现在不会因路由切换而丢失
|
||||||
|
```
|
||||||
|
|
||||||
|
**同时处理其他生命周期同步问题:**
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// src/main.ts — microApp.start() 的 lifeCycles
|
||||||
|
lifeCycles: {
|
||||||
|
mounted(_e, appName) {
|
||||||
|
// ✅ 子应用首次加载时,自动推送当前计数
|
||||||
|
microApp.setData(appName, { type: 'countUpdate', count: sharedCount.value, from: 'main' })
|
||||||
|
},
|
||||||
|
beforeshow(_e, appName) {
|
||||||
|
// ✅ keepAlive 恢复时,子应用可能状态过期,重新推送
|
||||||
|
microApp.setData(appName, { type: 'countUpdate', count: sharedCount.value, from: 'main' })
|
||||||
|
},
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**六、完整通信时序(子→子 为例)**
|
||||||
|
|
||||||
|
```
|
||||||
|
vue2-app 主应用(main.ts) vue3-app
|
||||||
|
│ │ │
|
||||||
|
│ dispatch({ │ │
|
||||||
|
│ count: 5, │ │
|
||||||
|
│ from: 'vue2-app' │ │
|
||||||
|
│ }) │ │
|
||||||
|
│ ─────────────────────→ │ │
|
||||||
|
│ │ addDataListener 触发 │
|
||||||
|
│ │ sharedCount.value = 5 │
|
||||||
|
│ │ │
|
||||||
|
│ │ setData('vue3-app', { │
|
||||||
|
│ │ count: 5, │
|
||||||
|
│ │ from: 'vue2-app' │
|
||||||
|
│ │ }) │
|
||||||
|
│ │ ───────────────────────→ │
|
||||||
|
│ │ │ addDataListener 触发
|
||||||
|
│ │ │ count.value = 5
|
||||||
|
│ │ │ 日志: "收到来自 vue2-app"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Q12-3:有没有比 `setData`/`dispatch` 更优雅的微前端通信方案?对比各方案的优劣。
|
||||||
|
|
||||||
|
**答:**
|
||||||
|
|
||||||
|
有。`setData`/`dispatch` 是最基础的"点对点消息"模式,适合简单场景。对于复杂状态管理,有以下更优雅的方案。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**方案一:micro-app `globalData`(官方全局状态)**
|
||||||
|
|
||||||
|
micro-app 内置了全局状态管理,主应用设置,所有子应用都能监听。
|
||||||
|
|
||||||
|
主应用:
|
||||||
|
```ts
|
||||||
|
import microApp from '@micro-zoe/micro-app'
|
||||||
|
|
||||||
|
// 设置全局数据(所有子应用都能收到)
|
||||||
|
microApp.setGlobalData({ count: 5, theme: 'dark' })
|
||||||
|
|
||||||
|
// 强制覆盖(即使值相同也触发通知)
|
||||||
|
microApp.forceSetGlobalData({ count: 6 })
|
||||||
|
|
||||||
|
// 主应用自己也可以监听
|
||||||
|
microApp.addGlobalDataListener((data) => {
|
||||||
|
console.log('全局状态变化:', data)
|
||||||
|
}, true) // autoTrigger: 立即用当前值触发一次
|
||||||
|
```
|
||||||
|
|
||||||
|
子应用(任何子应用):
|
||||||
|
```ts
|
||||||
|
// 获取当前值
|
||||||
|
const data = window.microApp?.getGlobalData() // { count: 5, theme: 'dark' }
|
||||||
|
|
||||||
|
// 监听变化
|
||||||
|
window.microApp?.addGlobalDataListener((data) => {
|
||||||
|
console.log('全局状态变化:', data)
|
||||||
|
}, true)
|
||||||
|
```
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// 子应用也可以设置全局数据
|
||||||
|
window.microApp?.setGlobalData({ count: 10 })
|
||||||
|
```
|
||||||
|
|
||||||
|
**优点**:任何应用(主/子)都可以读/写/监听,天然的全局状态树
|
||||||
|
**缺点**:全局命名空间,多子应用时注意 key 冲突;不利于精细的"只发给某个子应用"
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**方案二:BroadcastChannel API(浏览器原生)**
|
||||||
|
|
||||||
|
如果所有子应用都和主应用**同源**部署,可以用 BroadcastChannel 实现真正的去中心化通信。
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// 任何应用(主或子)创建同名 channel
|
||||||
|
const channel = new BroadcastChannel('micro-app-bus')
|
||||||
|
|
||||||
|
// 发送消息(广播给所有同源窗口/iframe)
|
||||||
|
channel.postMessage({ type: 'countUpdate', count: 5, from: 'vue2-app' })
|
||||||
|
|
||||||
|
// 接收消息
|
||||||
|
channel.onmessage = (event) => {
|
||||||
|
const { type, count, from } = event.data
|
||||||
|
if (type === 'countUpdate') {
|
||||||
|
console.log(`收到 ${from} 的计数: ${count}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清理
|
||||||
|
channel.close()
|
||||||
|
```
|
||||||
|
|
||||||
|
**优点**:
|
||||||
|
- 真正的去中心化 — 不需要主应用中转
|
||||||
|
- 浏览器原生 API,零依赖
|
||||||
|
- 同源下 window / iframe / web worker 都能通
|
||||||
|
- 比 postMessage 语义更清晰(专门为消息设计)
|
||||||
|
|
||||||
|
**缺点**:
|
||||||
|
- 仅限同源部署
|
||||||
|
- 不兼容 IE
|
||||||
|
- 消息全广播,需自行过滤
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**方案三:CustomEvent + DOM 事件总线(同 document 场景)**
|
||||||
|
|
||||||
|
仅适用于 `iframe: false` 模式(with 沙箱),因为共享 document。
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// 主应用 — 创建事件总线
|
||||||
|
const bus = new EventTarget()
|
||||||
|
|
||||||
|
// 子应用 — 通过 window 访问
|
||||||
|
// 主应用中挂载
|
||||||
|
;(window as any).__MICRO_BUS__ = bus
|
||||||
|
|
||||||
|
// 任何应用发送
|
||||||
|
window.__MICRO_BUS__?.dispatchEvent(
|
||||||
|
new CustomEvent('count-update', { detail: { count: 5, from: 'vue2' } })
|
||||||
|
)
|
||||||
|
|
||||||
|
// 任何应用接收
|
||||||
|
window.__MICRO_BUS__?.addEventListener('count-update', (e: CustomEvent) => {
|
||||||
|
console.log(e.detail)
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
**缺点**:`iframe: true` 模式不可用(不同 document),仅限 with 沙箱。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**方案四:模块级单例 Store + reactive Watch(本项目采用的改进版)**
|
||||||
|
|
||||||
|
将状态抽到模块级单例,用 Vue `watch` 驱动行为。
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// src/stores/counterStore.ts — 模块级单例
|
||||||
|
import { ref, watch } from 'vue'
|
||||||
|
import microApp from '@micro-zoe/micro-app'
|
||||||
|
|
||||||
|
export const sharedCount = ref(0)
|
||||||
|
|
||||||
|
// ✅ 状态变化自动推送到所有子应用
|
||||||
|
watch(sharedCount, (val) => {
|
||||||
|
const activeApps = microApp.getActiveApps({ excludeHiddenApp: false })
|
||||||
|
activeApps.forEach(name => {
|
||||||
|
microApp.setData(name, { type: 'countUpdate', count: val, from: 'main' })
|
||||||
|
})
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
这样主应用组件里只要操作 `sharedCount.value++`,无需手动调用 `setData`。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**五、各方案对比**
|
||||||
|
|
||||||
|
| 方案 | 通信方向 | 去中心化 | 子→子 | iframe | 复杂度 | 适用场景 |
|
||||||
|
|------|----------|----------|-------|--------|--------|----------|
|
||||||
|
| `setData`/`dispatch` | 主↔子点对点 | ❌ 主中转 | 需转发 | ✅ | 低 | 简单数据传递 |
|
||||||
|
| `globalData` | 全局广播 | ✅ 任何应用可写 | ✅ | ✅ | 低 | 全局配置、主题、用户信息 |
|
||||||
|
| `BroadcastChannel` | 全局广播 | ✅ 完全去中心 | ✅ | ✅ (同源) | 低 | 同源部署的复杂协作 |
|
||||||
|
| `CustomEvent` | DOM 事件 | ✅ | ✅ | ❌ (with沙箱) | 低 | iframe:false 的轻量场景 |
|
||||||
|
| 模块 Store + watch | 主→子自动 | ❌ 主驱动 | 需转发 | ✅ | 中 | 复杂状态、自动同步 |
|
||||||
|
| `EventCenterForMicroApp` | 主↔子点对点 | ❌ | ❌ | ✅ | 低 | 按需精确通信 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**六、推荐选型**
|
||||||
|
|
||||||
|
```
|
||||||
|
简单传参(初始化配置) → setData / getData
|
||||||
|
全局共享(主题、用户、语言) → globalData ✅
|
||||||
|
同源复杂协作 → BroadcastChannel ✅
|
||||||
|
复杂跨应用状态管理 → 模块 Store + watch + setData 自动推送 ✅
|
||||||
|
子应用间点对点通信 → dispatch + 主应用中转(目前唯一方式)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Q12-4:用 `globalData` 改造项目的完整过程是怎样的?前后架构对比?
|
||||||
|
|
||||||
|
**答:**
|
||||||
|
|
||||||
|
以下记录了本项目从 `setData`/`dispatch` 点对点模式迁移到 `globalData` 全局广播模式的完整过程。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**一、改造前的架构(setData + dispatch + 主应用中转)**
|
||||||
|
|
||||||
|
```
|
||||||
|
vue2-app 主应用 vue3-app
|
||||||
|
│ │ │
|
||||||
|
│ dispatch({count:5}) │ │
|
||||||
|
│ ───────────────────────→ │ │
|
||||||
|
│ │ addDataListener('vue2') │
|
||||||
|
│ │ sharedCount = 5 │
|
||||||
|
│ │ │
|
||||||
|
│ │ setData('vue3', {count:5})│
|
||||||
|
│ │ ───────────────────────→ │
|
||||||
|
│ │ │ addDataListener()
|
||||||
|
│ │ │ count = 5
|
||||||
|
```
|
||||||
|
|
||||||
|
**痛点:**
|
||||||
|
- 主应用需要为每个子应用单独注册 `addDataListener`
|
||||||
|
- 子→子通信必须经过主应用中转
|
||||||
|
- 代码量大,新增子应用时需要在主应用中添加新的监听和中转逻辑
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**二、改造后的架构(globalData 全局广播)**
|
||||||
|
|
||||||
|
```
|
||||||
|
┌──────────────────────────┐
|
||||||
|
│ globalData 池 │
|
||||||
|
│ { count: 5, from: ... } │
|
||||||
|
└─────┬──────────┬─────────┘
|
||||||
|
│ │
|
||||||
|
setGlobalData() addGlobalDataListener()
|
||||||
|
getGlobalData()
|
||||||
|
│ │
|
||||||
|
┌──────────┼──────────┼──────────┐
|
||||||
|
│ │ │ │
|
||||||
|
主应用 vue2-app vue3-app 任何一方
|
||||||
|
```
|
||||||
|
|
||||||
|
**任何一方 `setGlobalData` → 其他所有方 `addGlobalDataListener` 都触发。**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**三、主应用改造(main.ts)**
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// ❌ 改造前:per-app 监听 + 中继
|
||||||
|
microApp.start({
|
||||||
|
lifeCycles: {
|
||||||
|
mounted(_e, appName) {
|
||||||
|
microApp.setData(appName, { type: 'countUpdate', count: sharedCount.value, from: 'main' })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
microApp.addDataListener('vue2-app', (data: any) => {
|
||||||
|
if (data.type === 'countUpdate') {
|
||||||
|
sharedCount.value = data.count
|
||||||
|
microApp.setData('vue3-app', { type: 'countUpdate', count: data.count, from: 'vue2-app' }) // 中继
|
||||||
|
}
|
||||||
|
})
|
||||||
|
microApp.addDataListener('vue3-app', (data: any) => {
|
||||||
|
// ... 同样的中继逻辑
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// ✅ 改造后:单一 globalData 监听,无需中继
|
||||||
|
microApp.setGlobalData({ count: 0, from: 'main' })
|
||||||
|
|
||||||
|
microApp.addGlobalDataListener((data: Record<string, any>) => {
|
||||||
|
if (data.count !== undefined) {
|
||||||
|
sharedCount.value = data.count
|
||||||
|
const from = data.from || 'unknown'
|
||||||
|
if (from !== 'main') {
|
||||||
|
addLog(`${from} → globalData: 计数 ${sharedCount.value}`, 'child')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
// 无需 mounted/beforeshow 手动推送 — 子应用通过 autoTrigger 自动拿值
|
||||||
|
```
|
||||||
|
|
||||||
|
**关键变化:**
|
||||||
|
- `setData(appName, ...)` → `setGlobalData(...)`(不再需要指定 appName)
|
||||||
|
- `addDataListener(appName, cb)` × N → `addGlobalDataListener(cb)` × 1
|
||||||
|
- 删除所有中继逻辑
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**四、主应用 Home.vue 改造**
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// ❌ 改造前:分别推送到不同子应用
|
||||||
|
function pushToVue2() {
|
||||||
|
microApp.setData('vue2-app', { type: 'countUpdate', count: sharedCount.value, from: 'main' })
|
||||||
|
}
|
||||||
|
function pushToVue3() {
|
||||||
|
microApp.setData('vue3-app', { type: 'countUpdate', count: sharedCount.value, from: 'main' })
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// ✅ 改造后:一键广播,所有子应用同时收到
|
||||||
|
function doBroadcast(action: string) {
|
||||||
|
microApp.setGlobalData({ count: sharedCount.value, from: 'main' })
|
||||||
|
// vue2-app 和 vue3-app 同时收到,无需区分目标
|
||||||
|
}
|
||||||
|
|
||||||
|
function increment() {
|
||||||
|
sharedCount.value++
|
||||||
|
doBroadcast('+1') // +1 后自动广播
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**UI 变化:**
|
||||||
|
```
|
||||||
|
改造前: 改造后:
|
||||||
|
[− 1] [+ 1] [− 1] [+ 1] [📡 广播到所有子应用]
|
||||||
|
[📤 推送到 Vue2] [📤 推送到 Vue3] ↑ +1/−1 自动广播,按钮为手动同步
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**五、子应用改造(vue2-app / vue3-app)**
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// ❌ 改造前:dispatch + addDataListener
|
||||||
|
created() {
|
||||||
|
const data = window.microApp?.getData()
|
||||||
|
if (data && data.count !== undefined) this.count = data.count
|
||||||
|
|
||||||
|
window.microApp?.addDataListener((data) => {
|
||||||
|
if (data.type === 'countUpdate') {
|
||||||
|
this.count = data.count
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
sendToMain() {
|
||||||
|
this.count++
|
||||||
|
window.microApp.dispatch({ type: 'countUpdate', count: this.count, from: 'vue2-app' })
|
||||||
|
},
|
||||||
|
sendToVue3() {
|
||||||
|
this.count++
|
||||||
|
// 需要主应用中转
|
||||||
|
window.microApp.dispatch({ type: 'countUpdate', count: this.count, from: 'vue2-app', target: 'vue3-app' })
|
||||||
|
},
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// ✅ 改造后:setGlobalData + addGlobalDataListener(autoTrigger)
|
||||||
|
created() {
|
||||||
|
// autoTrigger: true → 立即用当前 globalData 触发一次,无需手动 getData
|
||||||
|
window.microApp?.addGlobalDataListener((data) => {
|
||||||
|
if (data.count !== undefined) {
|
||||||
|
this.count = data.count
|
||||||
|
}
|
||||||
|
}, true) // ← 这个 true 替代了 getData() + addDataListener 两件事
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
broadcastIncrement() {
|
||||||
|
this.count++
|
||||||
|
// globalData 自动广播给主应用 + 所有其他子应用!
|
||||||
|
window.microApp.setGlobalData({ count: this.count, from: 'vue2-app' })
|
||||||
|
// vue3-app 直接收到,无需主应用中转 ✅
|
||||||
|
},
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**关键变化:**
|
||||||
|
- `dispatch({ type: 'countUpdate', ... })` → `setGlobalData({ count: N, from: 'xxx' })`
|
||||||
|
- `getData()` + `addDataListener(cb)` → `addGlobalDataListener(cb, true)`
|
||||||
|
- `sendToMain()` 和 `sendToVue3()` 合并为 `broadcastIncrement()` — 都是广播
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**六、架构对比总结**
|
||||||
|
|
||||||
|
| 维度 | 改造前 (setData/dispatch) | 改造后 (globalData) |
|
||||||
|
|------|--------------------------|---------------------|
|
||||||
|
| 主→子 | `setData(appName, data)` 逐个推送 | `setGlobalData(data)` 全部广播 |
|
||||||
|
| 子→主 | `dispatch(data)` → `addDataListener(appName, cb)` | `setGlobalData(data)` → `addGlobalDataListener(cb)` |
|
||||||
|
| 子→子 | dispatch → 主应用中转 → setData | setGlobalData → 其他子应用直接收到 ✅ |
|
||||||
|
| 生命周期同步 | `mounted`/`beforeshow` 中手动 `setData` | `autoTrigger: true` 自动同步 |
|
||||||
|
| 主应用监听器 | N 个(每个子应用一个) | 1 个 |
|
||||||
|
| 新增子应用 | 需加新的 addDataListener + 转发逻辑 | 不需要改任何代码 |
|
||||||
|
| 数据格式 | `{ type: 'countUpdate', count, from }` | `{ count, from }` 更简洁 |
|
||||||
|
| 类型声明(子应用) | `addDataListener / dispatch` | 加 `addGlobalDataListener / setGlobalData / getGlobalData` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**七、globalData 的注意事项**
|
||||||
|
|
||||||
|
1. **广播语义**:`setGlobalData` 会通知**所有**应用,无法定向推送给特定子应用。如需定向,在 data 中带 `target` 字段并在子应用端过滤。
|
||||||
|
|
||||||
|
2. **合并策略**:`setGlobalData` 会**合并**(不是替换)到现有 globalData,所以多次调用会累积 key。
|
||||||
|
|
||||||
|
3. **不要高频调用**:每次 `setGlobalData` 都会触发所有子应用的回调。计数器这种低频场景没问题,但高频场景(如鼠标移动、实时输入)需要用节流或改用点对点 `setData`。
|
||||||
|
|
||||||
|
4. **autoTrigger 陷阱**:`addGlobalDataListener(cb, true)` 中的 `autoTrigger` 会传递**整个 globalData 对象**,不是单次变更的 delta。回调逻辑要做兼容:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// ✅ 正确:通过 count 字段判断
|
||||||
|
addGlobalDataListener((data) => {
|
||||||
|
if (data.count !== undefined) { // 不是 if (data.type === 'countUpdate')
|
||||||
|
this.count = data.count
|
||||||
|
}
|
||||||
|
}, true)
|
||||||
|
```
|
||||||
|
|
||||||
|
5. **与 keepAlive 的配合**:`autoTrigger: true` 天然解决了 keepAlive 恢复时的状态同步 — 子应用恢复后重新挂载时,`addGlobalDataListener(cb, true)` 会立即用最新 globalData 触发。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
### Q13:`keep-alive` 保活机制的原理是什么?有什么注意事项?
|
### Q13:`keep-alive` 保活机制的原理是什么?有什么注意事项?
|
||||||
|
|
||||||
**答:**
|
**答:**
|
||||||
|
|||||||
37
src/main.ts
37
src/main.ts
@@ -2,6 +2,7 @@ import { createApp } from 'vue'
|
|||||||
import microApp from '@micro-zoe/micro-app'
|
import microApp from '@micro-zoe/micro-app'
|
||||||
import App from './App.vue'
|
import App from './App.vue'
|
||||||
import router from './router'
|
import router from './router'
|
||||||
|
import { sharedCount, addLog } from './stores/counterStore'
|
||||||
|
|
||||||
// 启动 micro-app 微前端框架
|
// 启动 micro-app 微前端框架
|
||||||
// https://jd-opensource.github.io/micro-app/docs.html#/start
|
// https://jd-opensource.github.io/micro-app/docs.html#/start
|
||||||
@@ -21,15 +22,51 @@ microApp.start({
|
|||||||
},
|
},
|
||||||
mounted(_e, appName) {
|
mounted(_e, appName) {
|
||||||
console.log(`[micro-app] 子应用 ${appName} 挂载完成`)
|
console.log(`[micro-app] 子应用 ${appName} 挂载完成`)
|
||||||
|
// 子应用挂载后会通过 addGlobalDataListener(autoTrigger=true) 自动拿到当前值
|
||||||
|
addLog(`子应用 ${appName} 挂载完成(将通过 globalData 自动同步)`, 'main')
|
||||||
|
},
|
||||||
|
beforeshow(_e, appName) {
|
||||||
|
console.log(`[micro-app] 子应用 ${appName} 即将显示(keepAlive 恢复)`)
|
||||||
|
addLog(`子应用 ${appName} 恢复显示(globalData 自动同步)`, 'main')
|
||||||
},
|
},
|
||||||
unmount(_e, appName) {
|
unmount(_e, appName) {
|
||||||
console.log(`[micro-app] 子应用 ${appName} 已卸载`)
|
console.log(`[micro-app] 子应用 ${appName} 已卸载`)
|
||||||
},
|
},
|
||||||
error(_e, appName) {
|
error(_e, appName) {
|
||||||
console.error(`[micro-app] 子应用 ${appName} 加载错误:`, _e)
|
console.error(`[micro-app] 子应用 ${appName} 加载错误:`, _e)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// 全局通信(globalData 方案)
|
||||||
|
//
|
||||||
|
// globalData 是 micro-app 内置的全局状态池:
|
||||||
|
// - 主应用 / 任何子应用 都可以 setGlobalData / getGlobalData
|
||||||
|
// - 任何一方调用 setGlobalData → 其他所有方的 addGlobalDataListener 都触发
|
||||||
|
// - 子→子通信不再需要主应用中转!
|
||||||
|
//
|
||||||
|
// 架构变化:
|
||||||
|
// 之前:setData(per-app) + addDataListener(per-app) + 主应用中转
|
||||||
|
// 现在:setGlobalData(broadcast) + addGlobalDataListener(global)
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
// 初始化全局数据
|
||||||
|
microApp.setGlobalData({ count: 0, from: 'main' })
|
||||||
|
|
||||||
|
// 单一监听器 — 接收任何子应用发来的 globalData 更新
|
||||||
|
microApp.addGlobalDataListener((data: Record<string, any>) => {
|
||||||
|
if (data.count !== undefined) {
|
||||||
|
const prev = sharedCount.value
|
||||||
|
sharedCount.value = data.count
|
||||||
|
const from = data.from || 'unknown'
|
||||||
|
if (from !== 'main') {
|
||||||
|
// 子应用发起的更新
|
||||||
|
addLog(`${from} → globalData: 计数 ${prev} → ${data.count}`, 'child')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
// 注意:不需要 autoTrigger,main.ts 是入口文件,sharedCount 默认就是 0
|
||||||
|
|
||||||
const app = createApp(App)
|
const app = createApp(App)
|
||||||
app.use(router)
|
app.use(router)
|
||||||
|
|||||||
50
src/stores/counterStore.ts
Normal file
50
src/stores/counterStore.ts
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
/**
|
||||||
|
* 共享计数器 Store
|
||||||
|
*
|
||||||
|
* 使用模块级 ref,状态绑定到模块单例而不是 Vue 组件实例。
|
||||||
|
* 即使 Home.vue 被卸载(路由切走),计数器也不会重置。
|
||||||
|
*
|
||||||
|
* 通信流程:
|
||||||
|
* 主应用 ←→ 子应用(双向,主应用为消息中转站)
|
||||||
|
* 子←→子:子A → dispatch → 主应用 → relay setData → 子B
|
||||||
|
*/
|
||||||
|
import { ref } from 'vue'
|
||||||
|
|
||||||
|
export interface LogEntry {
|
||||||
|
time: string
|
||||||
|
msg: string
|
||||||
|
type: 'main' | 'child' | 'relay'
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// 模块级状态 — 不随组件卸载而销毁
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
/** 共享计数器 */
|
||||||
|
export const sharedCount = ref(0)
|
||||||
|
|
||||||
|
/** 通信日志 */
|
||||||
|
export const counterLogs = ref<LogEntry[]>([])
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// 工具函数
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
/** 添加一条通信日志 */
|
||||||
|
export function addLog(msg: string, type: 'main' | 'child' | 'relay' = 'main') {
|
||||||
|
const now = new Date()
|
||||||
|
const time = [
|
||||||
|
String(now.getHours()).padStart(2, '0'),
|
||||||
|
String(now.getMinutes()).padStart(2, '0'),
|
||||||
|
String(now.getSeconds()).padStart(2, '0'),
|
||||||
|
].join(':')
|
||||||
|
counterLogs.value.push({ time, msg, type })
|
||||||
|
if (counterLogs.value.length > 30) {
|
||||||
|
counterLogs.value.shift()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddLog function but without reactivity side effects - for use in main.ts
|
||||||
|
export function addSystemLog(msg: string, type: 'main' | 'child' | 'relay' = 'main') {
|
||||||
|
addLog(msg, type)
|
||||||
|
}
|
||||||
@@ -74,11 +74,110 @@
|
|||||||
<p class="hint">请在 <code>src/config/subApps.ts</code> 中配置子应用信息</p>
|
<p class="hint">请在 <code>src/config/subApps.ts</code> 中配置子应用信息</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- ============================================
|
||||||
|
跨应用通信演示
|
||||||
|
============================================ -->
|
||||||
|
<div class="communication-demo">
|
||||||
|
<div class="comm-header">
|
||||||
|
📡 跨应用通信演示
|
||||||
|
</div>
|
||||||
|
<div class="comm-body">
|
||||||
|
<div class="comm-counter">
|
||||||
|
<span class="counter-label">共享计数器</span>
|
||||||
|
<span class="counter-value" :class="{ changed: counterFlash }">{{ sharedCount }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="comm-controls">
|
||||||
|
<button class="btn-dec" @click="decrement">− 1</button>
|
||||||
|
<button class="btn-inc" @click="increment">+ 1</button>
|
||||||
|
<button class="btn-broadcast" @click="broadcast">
|
||||||
|
📡 广播到所有子应用
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="comm-log">
|
||||||
|
<div class="log-title">📋 通信日志(主应用视角)</div>
|
||||||
|
<div class="log-list" ref="logListRef">
|
||||||
|
<p v-for="(log, i) in counterLogs" :key="i" class="log-item" :class="log.type">
|
||||||
|
<span class="log-time">{{ log.time }}</span>
|
||||||
|
<span class="log-msg">{{ log.msg }}</span>
|
||||||
|
</p>
|
||||||
|
<p v-if="counterLogs.length === 0" class="log-empty">暂无通信记录,点击按钮开始测试 👆</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { ref, nextTick, watch } from 'vue'
|
||||||
|
import microApp from '@micro-zoe/micro-app'
|
||||||
import { subApps } from '@/config/subApps'
|
import { subApps } from '@/config/subApps'
|
||||||
|
import { sharedCount, counterLogs, addLog } from '@/stores/counterStore'
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// 计数器状态来自 counterStore(模块级单例,切换路由不丢失)
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
const counterFlash = ref(false)
|
||||||
|
const logListRef = ref<HTMLElement | null>(null)
|
||||||
|
|
||||||
|
// 本地管理的滚动同步函数,包装 store 的 addLog
|
||||||
|
function localAddLog(msg: string, type: 'main' | 'child' | 'relay' = 'main') {
|
||||||
|
addLog(msg, type)
|
||||||
|
nextTick(() => {
|
||||||
|
if (logListRef.value) {
|
||||||
|
logListRef.value.scrollTop = logListRef.value.scrollHeight
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function flashCounter() {
|
||||||
|
counterFlash.value = true
|
||||||
|
setTimeout(() => (counterFlash.value = false), 300)
|
||||||
|
}
|
||||||
|
|
||||||
|
function doBroadcast(action: string) {
|
||||||
|
// globalData 自动广播给所有子应用,不再需要指定 appName
|
||||||
|
microApp.setGlobalData({ count: sharedCount.value, from: 'main' })
|
||||||
|
localAddLog(`主 → globalData: ${action} → ${sharedCount.value}(广播到全部子应用)`, 'main')
|
||||||
|
}
|
||||||
|
|
||||||
|
function increment() {
|
||||||
|
sharedCount.value++
|
||||||
|
flashCounter()
|
||||||
|
doBroadcast(`+1`)
|
||||||
|
}
|
||||||
|
|
||||||
|
function decrement() {
|
||||||
|
sharedCount.value--
|
||||||
|
flashCounter()
|
||||||
|
doBroadcast(`−1`)
|
||||||
|
}
|
||||||
|
|
||||||
|
function broadcast() {
|
||||||
|
flashCounter()
|
||||||
|
doBroadcast(`手动同步`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// addDataListener 已移至 main.ts 全局注册,避免组件重复挂载时重复监听
|
||||||
|
|
||||||
|
// 监听外部(main.ts / 子应用)对 sharedCount 的修改,触发 UI 动画
|
||||||
|
watch(sharedCount, () => {
|
||||||
|
flashCounter()
|
||||||
|
})
|
||||||
|
|
||||||
|
// 监听外部(main.ts)新增的日志,自动滚动到底部
|
||||||
|
watch(
|
||||||
|
() => counterLogs.value.length,
|
||||||
|
() => {
|
||||||
|
nextTick(() => {
|
||||||
|
if (logListRef.value) {
|
||||||
|
logListRef.value.scrollTop = logListRef.value.scrollHeight
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
@@ -363,4 +462,179 @@ import { subApps } from '@/config/subApps'
|
|||||||
line-height: 1.8 !important;
|
line-height: 1.8 !important;
|
||||||
border: 1px dashed #ffcc80;
|
border: 1px dashed #ffcc80;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ============================================
|
||||||
|
跨应用通信演示区块样式
|
||||||
|
============================================ */
|
||||||
|
.communication-demo {
|
||||||
|
margin-top: 32px;
|
||||||
|
border-radius: 12px;
|
||||||
|
overflow: hidden;
|
||||||
|
border: 2px solid #667eea;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.comm-header {
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
color: #fff;
|
||||||
|
padding: 12px 20px;
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 15px;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.comm-body {
|
||||||
|
padding: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 计数器区域 */
|
||||||
|
.comm-counter {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 16px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
padding: 20px;
|
||||||
|
background: #f8f9ff;
|
||||||
|
border-radius: 10px;
|
||||||
|
border: 1px solid #e0e0f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.counter-label {
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #555;
|
||||||
|
}
|
||||||
|
|
||||||
|
.counter-value {
|
||||||
|
font-size: 48px;
|
||||||
|
font-weight: 800;
|
||||||
|
color: #667eea;
|
||||||
|
transition: transform 0.3s, color 0.3s;
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
}
|
||||||
|
|
||||||
|
.counter-value.changed {
|
||||||
|
transform: scale(1.2);
|
||||||
|
color: #ff6b35;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 增减按钮 */
|
||||||
|
.comm-controls {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-dec,
|
||||||
|
.btn-inc,
|
||||||
|
.btn-broadcast {
|
||||||
|
flex: 1;
|
||||||
|
padding: 12px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 700;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-dec {
|
||||||
|
background: #fee2e2;
|
||||||
|
color: #dc2626;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-dec:hover {
|
||||||
|
background: #fca5a5;
|
||||||
|
transform: scale(1.02);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-inc {
|
||||||
|
background: #dcfce7;
|
||||||
|
color: #16a34a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-inc:hover {
|
||||||
|
background: #86efac;
|
||||||
|
transform: scale(1.02);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 广播按钮 */
|
||||||
|
.btn-broadcast {
|
||||||
|
padding: 12px;
|
||||||
|
background: linear-gradient(135deg, #f59e0b, #d97706);
|
||||||
|
color: #fff;
|
||||||
|
border: none;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-broadcast:hover {
|
||||||
|
transform: translateY(-1px);
|
||||||
|
box-shadow: 0 3px 12px rgba(245, 158, 11, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 通信日志 */
|
||||||
|
.comm-log {
|
||||||
|
border-top: 1px solid #f0f0f0;
|
||||||
|
padding-top: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-title {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #666;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-list {
|
||||||
|
max-height: 200px;
|
||||||
|
overflow-y: auto;
|
||||||
|
background: #fafafa;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 8px 12px;
|
||||||
|
border: 1px solid #f0f0f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-item {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
font-size: 13px;
|
||||||
|
padding: 4px 0;
|
||||||
|
border-bottom: 1px solid #f5f5f5;
|
||||||
|
font-family: 'Consolas', 'Courier New', monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-item:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-time {
|
||||||
|
color: #999;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-item.main .log-msg {
|
||||||
|
color: #667eea;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-item.child .log-msg {
|
||||||
|
color: #16a34a;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-item.relay .log-msg {
|
||||||
|
color: #f59e0b;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-empty {
|
||||||
|
font-size: 13px;
|
||||||
|
color: #ccc;
|
||||||
|
text-align: center;
|
||||||
|
padding: 16px 0;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
Reference in New Issue
Block a user