SSE消息通信集成方案
基于对项目代码的分析,我发现目前系统中消息获取都是通过传统HTTP请求实现的(如定时轮询或手动刷新)。下面设计一套完整的SSE(Server-Sent Events)消息通信集成方案,实现实时消息推送功能。
一、系统架构设计
1. 整体架构
- 服务器端:提供SSE接口,在有新消息时向客户端推送
- 客户端:建立SSE连接,接收并处理推送消息,更新UI提示
2. 消息类型定义
二、核心功能实现
1. 创建SSE服务管理工具
import { ElMessage } from 'element-plus';
import { getToken } from '@/utils/auth';
class SSEManager {
constructor() {
this.eventSource = null;
this.listeners = new Map();
this.reconnectInterval = 5000; // 重连间隔5秒
this.maxReconnectAttempts = 10;
this.reconnectAttempts = 0;
this.isConnected = false;
}
// 建立SSE连接
connect() {
if (this.isConnected) return;
const token = getToken();
if (!token) {
console.warn('用户未登录,无法建立SSE连接');
return;
}
try {
// 假设后端提供的SSE接口路径为/sse/connect
const url = `${import.meta.env.VITE_APP_API_URL || '/api'}/sse/connect?token=${token}`;
this.eventSource = new EventSource(url, {
withCredentials: true
});
this.eventSource.onopen = this.handleOpen.bind(this);
this.eventSource.onmessage = this.handleMessage.bind(this);
this.eventSource.onerror = this.handleError.bind(this);
} catch (error) {
console.error('创建SSE连接失败:', error);
this.scheduleReconnect();
}
}
// 处理连接打开
handleOpen() {
console.log('SSE连接已建立');
this.isConnected = true;
this.reconnectAttempts = 0;
}
// 处理接收到的消息
handleMessage(event) {
try {
const data = JSON.parse(event.data);
const messageType = event.type || 'message';
// 触发对应的事件监听器
if (this.listeners.has(messageType)) {
this.listeners.get(messageType).forEach(listener => {
listener(data);
});
}
// 触发所有消息的通用监听器
if (this.listeners.has('*')) {
this.listeners.get('*').forEach(listener => {
listener(data, messageType);
});
}
} catch (error) {
console.error('解析SSE消息失败:', error);
}
}
// 处理连接错误
handleError(error) {
console.error('SSE连接错误:', error);
this.isConnected = false;
this.close();
this.scheduleReconnect();
}
// 安排重连
scheduleReconnect() {
if (this.reconnectAttempts < this.maxReconnectAttempts) {
setTimeout(() => {
console.log(`尝试重新连接SSE (${++this.reconnectAttempts}/${this.maxReconnectAttempts})`);
this.connect();
}, this.reconnectInterval);
} else {
console.error('达到最大重连次数,停止重连');
}
}
// 添加消息监听器
on(eventType, callback) {
if (!this.listeners.has(eventType)) {
this.listeners.set(eventType, []);
}
this.listeners.get(eventType).push(callback);
// 如果还没有连接,则建立连接
if (!this.isConnected) {
this.connect();
}
}
// 移除消息监听器
off(eventType, callback) {
if (this.listeners.has(eventType)) {
const callbacks = this.listeners.get(eventType);
const index = callbacks.indexOf(callback);
if (index > -1) {
callbacks.splice(index, 1);
}
// 如果该事件类型没有监听器了,则删除该事件类型
if (callbacks.length === 0) {
this.listeners.delete(eventType);
}
}
}
// 关闭连接
close() {
if (this.eventSource) {
this.eventSource.close();
this.eventSource = null;
}
this.isConnected = false;
}
}
// 导出单例实例
export default new SSEManager();
2. 创建消息通知服务
import sseManager from './sse';
import { ElNotification, ElMessage } from 'element-plus';
import useUserStore from '@/stores/user';
class NotificationService {
constructor() {
this.unreadCount = 0;
this.setupSSEListeners();
}
// 设置SSE消息监听器
setupSSEListeners() {
// 监听所有类型的消息
sseManager.on('*', this.handleAnyMessage.bind(this));
// 监听特定类型的消息
sseManager.on('reply', this.handleReplyMessage.bind(this));
sseManager.on('system', this.handleSystemMessage.bind(this));
sseManager.on('notification', this.handleNotificationMessage.bind(this));
}
// 处理任何类型的消息
handleAnyMessage(data, messageType) {
console.log(`收到${messageType}类型消息:`, data);
// 更新未读消息计数
if (!data.isRead) {
this.unreadCount++;
this.updateUnreadCount();
}
}
// 处理回复消息
handleReplyMessage(data) {
ElNotification({
title: '新回复通知',
message: `${data.title}`,
type: 'info',
duration: 5000,
onClick: () => {
// 点击通知时跳转到相关内容
this.navigateToRelatedContent(data);
}
});
}
// 处理系统消息
handleSystemMessage(data) {
ElNotification({
title: '系统通知',
message: `${data.title}`,
type: 'warning',
duration: 5000,
onClick: () => {
// 点击通知时跳转到消息详情
this.navigateToMessageDetail(data.id);
}
});
}
// 处理通知消息
handleNotificationMessage(data) {
ElNotification({
title: '通知提醒',
message: `${data.title}`,
type: 'success',
duration: 5000,
onClick: () => {
// 点击通知时跳转到通知详情
this.navigateToNotificationDetail(data.id);
}
});
}
// 更新未读消息计数
updateUnreadCount() {
// 这里可以通过事件总线或store更新全局未读消息计数
// 例如: useUserStore().setUnreadCount(this.unreadCount);
}
// 跳转到相关内容
navigateToRelatedContent(data) {
if (data.relatedId) {
const router = useRouter();
// 根据不同类型跳转到不同页面
// 例如: router.push(`/community/post/${data.relatedId}`);
}
}
// 跳转到消息详情
navigateToMessageDetail(messageId) {
const router = useRouter();
router.push(`/personal-center/inbox?messageId=${messageId}`);
}
// 跳转到通知详情
navigateToNotificationDetail(notificationId) {
const router = useRouter();
// 跳转到通知详情页面
}
// 初始化服务
init() {
// 检查用户登录状态,如果已登录则连接SSE
const userStore = useUserStore();
if (userStore.name) {
sseManager.connect();
}
}
// 销毁服务
destroy() {
sseManager.close();
}
}
export default new NotificationService();
三、集成到主应用
1. 在main.js中初始化SSE服务
import { createApp } from 'vue';
import App from './App.vue';
import router from './router';
import store from './stores';
import notificationService from './utils/notificationService';
const app = createApp(App);
// ...其他配置
app.use(store);
app.use(router);
// 初始化应用后启动通知服务
app.mount('#app');
notificationService.init();
// 在应用卸载时销毁服务
window.addEventListener('beforeunload', () => {
notificationService.destroy();
});
2. 在Header组件中显示未读消息计数
<template>
<header class="header">
<!-- ...其他Header内容 -->
<div class="header-right">
<!-- 消息通知图标 -->
<div class="notification-icon" @click="navigateToInbox">
<el-icon><Bell /></el-icon>
<span v-if="unreadCount > 0" class="unread-badge">{{ unreadCount }}</span>
</div>
<!-- 用户头像等 -->
</div>
</header>
</template>
<script setup>
import { ref, computed, onMounted, onUnmounted } from 'vue';
import { useRouter } from 'vue-router';
import { Bell } from '@element-plus/icons-vue';
import useUserStore from '@/stores/user';
import notificationService from '@/utils/notificationService';
const router = useRouter();
const userStore = useUserStore();
const unreadCount = ref(0);
// 导航到收件箱
const navigateToInbox = () => {
router.push('/personal-center/inbox');
};
// 监听未读消息数量变化
// 这里假设userStore中有unreadCount字段
const updateUnreadCount = () => {
unreadCount.value = userStore.unreadCount || 0;
};
onMounted(() => {
updateUnreadCount();
// 这里可以添加一个监听器,当未读消息数变化时更新UI
// 例如通过store的subscribe或事件总线
});
onUnmounted(() => {
// 清理监听器
});
</script>
<style scoped>
/* ...其他样式 */
.notification-icon {
position: relative;
cursor: pointer;
padding: 0 15px;
}
.unread-badge {
position: absolute;
top: -5px;
right: 5px;
min-width: 18px;
height: 18px;
padding: 0 6px;
border-radius: 9px;
background-color: #f56c6c;
color: #fff;
font-size: 12px;
line-height: 18px;
text-align: center;
white-space: nowrap;
}
</style>
四、错误处理与优化
-
连接稳定性:
- 实现自动重连机制,处理网络波动
- 记录重连次数,避免无限重连
-
性能优化:
- 仅在用户登录状态下建立SSE连接
- 在页面隐藏时可以考虑关闭连接,显示时重新建立
-
安全性:
- 确保SSE连接使用token认证
- 实现心跳机制,检测连接状态
-
兼容性考虑:
- 对于不支持SSE的浏览器,可以降级为定时轮询
五、后端接口设计建议
为配合前端实现SSE功能,后端需要提供以下接口:
/sse/connect- 建立SSE连接的接口- 支持发送不同类型的消息(回复、系统通知等)
- 实现消息持久化,确保用户离线后上线能看到历史消息
这套方案实现后,当有新的消息(如有人回复了你,或者系统推送了新消息)时,系统会实时通知用户,并更新UI上的未读消息提示。