主应用子应用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

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