主应用子应用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` 保活机制的原理是什么?有什么注意事项?
|
||||
|
||||
**答:**
|
||||
|
||||
37
src/main.ts
37
src/main.ts
@@ -2,6 +2,7 @@ import { createApp } from 'vue'
|
||||
import microApp from '@micro-zoe/micro-app'
|
||||
import App from './App.vue'
|
||||
import router from './router'
|
||||
import { sharedCount, addLog } from './stores/counterStore'
|
||||
|
||||
// 启动 micro-app 微前端框架
|
||||
// https://jd-opensource.github.io/micro-app/docs.html#/start
|
||||
@@ -21,15 +22,51 @@ microApp.start({
|
||||
},
|
||||
mounted(_e, 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) {
|
||||
console.log(`[micro-app] 子应用 ${appName} 已卸载`)
|
||||
},
|
||||
error(_e, appName) {
|
||||
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)
|
||||
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>
|
||||
</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>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, nextTick, watch } from 'vue'
|
||||
import microApp from '@micro-zoe/micro-app'
|
||||
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>
|
||||
|
||||
<style scoped>
|
||||
@@ -363,4 +462,179 @@ import { subApps } from '@/config/subApps'
|
||||
line-height: 1.8 !important;
|
||||
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>
|
||||
|
||||
Reference in New Issue
Block a user