SSE消息通信集成方案(四)

72 阅读5分钟

SSE消息通信集成方案

基于对项目代码的分析,我发现目前系统中消息获取都是通过传统HTTP请求实现的(如定时轮询或手动刷新)。下面设计一套完整的SSE(Server-Sent Events)消息通信集成方案,实现实时消息推送功能。

一、系统架构设计

1. 整体架构

  • 服务器端:提供SSE接口,在有新消息时向客户端推送
  • 客户端:建立SSE连接,接收并处理推送消息,更新UI提示

2. 消息类型定义

// 消息类型定义
export interface ServerSentEvent {
  id?: string;
  event: string;
  data: string;
}

// 消息内容接口
export interface MessageData {
  id: string;
  type: 'reply' | 'system' | 'notification'; // 回复消息、系统消息、通知
  title: string;
  content: string;
  createTime: string;
  isRead: boolean;
  relatedId?: string; // 关联的帖子ID、评论ID等
}

二、核心功能实现

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>

四、错误处理与优化

  1. 连接稳定性

    • 实现自动重连机制,处理网络波动
    • 记录重连次数,避免无限重连
  2. 性能优化

    • 仅在用户登录状态下建立SSE连接
    • 在页面隐藏时可以考虑关闭连接,显示时重新建立
  3. 安全性

    • 确保SSE连接使用token认证
    • 实现心跳机制,检测连接状态
  4. 兼容性考虑

    • 对于不支持SSE的浏览器,可以降级为定时轮询

五、后端接口设计建议

为配合前端实现SSE功能,后端需要提供以下接口:

  1. /sse/connect - 建立SSE连接的接口
  2. 支持发送不同类型的消息(回复、系统通知等)
  3. 实现消息持久化,确保用户离线后上线能看到历史消息

这套方案实现后,当有新的消息(如有人回复了你,或者系统推送了新消息)时,系统会实时通知用户,并更新UI上的未读消息提示。