Files

500 lines
10 KiB
Vue
Raw Permalink Normal View History

<!--
PointToPointVsPubSubDemo.vue
点对点 vs 发布订阅对比演示
-->
<template>
<div class="messaging-patterns-demo">
<div class="header">
<div class="title">
消息模式点对点 vs 发布订阅
</div>
<div class="subtitle">
选择模式观察消息如何分发
</div>
</div>
<div class="mode-selector">
<button
class="mode-btn"
:class="{ active: mode === 'p2p' }"
@click="setMode('p2p')"
>
点对点 (P2P)
</button>
<button
class="mode-btn"
:class="{ active: mode === 'pubsub' }"
@click="setMode('pubsub')"
>
发布订阅 (Pub/Sub)
</button>
</div>
<div class="description">
<div
v-if="mode === 'p2p'"
class="desc-text"
>
<strong>点对点模式</strong>一条消息只能被<strong>一个消费者</strong>消费适合任务分配负载均衡场景
</div>
<div
v-else
class="desc-text"
>
<strong>发布订阅模式</strong>一条消息可以被<strong>多个消费者</strong>同时接收适合事件通知广播场景
</div>
</div>
<div class="demo-area">
<div class="producer-section">
<div class="section-title">
生产者 Producer
</div>
<div class="producer-box">
<div class="icon">
📤
</div>
<div class="label">
订单服务
</div>
</div>
<button
class="send-btn"
:disabled="sending"
@click="sendMessage"
>
{{ sending ? '发送中...' : '发送消息' }}
</button>
</div>
<div class="broker-section">
<div class="section-title">
{{ mode === 'p2p' ? '队列 Queue' : '主题 Topic' }}
</div>
<div class="broker-box">
<div class="broker-icon">
{{ mode === 'p2p' ? '📦' : '📡' }}
</div>
<div class="broker-label">
{{ mode === 'p2p' ? '消息队列' : '发布主题' }}
</div>
<div
v-if="lastMessage"
class="message-indicator"
>
消息 #{{ lastMessage }}
</div>
</div>
<div class="mode-badge">
{{ mode === 'p2p' ? '竞争消费' : '广播' }}
</div>
</div>
<div class="consumer-section">
<div class="section-title">
消费者 Consumers
</div>
<div class="consumers-grid">
<div
v-for="consumer in consumers"
:key="consumer.id"
class="consumer-box"
:class="{ active: consumer.active }"
>
<div class="consumer-icon">
{{ consumer.active ? '⚙️' : '💤' }}
</div>
<div class="consumer-label">
{{ consumer.name }}
</div>
<div class="consumer-count">
已处理: {{ consumer.count }}
</div>
<div class="consumer-status">
{{ consumer.active ? '处理中' : '空闲' }}
</div>
</div>
</div>
</div>
</div>
<div class="comparison-table">
<table>
<thead>
<tr>
<th>特性</th>
<th>点对点 (P2P)</th>
<th>发布订阅 (Pub/Sub)</th>
</tr>
</thead>
<tbody>
<tr>
<td>消息消费</td>
<td>一个消费者</td>
<td>多个消费者</td>
</tr>
<tr>
<td>典型场景</td>
<td>任务分配负载均衡</td>
<td>事件通知数据广播</td>
</tr>
<tr>
<td>消费关系</td>
<td>竞争消费</td>
<td>独立订阅</td>
</tr>
<tr>
<td>例子</td>
<td>Excel 导出任务分发给工作节点</td>
<td>用户注册后发邮件+短信+优惠券</td>
</tr>
</tbody>
</table>
</div>
<div class="example-scenario">
<div class="scenario-title">
📌 实际场景
</div>
<div
v-if="mode === 'p2p'"
class="scenario-content"
>
<div>
<strong>任务分配</strong>批量导入 10000 条用户数据分发给 3
个工作节点并行处理
</div>
<div class="flow">
任务入队 [Worker1, Worker2, Worker3] 竞争抢任务
每个任务只被处理一次
</div>
</div>
<div
v-else
class="scenario-content"
>
<div><strong>事件通知</strong>用户下单成功后同时通知多个系统</div>
<div class="flow">
发布事件 [库存服务, 积分服务, 通知服务, 数据仓库] 各自独立处理
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
const mode = ref('p2p')
const sending = ref(false)
const lastMessage = ref(null)
let messageId = 0
const consumers = ref([
{ id: 1, name: '消费者 A', count: 0, active: false },
{ id: 2, name: '消费者 B', count: 0, active: false },
{ id: 3, name: '消费者 C', count: 0, active: false }
])
const setMode = (newMode) => {
mode.value = newMode
consumers.value.forEach((c) => {
c.count = 0
c.active = false
})
lastMessage.value = null
}
const sendMessage = () => {
if (sending.value) return
sending.value = true
messageId++
lastMessage.value = messageId
setTimeout(() => {
if (mode.value === 'p2p') {
// P2P: 随机选择一个消费者
const availableConsumers = consumers.value.filter((c) => !c.active)
if (availableConsumers.length > 0) {
const consumer =
availableConsumers[
Math.floor(Math.random() * availableConsumers.length)
]
processMessage(consumer)
}
} else {
// Pub/Sub: 所有消费者都接收
consumers.value.forEach((consumer) => {
setTimeout(() => {
processMessage(consumer)
}, Math.random() * 500)
})
}
sending.value = false
}, 500)
}
const processMessage = (consumer) => {
consumer.active = true
setTimeout(
() => {
consumer.count++
consumer.active = false
},
1000 + Math.random() * 1000
)
}
</script>
<style scoped>
.messaging-patterns-demo {
border: 1px solid var(--vp-c-divider);
background: var(--vp-c-bg-soft);
border-radius: 12px;
padding: 1.5rem;
margin: 1.5rem 0;
font-family: var(--vp-font-family-base);
}
.header {
margin-bottom: 1rem;
}
.title {
font-weight: 700;
font-size: 1.05rem;
}
.subtitle {
color: var(--vp-c-text-2);
font-size: 0.9rem;
margin-top: 0.25rem;
}
.mode-selector {
display: flex;
gap: 1rem;
margin-bottom: 1rem;
}
.mode-btn {
flex: 1;
padding: 0.75rem 1rem;
border: 2px solid var(--vp-c-divider);
background: var(--vp-c-bg);
border-radius: 6px;
cursor: pointer;
font-weight: 600;
transition: all 0.2s;
}
.mode-btn:hover {
border-color: var(--vp-c-brand);
}
.mode-btn.active {
background: var(--vp-c-brand);
color: #fff;
border-color: var(--vp-c-brand);
}
.description {
margin-bottom: 1.5rem;
padding: 0.75rem 1rem;
background: rgba(59, 130, 246, 0.1);
border-radius: 6px;
}
.desc-text {
font-size: 0.9rem;
line-height: 1.5;
}
.demo-area {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 1.5rem;
margin-bottom: 1.5rem;
}
.section-title {
font-size: 0.85rem;
font-weight: 600;
color: var(--vp-c-text-2);
text-align: center;
margin-bottom: 0.75rem;
text-transform: uppercase;
}
.producer-section,
.broker-section,
.consumer-section {
display: flex;
flex-direction: column;
align-items: center;
}
.producer-box,
.broker-box {
background: var(--vp-c-bg);
border: 2px solid var(--vp-c-brand);
border-radius: 10px;
padding: 0.75rem;
text-align: center;
min-width: 140px;
margin-bottom: 0.75rem;
}
.icon,
.broker-icon {
font-size: 2rem;
margin-bottom: 0.5rem;
}
.label,
.broker-label {
font-size: 0.9rem;
font-weight: 600;
}
.message-indicator {
margin-top: 0.5rem;
padding: 0.35rem 0.5rem;
background: rgba(59, 130, 246, 0.1);
border-radius: 4px;
font-size: 0.8rem;
font-weight: 600;
}
.send-btn {
background: var(--vp-c-brand);
color: #fff;
border: none;
border-radius: 6px;
padding: 0.6rem 1.2rem;
cursor: pointer;
font-weight: 600;
transition: all 0.2s;
}
.send-btn:hover:not(:disabled) {
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(59, 130, 246, 0.3);
}
.send-btn:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.mode-badge {
padding: 0.4rem 0.8rem;
background: rgba(59, 130, 246, 0.15);
color: var(--vp-c-brand);
border-radius: 6px;
font-size: 0.85rem;
font-weight: 600;
}
.consumers-grid {
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.consumer-box {
background: var(--vp-c-bg);
border: 2px solid var(--vp-c-divider);
border-radius: 6px;
padding: 0.75rem;
text-align: center;
transition: all 0.3s;
}
.consumer-box.active {
border-color: #f59e0b;
background: rgba(245, 158, 11, 0.05);
}
.consumer-icon {
font-size: 1.5rem;
margin-bottom: 0.25rem;
}
.consumer-label {
font-size: 0.85rem;
font-weight: 600;
margin-bottom: 0.25rem;
}
.consumer-count {
font-size: 0.8rem;
color: var(--vp-c-text-2);
}
.consumer-status {
font-size: 0.75rem;
margin-top: 0.25rem;
color: var(--vp-c-text-3);
}
.comparison-table {
margin: 1.5rem 0;
overflow-x: auto;
}
table {
width: 100%;
border-collapse: collapse;
font-size: 0.9rem;
}
th,
td {
padding: 0.75rem;
text-align: left;
border-bottom: 1px solid var(--vp-c-divider);
}
th {
background: var(--vp-c-bg);
font-weight: 600;
}
tr:hover td {
background: var(--vp-c-bg-soft);
}
.example-scenario {
background: var(--vp-c-bg);
border-radius: 10px;
padding: 0.75rem;
border: 1px solid var(--vp-c-divider);
}
.scenario-title {
font-weight: 600;
margin-bottom: 0.75rem;
}
.scenario-content {
font-size: 0.9rem;
line-height: 1.6;
}
.scenario-content > div:first-child {
margin-bottom: 0.5rem;
}
.flow {
margin-top: 0.5rem;
padding: 0.5rem 0.75rem;
background: var(--vp-c-bg-soft);
border-radius: 6px;
font-size: 0.85rem;
font-family: monospace;
}
</style>