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>
🚀 使用说明
- 安装依赖:
npm install sockjs-client @stomp/stompjs
- 创建服务: 复制
WebSocketService 类到项目中
- 集成组件: 在需要的Vue组件中使用
- 处理消息: 监听
websocket-message 事件
- 错误处理: 实现重连机制和错误提示
⚠️ 注意事项
- 确保Token有效且格式正确
- 处理网络断开和重连
- 及时清理事件监听器
- 适配后端API接口格式