在开发基于Vue 3 + WebSocket的实时通信系统时,我们遇到了一个看似简单但影响深远的问题:聊天消息能够实时推送,但通知消息却需要刷新页面才能显示。这个问题的根源在于WebSocket连接管理的架构设计缺陷。本文将详细记录从问题发现到架构重构的完整过程,希望能为遇到类似问题的开发者提供参考。
我们的系统、,包含以下核心功能:
技术栈:
系统上线后,用户反馈了一个奇怪的现象:
首先检查了后端的通知发送逻辑:
@Service
public class NotificationServiceImpl implements NotificationService {
@Autowired
private SimpMessagingTemplate messagingTemplate;
@Override
public void sendNotificationToUser(Long userId, NotificationDTO notification) {
try {
// 发送到用户特定的通知队列
messagingTemplate.convertAndSendToUser(
userId.toString(),
"/queue/notifications",
notification
);
log.info("通知已发送给用户 {}: {}", userId, notification.getTitle());
} catch (Exception e) {
log.error("发送通知失败,用户ID: {}, 错误: {}", userId, e.getMessage());
}
}
}
后端逻辑看起来没有问题,消息确实在发送。
检查前端的消息处理逻辑:
// messageHandler.js
export const messageHandler = {
handleNotification(notification) {
console.log('收到通知:', notification);
// 添加到通知store
const notificationStore = useNotificationStore();
notificationStore.addNotification(notification);
// 显示全局通知
eventBus.emit('show-notification', {
title: notification.title,
text: notification.content,
// ... 其他配置
});
}
};
前端的消息处理逻辑也是正确的。
深入调查后发现了问题的根源:WebSocket连接只在用户访问聊天功能时才建立!
原有的连接逻辑:
// 只有在ChatManagement.vue组件挂载时才连接WebSocket
onMounted(async () => {
await chatStore.connectWebSocket(); // 只有访问聊天页面才会执行
});
这导致了以下问题:
┌─────────────────┐ ┌──────────────────┐ ┌─────────────────┐
│ WebSocket │ │ Chat Store │ │ Notification │
│ Store │◄───┤ │ │ Store │
│ │ │ │ │ │
│ - 连接管理 │ │ - 消息处理 │ │ - 通知处理 │
│ - 重连策略 │ │ - 聊天状态 │ │ - 通知状态 │
│ - 订阅管理 │ └──────────────────┘ └─────────────────┘
└─────────────────┘
▲
│
┌────┴────┐
│ main.js │
│ 应用启动 │
└─────────┘
// store/websocket.js
import { defineStore } from 'pinia'
import { webSocketService } from '@/utils/websocket'
export const useWebSocketStore = defineStore('websocket', {
state: () => ({
connected: false,
connecting: false,
reconnectAttempts: 0,
maxReconnectAttempts: 5,
reconnectInterval: 1000, // 初始重连间隔1秒
maxReconnectInterval: 30000, // 最大重连间隔30秒
connectionHistory: []
}),
actions: {
async connect() {
if (this.connected || this.connecting) {
console.log('WebSocket已连接或正在连接中');
return;
}
try {
this.connecting = true;
console.log('开始建立WebSocket连接...');
await webSocketService.connect();
this.connected = true;
this.connecting = false;
this.reconnectAttempts = 0;
this.reconnectInterval = 1000; // 重置重连间隔
this.addConnectionHistory('connected');
console.log('WebSocket连接成功');
// 通知其他store连接状态变化
this.notifyConnectionChange(true);
} catch (error) {
console.error('WebSocket连接失败:', error);
this.connecting = false;
this.scheduleReconnect();
}
},
scheduleReconnect() {
if (this.reconnectAttempts >= this.maxReconnectAttempts) {
console.error('达到最大重连次数,停止重连');
return;
}
this.reconnectAttempts++;
const delay = Math.min(
this.reconnectInterval * Math.pow(2, this.reconnectAttempts - 1),
this.maxReconnectInterval
);
console.log(`${delay/1000}秒后进行第${this.reconnectAttempts}次重连...`);
setTimeout(() => {
this.connect();
}, delay);
},
// 通知其他store连接状态变化
notifyConnectionChange(connected) {
// 通知聊天store
const chatStore = useChatStore();
chatStore.onWebSocketConnectionChange(connected);
// 通知通知store
const notificationStore = useNotificationStore();
notificationStore.onWebSocketConnectionChange(connected);
}
}
});
移除WebSocket连接逻辑,专注于聊天功能:
// store/chat.js
export const useChatStore = defineStore('chat', {
actions: {
// 移除connectWebSocket方法,改为检查连接状态
checkWebSocketConnection() {
const websocketStore = useWebSocketStore();
if (!websocketStore.connected) {
console.log('WebSocket未连接,请求连接...');
websocketStore.connect();
}
return websocketStore.connected;
},
// WebSocket连接状态变化时的回调
onWebSocketConnectionChange(connected) {
this.wsConnected = connected;
if (connected) {
console.log('WebSocket已连接,聊天功能可用');
// 重新订阅聊天相关频道
this.subscribeToChannels();
} else {
console.log('WebSocket断开,切换到轮询模式');
// 启动轮询作为备用方案
this.startPollingIfNeeded();
}
}
}
});
// main.js
import { useWebSocketStore } from '@/store/websocket'
import { useUserStore } from '@/store/user'
const app = createApp(App)
// 应用启动后初始化WebSocket
app.mount('#app').$nextTick(async () => {
try {
const userStore = useUserStore()
const websocketStore = useWebSocketStore()
// 检查用户登录状态
await userStore.checkLoginStatus()
// 如果用户已登录,建立WebSocket连接
if (userStore.isLoggedIn) {
console.log('用户已登录,初始化WebSocket连接')
await websocketStore.connect()
}
} catch (error) {
console.error('应用初始化失败:', error)
}
})
// store/user.js
export const useUserStore = defineStore('user', {
actions: {
async login(credentials) {
try {
const response = await authApi.login(credentials)
// ... 登录逻辑
// 登录成功后建立WebSocket连接
const websocketStore = useWebSocketStore()
await websocketStore.connect()
} catch (error) {
console.error('登录失败:', error)
throw error
}
},
async logout() {
try {
// 断开WebSocket连接
const websocketStore = useWebSocketStore()
websocketStore.disconnect()
// ... 登出逻辑
} catch (error) {
console.error('登出失败:', error)
}
}
}
})
将所有组件中的 connectWebSocket()
调用替换为 checkWebSocketConnection()
:
// 修复前
await chatStore.connectWebSocket()
// 修复后
chatStore.checkWebSocketConnection()
涉及的文件:
ChatManagement.vue
ChatRoom.vue
GlobalNotification.vue
管理员登录测试
网络断开重连测试
多标签页测试
功能独立性测试
✅ 所有测试场景通过
✅ 实时通知功能正常
✅ 聊天功能不受影响
✅ 自动重连机制工作正常
// 避免重复连接
if (this.connected || this.connecting) {
return; // 直接返回,不重复建立连接
}
// 指数退避算法
const delay = Math.min(
this.reconnectInterval * Math.pow(2, this.reconnectAttempts - 1),
this.maxReconnectInterval
);
// 组件卸载时清理订阅
onUnmounted(() => {
// 不断开全局WebSocket连接,只清理组件特定的订阅
websocketStore.unsubscribeComponent(componentId);
});
这次WebSocket架构重构解决了一个看似简单但影响用户体验的关键问题。通过统一连接管理、智能重连策略和清晰的职责分离,我们不仅修复了通知功能,还为系统的后续扩展奠定了坚实的基础。
在实际开发中,架构设计的重要性往往在问题出现时才被重视。希望这次的经验分享能够帮助其他开发者在设计阶段就考虑到这些问题,避免后期的大规模重构。
关键要点回顾:
本文记录了一次真实的WebSocket架构重构经历,所有代码示例均来自实际项目。如果你在类似项目中遇到相关问题,欢迎交流讨论。