Vue前端WebSocket接入文档.md 12 KB

Vue前端WebSocket接入文档

📋 环境要求

  • Vue 2.x / Vue 3.x
  • SockJS客户端
  • STOMP.js客户端

🔧 安装依赖

npm install sockjs-client @stomp/stompjs

📝 接入步骤

1. 引入WebSocket库

import SockJS from 'sockjs-client';
import { Client } from '@stomp/stompjs';

2. 创建WebSocket服务

// websocket.service.js
export class WebSocketService {
  constructor() {
    this.client = null;
    this.connected = false;
    this.reconnectAttempts = 0;
    this.maxReconnectAttempts = 5;
  }

  connect(token) {
    if (this.connected) return Promise.resolve();

    return new Promise((resolve, reject) => {
      // 建立WebSocket连接
      const socket = new SockJS('http://localhost:8080/ws-notification');
      this.client = new Client({
        webSocketFactory: () => socket,
        connectHeaders: {
          Authorization: token
        },
        debug: (str) => console.log('STOMP Debug:', str),
        reconnectDelay: 5000,
        heartbeatIncoming: 4000,
        heartbeatOutgoing: 4000,
      });

      // 连接成功回调
      this.client.onConnect = (frame) => {
        console.log('WebSocket连接成功:', frame);
        this.connected = true;
        this.reconnectAttempts = 0;
        this.subscribeToChannels();
        resolve(frame);
      };

      // 连接失败回调
      this.client.onStompError = (frame) => {
        console.error('WebSocket连接失败:', frame);
        this.connected = false;
        reject(frame);
      };

      // 断开连接回调
      this.client.onDisconnect = () => {
        console.log('WebSocket连接断开');
        this.connected = false;
        this.handleReconnect();
      };

      // 启动连接
      this.client.activate();
    });
  }

  // 订阅消息频道
  subscribeToChannels() {
    if (!this.connected) return;

    // 订阅个人消息
    this.client.subscribe('/user/queue/notifications', (message) => {
      const notification = JSON.parse(message.body);
      this.handleMessage(notification);
    });

    // 订阅系统消息
    this.client.subscribe('/user/queue/system', (message) => {
      const notification = JSON.parse(message.body);
      this.handleMessage(notification);
    });

    // 订阅广播消息
    this.client.subscribe('/topic/broadcast', (message) => {
      const notification = JSON.parse(message.body);
      this.handleMessage(notification);
    });
  }

  // 处理接收到的消息
  handleMessage(notification) {
    console.log('收到消息:', notification);

    // 触发全局事件
    window.dispatchEvent(new CustomEvent('websocket-message', {
      detail: notification
    }));

    // 根据消息类型处理
    switch (notification.messageType) {
      case 'SYSTEM':
        this.handleSystemMessage(notification);
        break;
      case 'EMERGENCY':
        this.handleEmergencyMessage(notification);
        break;
      default:
        this.handleDefaultMessage(notification);
    }
  }

  // 发送消息
  sendMessage(destination, body) {
    if (!this.connected) {
      console.error('WebSocket未连接');
      return Promise.reject('WebSocket未连接');
    }

    return new Promise((resolve, reject) => {
      this.client.publish({
        destination: destination,
        body: JSON.stringify(body)
      });
      resolve();
    });
  }

  // 断开连接
  disconnect() {
    if (this.client) {
      this.client.deactivate();
      this.connected = false;
    }
  }

  // 自动重连
  handleReconnect() {
    if (this.reconnectAttempts < this.maxReconnectAttempts) {
      this.reconnectAttempts++;
      console.log(`尝试重连 (${this.reconnectAttempts}/${this.maxReconnectAttempts})`);
      setTimeout(() => {
        this.connect();
      }, 5000);
    } else {
      console.error('重连次数已达上限');
    }
  }
}

3. 在Vue组件中使用

<template>
  <div class="websocket-container">
    <!-- 连接状态 -->
    <div class="status" :class="statusClass">
      状态: {{ statusText }}
    </div>

    <!-- 未读消息数量 -->
    <div class="unread-count">
      未读消息: <span class="count">{{ unreadCount }}</span>
    </div>

    <!-- 消息列表 -->
    <div class="message-list">
      <div
        v-for="message in messages"
        :key="message.id"
        class="message"
        :class="{ 'unread': message.status === 'UNREAD' }"
      >
        <div class="message-header">
          <span class="title">{{ message.title }}</span>
          <span class="type">{{ message.messageType }}</span>
        </div>
        <div class="content">{{ message.content }}</div>
        <div class="time">{{ formatTime(message.sendTime) }}</div>
      </div>
    </div>

    <!-- 控制按钮 -->
    <div class="controls">
      <button @click="connect" :disabled="connected">
        连接WebSocket
      </button>
      <button @click="disconnect" :disabled="!connected">
        断开连接
      </button>
      <button @click="sendTestMessage">
        发送测试消息
      </button>
    </div>
  </div>
</template>

<script>
import { WebSocketService } from '@/services/websocket.service';

export default {
  name: 'WebSocketComponent',
  data() {
    return {
      wsService: new WebSocketService(),
      connected: false,
      statusText: '未连接',
      statusClass: 'disconnected',
      messages: [],
      unreadCount: 0
    };
  },
  mounted() {
    this.initEventListeners();
    this.checkAndConnect();
  },
  beforeDestroy() {
    this.removeEventListeners();
    this.wsService.disconnect();
  },
  methods: {
    // 初始化事件监听
    initEventListeners() {
      window.addEventListener('websocket-message', this.handleWebSocketMessage);
    },

    // 移除事件监听
    removeEventListeners() {
      window.removeEventListener('websocket-message', this.handleWebSocketMessage);
    },

    // 检查并连接
    async checkAndConnect() {
      const token = this.getToken();
      if (token) {
        await this.connect();
      }
    },

    // 获取Token
    getToken() {
      return localStorage.getItem('authToken') || this.$cookies.get('authToken');
    },

    // 连接WebSocket
    async connect() {
      try {
        const token = this.getToken();
        if (!token) {
          this.$message.error('未找到Token,请先登录');
          return;
        }

        await this.wsService.connect(token);
        this.connected = true;
        this.updateStatus('已连接', 'connected');

        // 连接成功后加载历史消息
        this.loadHistoryMessages();

      } catch (error) {
        console.error('连接失败:', error);
        this.$message.error('WebSocket连接失败');
      }
    },

    // 断开连接
    disconnect() {
      this.wsService.disconnect();
      this.connected = false;
      this.updateStatus('已断开', 'disconnected');
    },

    // 更新状态
    updateStatus(text, className) {
      this.statusText = text;
      this.statusClass = className;
    },

    // 处理WebSocket消息
    handleWebSocketMessage(event) {
      const notification = event.detail;

      // 添加到消息列表
      this.messages.unshift(notification);

      // 更新未读数量
      if (notification.status === 'UNREAD') {
        this.unreadCount++;
      }

      // 显示通知
      this.showNotification(notification);
    },

    // 显示通知
    showNotification(notification) {
      if (Notification.permission === 'granted') {
        new Notification(notification.title, {
          body: notification.content,
          icon: '/favicon.ico'
        });
      } else {
        this.$message.info(`${notification.title}: ${notification.content}`);
      }
    },

    // 加载历史消息
    async loadHistoryMessages() {
      try {
        const response = await this.$axios.get('/api/notification/list', {
          headers: {
            Authorization: this.getToken()
          }
        });

        if (response.data.success) {
          this.messages = response.data.result.records || [];
          this.updateUnreadCount();
        }
      } catch (error) {
        console.error('加载历史消息失败:', error);
      }
    },

    // 更新未读数量
    async updateUnreadCount() {
      try {
        const response = await this.$axios.get('/api/notification/unread-count', {
          headers: {
            Authorization: this.getToken()
          }
        });

        if (response.data.success) {
          this.unreadCount = response.data.result;
        }
      } catch (error) {
        console.error('获取未读数量失败:', error);
      }
    },

    // 发送测试消息
    async sendTestMessage() {
      try {
        await this.wsService.sendMessage('/app/message', {
          receiverId: 1,
          messageType: 'SYSTEM',
          title: '测试消息',
          content: '这是一条测试消息',
          priority: 'MEDIUM'
        });
        this.$message.success('消息发送成功');
      } catch (error) {
        console.error('发送消息失败:', error);
        this.$message.error('消息发送失败');
      }
    },

    // 格式化时间
    formatTime(time) {
      return new Date(time).toLocaleString();
    },

    // 标记已读
    async markAsRead(messageId) {
      try {
        await this.$axios.post(`/api/notification/read/${messageId}`, {}, {
          headers: {
            Authorization: this.getToken()
          }
        });

        // 更新消息状态
        const message = this.messages.find(m => m.id === messageId);
        if (message) {
          message.status = 'READ';
          this.unreadCount--;
        }
      } catch (error) {
        console.error('标记已读失败:', error);
      }
    }
  }
};
</script>

<style scoped>
.websocket-container {
  max-width: 800px;
  margin: 0 auto;
  padding: 20px;
}

.status {
  padding: 10px;
  border-radius: 4px;
  margin-bottom: 20px;
  text-align: center;
  font-weight: bold;
}

.status.connected {
  background-color: #d4edda;
  color: #155724;
  border: 1px solid #c3e6cb;
}

.status.disconnected {
  background-color: #f8d7da;
  color: #721c24;
  border: 1px solid #f5c6cb;
}

.unread-count {
  margin-bottom: 20px;
  font-weight: bold;
}

.count {
  color: #e74c3c;
  font-size: 18px;
}

.message-list {
  border: 1px solid #ddd;
  border-radius: 4px;
  max-height: 400px;
  overflow-y: auto;
  margin-bottom: 20px;
}

.message {
  padding: 15px;
  border-bottom: 1px solid #eee;
}

.message:last-child {
  border-bottom: none;
}

.message.unread {
  background-color: #fff3cd;
  border-left: 4px solid #ffc107;
}

.message-header {
  display: flex;
  justify-content: space-between;
  margin-bottom: 8px;
}

.title {
  font-weight: bold;
}

.type {
  font-size: 12px;
  padding: 2px 6px;
  border-radius: 3px;
  background-color: #007bff;
  color: white;
}

.content {
  margin-bottom: 8px;
  line-height: 1.5;
}

.time {
  font-size: 12px;
  color: #666;
}

.controls {
  display: flex;
  gap: 10px;
}

.controls button {
  padding: 10px 20px;
  border: none;
  border-radius: 4px;
  cursor: pointer;
}

.controls button:not(:disabled) {
  background-color: #007bff;
  color: white;
}

.controls button:disabled {
  background-color: #6c757d;
  cursor: not-allowed;
}
</style>

🚀 使用说明

  1. 安装依赖: npm install sockjs-client @stomp/stompjs
  2. 创建服务: 复制 WebSocketService 类到项目中
  3. 集成组件: 在需要的Vue组件中使用
  4. 处理消息: 监听 websocket-message 事件
  5. 错误处理: 实现重连机制和错误提示

⚠️ 注意事项

  • 确保Token有效且格式正确
  • 处理网络断开和重连
  • 及时清理事件监听器
  • 适配后端API接口格式