主应用子应用globalData通信

This commit is contained in:
2026-06-21 21:09:04 +08:00
parent fb04230958
commit 437b882f03
4 changed files with 946 additions and 0 deletions

View File

@@ -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 + addGlobalDataListenerautoTrigger
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` 保活机制的原理是什么?有什么注意事项?
**答:**

View File

@@ -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')
}
}
})
// 注意:不需要 autoTriggermain.ts 是入口文件sharedCount 默认就是 0
const app = createApp(App)
app.use(router)

View 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)
}

View File

@@ -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>