WebSocket 功能说明文档
目录
功能概述
WebSocket 功能为小程序提供实时通信能力,支持以下功能:
- 聊天室功能:多人在线实时聊天,支持文本和图片消息
- 私信功能:用户之间的点对点私信聊天
- 自动重连:网络断开时自动重连,支持指数退避策略
- 心跳机制:定期发送心跳包,保持连接活跃
- 消息队列:连接断开时缓存消息,连接恢复后自动发送
- 消息缓存:缓存重要消息,供后注册的页面获取
- 性能监控:实时监控连接性能指标
架构设计
三层架构
核心层 (WebSocketManager)
服务层 (WebSocketService)
页面层 (Vue Components)
chatroom.vue
聊天室页面
myMessageDetail.vue
私信详情页
myMessage.vue
消息列表页
统一连接管理
单例模式
消息分发
观察者模式
消息缓存
30秒有效期
连接管理
自动重连
指数退避
心跳机制
消息队列
设计特点
- 单例模式:全局只有一个 WebSocket 连接,避免资源浪费
- 事件分发:使用观察者模式,将消息分发到各个页面
- 消息缓存:重要消息自动缓存,新页面注册时可立即获取
- 自动重连:支持指数退避重连策略,避免频繁重连
- 容错机制:心跳容错、消息队列、连接超时等
核心组件
1. WebSocketManager (utils/websocket.js)
WebSocket 连接的核心管理器,负责:
- WebSocket 连接的建立和维护
- 自动重连机制(指数退避)
- 心跳机制(保持连接活跃)
- 消息队列(连接断开时缓存消息)
- 性能监控(连接耗时、消息延迟等)
关键属性:
ws: WebSocket 连接实例messageQueue: 消息队列(最大100条)reconnectAttempts: 重连次数currentReconnectDelay: 当前重连延迟(指数递增)
2. WebSocketService (utils/websocketService.js)
全局 WebSocket 服务,负责:
- 统一管理 WebSocket 连接(单例模式)
- 消息分发到各个页面(观察者模式)
- 消息缓存机制(缓存重要消息)
- 页面生命周期管理(注册/注销监听器)
关键属性:
wsManager: WebSocketManager 实例listeners: 消息监听器 Map(key: pageId, value: callback)statusListeners: 状态监听器 MapmessageCache: 消息缓存 Map(key: 消息类型, value: { data, timestamp })
3. 页面组件
- chatroom.vue:聊天室页面,支持多人在线聊天
- myMessageDetail.vue:私信详情页面,支持点对点聊天
- myMessage.vue:消息列表页面,显示私信和系统消息
关键逻辑
1. 初始化流程
"WebSocket服务器" WebSocketManager WebSocketService App.vue "WebSocket服务器" WebSocketManager WebSocketService App.vue init(userInfo) new WebSocketManager() connect() connect() uni.connectSocket() onSocketOpen authenticate() 发送认证消息 login_success handleMessage(login_success) cacheMessage(login_success) 分发消息到所有监听器
2. 消息分发流程
是
否
是
否
WebSocket收到消息
是否重要消息?
缓存消息
login_success/history/users_list
直接分发
广播到所有监听器
页面1处理消息
页面2处理消息
页面N处理消息
新页面注册监听
连接已建立?
立即分发缓存消息
等待连接建立
页面收到缓存消息
3. 自动重连流程
否
是
是
否
是
否
连接断开
autoReconnect?
停止
计算重连延迟
min(初始延迟 × 2^次数, 最大延迟)
等待延迟时间
尝试重连
重连成功?
重置重连计数和延迟
重连次数+1
超过最大次数?
发送队列中的消息
4. 心跳机制
"WebSocket服务器" WebSocketManager "WebSocket服务器" WebSocketManager "连接成功后延迟2秒启动心跳" "设置心跳超时定时器(30秒)" "允许继续,不断开连接" alt [容错模式且计数小于3] [非容错模式或计数大于等于- 3] alt [收到响应] [超时未响应] loop [每60秒] 发送心跳包 {type: 'ping'} {type: 'pong'} "清除超时定时器 重置心跳计数" 心跳计数+1 "断开连接 触发重连"
5. 消息队列机制
是
否
是
否
页面发送消息
连接已建立?
立即发送
加入消息队列
队列已满?
移除最旧的消息
添加到队列
等待连接恢复
连接恢复
发送队列中的所有消息
清空队列
6. 聊天室页面初始化流程
"WebSocket服务器" WebSocketManager WebSocketService chatroom.vue "WebSocket服务器" WebSocketManager WebSocketService chatroom.vue alt [login_success 包含历史消息] [login_success 不包含历史消息] setBuildAuthData(聊天室认证) onMessage(pageId, callback, true) onStatusChange(pageId, callback) connect() connect() 建立连接 连接成功 authenticate(聊天室认证) {type: 'login', user_id, is_admin} login_success {users, messages?} handleMessage(login_success) cacheMessage(login_success) 分发 login_success processHistoryMessages() sendMessage({type: 'get_history'}) 请求历史消息 history {messages} 分发 history sendMessage({type: 'get_users'}) 请求用户列表 users_list {users} 分发 users_list
7. 私信页面初始化流程
"WebSocket服务器" WebSocketManager WebSocketService myMessageDetail.vue "WebSocket服务器" WebSocketManager WebSocketService myMessageDetail.vue alt [连接未建立] "加载历史消息(HTTP API)" setBuildAuthData(私信认证) onMessage(pageId, callback) onStatusChange(pageId, callback) connect() connect() 建立连接 连接成功 authenticate(私信认证) {type: 'login', user_id, nickname} login_success handleMessage(login_success) 状态变为 connected sendMessage({type: 'enter_private_chat', target_id}) 进入私聊状态 确认进入私聊 loadHistory()
8. 认证数据自定义机制
不同页面需要不同的认证数据:
- 聊天室 :需要
is_admin字段,用于判断管理员权限 - 私信 :需要
nickname字段,用于显示发送者名称
实现方式:
- 每个页面在初始化时调用
setBuildAuthData()设置自定义认证函数 - WebSocketManager 在认证时调用该函数构建认证数据
- 支持动态切换,后设置的会覆盖先设置的
9. 消息缓存机制
缓存的消息类型:
login_success:登录成功消息,包含在线用户列表和历史消息history:历史消息列表users_list:在线用户列表
缓存策略:
- 缓存时间:30秒
- 自动清理:过期消息自动删除
- 新页面注册:如果连接已建立,立即分发缓存的消息
使用场景:
- 用户从聊天室页面切换到其他页面,再返回聊天室
- 页面加载时连接已建立,可以立即获取历史数据
10. 页面生命周期管理
页面加载
初始化WebSocket
onMessage/onStatusChange
确保连接
正常使用
页面隐藏
不断开连接
页面显示
继续使用
页面卸载
offMessage/offStatusChange
onLoad
initWebSocket
注册监听器
连接建立
接收消息
onHide
保持连接
onShow
onUnload
取消监听
消息类型
聊天室消息类型
发送消息
javascript
// 文本消息
{ type: 'message', content: '消息内容', message_type: 1 }
// 图片消息
{ type: 'message', content: '图片URL', message_type: 2 }
// 获取历史消息
{ type: 'get_history' }
// 获取用户列表
{ type: 'get_users' }
// 删除消息(管理员)
{ type: 'delete_message', message_id: 123 }
// 输入状态
{ type: 'typing', is_typing: true }
接收消息
javascript
// 登录成功
{ type: 'login_success', users: [], messages: [] }
// 新消息
{ type: 'new_message', message: {...} }
// 历史消息
{ type: 'history', messages: [] }
// 用户列表
{ type: 'users_list', users: [] }
// 用户加入/离开
{ type: 'user_joined', nickname: '用户名' }
{ type: 'user_left', nickname: '用户名' }
// 输入状态
{ type: 'user_typing', nickname: '用户名', is_typing: true }
// 消息删除
{ type: 'message_deleted', message_id: 123 }
// 系统消息
{ type: 'system', content: '系统消息内容' }
// 错误消息
{ type: 'error', message: '错误信息' }
私信消息类型
发送消息
javascript
// 进入私聊状态
{ type: 'enter_private_chat', target_id: 123 }
// 发送私信
{ type: 'private_message', receiver_id: 123, content: '消息内容', message_type: 1 }
接收消息
javascript
// 新私信
{ type: 'new_private_message', message: {...} }
// 发送成功回执
{ type: 'private_message_success', message: {...} }
注意事项
1. 页面生命周期管理
必须 在 onUnload 中取消监听,避免内存泄漏:
javascript
onUnload() {
websocketService.offMessage(this.pageId);
websocketService.offStatusChange(this.pageId);
}
2. 页面唯一标识
每个页面必须使用唯一的 pageId,建议使用页面路径或功能名称。
3. 消息缓存
重要消息(login_success、history、users_list)会自动缓存 30 秒。新页面注册时,如果连接已建立,会自动分发缓存的消息。
4. 认证数据自定义
不同页面可能需要不同的认证数据:
- 聊天室 :需要
is_admin字段 - 私信 :需要
nickname字段
使用 setBuildAuthData() 设置自定义认证函数。
5. 连接状态检查
发送消息前,建议检查连接状态。
6. 自动重连
WebSocket 支持自动重连,使用指数退避策略:
- 初始延迟:3 秒
- 最大延迟:30 秒
- 重连次数:无限(默认)
7. 心跳机制
心跳机制用于保持连接活跃:
- 心跳间隔:60 秒(默认)
- 心跳超时:30 秒(默认)
- 容错模式:开启(默认)
如果服务器不支持心跳响应,容错模式会允许一定次数的心跳超时,不会立即断开连接。
8. 消息队列
连接断开时,消息会自动进入队列。连接恢复后,队列中的消息会自动发送。队列最大长度为 100,超出限制会移除最旧的消息。
9. 性能监控
性能监控默认开启,可以获取以下指标:
- 连接耗时
- 重连次数
- 发送/接收消息数
- 消息延迟
- 连接持续时间
10. 错误处理
建议在消息处理函数中添加错误处理。
常见问题
Q: 为什么页面注册后收不到消息?
A: 检查以下几点:
- 是否在
onUnload中取消了监听 pageId是否唯一- 连接是否已建立(
websocketService.isConnected()) - 消息类型是否正确
Q: 如何获取历史消息?
A: 有两种方式:
- 在
login_success消息中可能包含历史消息 - 连接成功后,发送
{ type: 'get_history' }请求
Q: 如何判断连接状态?
A: 使用 websocketService.isConnected() 或注册状态监听。
Q: 消息发送失败怎么办?
A: 检查以下几点:
- 连接是否已建立
- 消息格式是否正确
- 查看控制台错误信息
Q: 如何清除消息缓存?
A: 使用 clearCache 方法。
更新日志
v1.0.0 (2025-12-24)
- 初始版本
- 支持聊天室和私信功能
- 支持自动重连和心跳机制
- 支持消息队列和缓存
- 支持性能监控
/utils/websocket.js 文件内容
javascript
import Storage from '@/utils/storage.js';
import { BASE_URL } from '@/utils/config.js';
/**
* WebSocket 工具类(优化版)
* 封装通用的 WebSocket 连接、认证、消息处理等功能
*
* @class WebSocketManager
* @description 提供统一的 WebSocket 连接管理,包括自动重连、心跳机制、消息队列、性能监控等
*/
class WebSocketManager {
/**
* 构造函数
* @param {Object} options - 配置选项
* @param {Number|String} options.userId - 用户ID(必填)
* @param {String} options.nickname - 用户昵称
* @param {Boolean} options.isAdmin - 是否为管理员
* @param {Number} options.connectTimeout - 连接超时时间(毫秒),默认10000
* @param {Number} options.reconnectDelay - 初始重连延迟时间(毫秒),默认3000
* @param {Number} options.maxReconnectDelay - 最大重连延迟时间(毫秒),默认30000
* @param {Number} options.reconnectAttempts - 最大重连次数,默认Infinity
* @param {Boolean} options.autoReconnect - 是否自动重连,默认true
* @param {Boolean} options.showError - 是否显示连接错误提示,默认true
* @param {Boolean} options.autoAuth - 连接成功后是否自动认证,默认true
* @param {Number} options.heartbeatInterval - 心跳间隔(毫秒),默认30000(30秒)
* @param {Number} options.heartbeatTimeout - 心跳超时时间(毫秒),默认10000(10秒)
* @param {Number} options.maxMessageQueueSize - 消息队列最大长度,默认100
* @param {Boolean} options.enablePerformanceMonitor - 是否启用性能监控,默认true
* @param {Function} options.buildAuthData - 自定义认证数据构建函数
* @param {Function} options.onMessage - 消息处理回调函数
* @param {Function} options.onStatusChange - 连接状态变化回调函数
* @param {Function} options.onConnected - 连接成功回调函数
* @param {Function} options.onError - 连接失败回调函数
* @param {Function} options.onClose - 连接关闭回调函数
* @param {Function} options.onPerformanceUpdate - 性能指标更新回调函数
*/
constructor(options = {}) {
// 用户信息(统一使用 userId,前端驼峰命名)
this.userId = options.userId || options.user_id || null; // 兼容 user_id 参数
this.nickname = options.nickname || '';
this.isAdmin = options.isAdmin || options.is_admin || false; // 兼容 is_admin 参数
// WebSocket 连接
this.ws = null;
this.isConnected = false;
this.connectionStatus = 'disconnected'; // connecting, connected, disconnected
// 定时器
this.reconnectTimer = null;
this.connectTimeoutTimer = null;
this.heartbeatTimer = null;
this.heartbeatTimeoutTimer = null;
// 心跳相关
this.heartbeatMissedCount = 0; // 心跳未响应次数
this.maxHeartbeatMissed = 3; // 最大允许心跳未响应次数(容错模式下)
// 重连相关
this.reconnectAttempts = 0; // 当前重连次数
this.currentReconnectDelay = options.reconnectDelay || 3000; // 当前重连延迟
// 消息队列(连接断开时缓存消息)
this.messageQueue = [];
this.maxMessageQueueSize = options.maxMessageQueueSize || 100;
// 配置选项
this.options = {
// 连接超时时间(毫秒)- 增加到20秒,适应网络较慢的情况
connectTimeout: options.connectTimeout || 20000,
// 初始重连延迟时间(毫秒)
reconnectDelay: options.reconnectDelay || 3000,
// 最大重连延迟时间(毫秒)
maxReconnectDelay: options.maxReconnectDelay || 30000,
// 最大重连次数
reconnectAttempts: options.reconnectAttempts !== undefined ? options.reconnectAttempts : Infinity,
// 是否自动重连
autoReconnect: options.autoReconnect !== false,
// 是否显示连接错误提示
showError: options.showError !== false,
// 连接成功后是否自动认证
autoAuth: options.autoAuth !== false,
// 心跳间隔(毫秒)- 增加到60秒,减少心跳频率
heartbeatInterval: options.heartbeatInterval || 60000,
// 心跳超时时间(毫秒)- 增加到30秒,给服务器更多响应时间
heartbeatTimeout: options.heartbeatTimeout || 30000,
// 是否启用心跳
enableHeartbeat: options.enableHeartbeat !== false,
// 心跳容错模式:如果服务器不支持心跳响应,设置为true时不会因心跳超时而断开连接
heartbeatTolerant: options.heartbeatTolerant !== false,
// 是否启用性能监控
enablePerformanceMonitor: options.enablePerformanceMonitor !== false,
// 认证数据构建函数
buildAuthData: options.buildAuthData || null,
// 消息处理回调
onMessage: options.onMessage || null,
// 连接状态变化回调
onStatusChange: options.onStatusChange || null,
// 连接成功回调
onConnected: options.onConnected || null,
// 连接失败回调
onError: options.onError || null,
// 连接关闭回调
onClose: options.onClose || null,
// 性能指标更新回调
onPerformanceUpdate: options.onPerformanceUpdate || null,
};
// 错误提示标记(避免频繁提示)
this.hasShownError = false;
// 性能监控指标
this.performanceMetrics = {
connectStartTime: null, // 连接开始时间
connectEndTime: null, // 连接结束时间
connectDuration: 0, // 连接耗时(毫秒)
reconnectCount: 0, // 重连次数
totalMessagesSent: 0, // 发送消息总数
totalMessagesReceived: 0, // 接收消息总数
messageLatencies: [], // 消息延迟数组(用于计算平均延迟)
lastMessageTime: null, // 最后一条消息时间
connectionUptime: 0, // 连接持续时间(毫秒)
connectionStartTime: null, // 连接开始时间戳
};
}
/**
* 构建 WebSocket URL
* @returns {String} WebSocket 连接地址
* @description 根据 BASE_URL 自动判断使用 ws:// 还是 wss:// 协议
*/
buildWsUrl() {
// 将 BASE_URL 中的 http/https 替换为 ws/wss
if (BASE_URL.includes('https')) {
return 'wss://' + BASE_URL.replace(/^https?:\/\//, '') + '/wss/';
} else {
return 'ws://' + BASE_URL.replace(/^http?:\/\//, '') + '/wss/';
}
}
/**
* 连接 WebSocket
* @returns {void}
* @description 建立 WebSocket 连接,包括连接超时处理、事件监听、自动认证、性能监控等
*/
connect() {
if (!this.userId) {
console.error('WebSocket 连接失败:用户ID未获取');
return;
}
// 避免重复连接
if (this.ws && this.connectionStatus === 'connecting') {
return;
}
// 清除之前的超时定时器
this.clearConnectTimeout();
this.clearHeartbeat();
// 记录连接开始时间(性能监控)
if (this.options.enablePerformanceMonitor) {
this.performanceMetrics.connectStartTime = Date.now();
}
this.setConnectionStatus('connecting');
try {
this.ws = uni.connectSocket({
url: this.buildWsUrl(),
header: {
Appid: 'F8CFA01E7757FE89',
Terminal: Storage.get('phbaAlumni_provider') || 'weixin',
},
success: () => {
console.log('WebSocket 连接中...', this.buildWsUrl());
},
fail: (err) => {
console.error('connectSocket error', err);
this.clearConnectTimeout();
this.setConnectionStatus('disconnected');
this.isConnected = false;
this.showConnectionError();
if (this.options.onError) {
this.options.onError(err);
}
if (this.options.autoReconnect) {
this.scheduleReconnect();
}
},
});
// 设置连接超时
this.connectTimeoutTimer = setTimeout(() => {
if (this.connectionStatus === 'connecting') {
console.warn(`[WebSocket] 连接超时(${this.options.connectTimeout}ms)`);
this.clearConnectTimeout();
this.setConnectionStatus('disconnected');
this.isConnected = false;
if (this.ws) {
uni.closeSocket();
this.ws = null;
}
this.showConnectionError();
if (this.options.onError) {
this.options.onError(new Error(`连接超时(${this.options.connectTimeout}ms)`));
}
if (this.options.autoReconnect) {
this.scheduleReconnect();
}
}
}, this.options.connectTimeout);
// 使用全局事件监听器
uni.onSocketOpen(() => {
const connectTime = this.performanceMetrics.connectStartTime
? Date.now() - this.performanceMetrics.connectStartTime
: 0;
console.log(`[WebSocket] 连接已建立,耗时: ${connectTime}ms`);
this.clearConnectTimeout();
this.isConnected = true;
this.setConnectionStatus('connected');
this.hasShownError = false; // 连接成功后重置错误提示标记
// 记录连接结束时间(性能监控)
if (this.options.enablePerformanceMonitor) {
this.performanceMetrics.connectEndTime = Date.now();
this.performanceMetrics.connectDuration =
this.performanceMetrics.connectEndTime - this.performanceMetrics.connectStartTime;
this.performanceMetrics.connectionStartTime = Date.now();
this.updatePerformanceMetrics();
}
// 重置重连计数和延迟
this.reconnectAttempts = 0;
this.currentReconnectDelay = this.options.reconnectDelay;
// 发送队列中的消息
this.flushMessageQueue();
if (this.options.onConnected) {
this.options.onConnected();
}
// 自动认证
if (this.options.autoAuth) {
this.authenticate();
}
// 启动心跳(延迟启动,给认证一些时间)
if (this.options.enableHeartbeat) {
setTimeout(() => {
if (this.isConnected) {
this.startHeartbeat();
}
}, 2000); // 延迟2秒启动心跳
}
});
uni.onSocketMessage((res) => {
try {
const data = JSON.parse(res.data);
console.log('WebSocket 收到消息:', data);
// 处理心跳响应(支持多种心跳响应格式)
if (data.type === 'pong' ||
data.type === 'heartbeat' ||
data.type === 'ping_response' ||
(data.type === 'ping' && data.response)) {
this.handleHeartbeatResponse();
return;
}
// 记录接收消息时间(性能监控)
if (this.options.enablePerformanceMonitor) {
this.performanceMetrics.totalMessagesReceived++;
this.performanceMetrics.lastMessageTime = Date.now();
}
this.handleMessage(data);
} catch (e) {
console.error('[WebSocket] 消息解析失败', e, res.data);
}
});
uni.onSocketClose(() => {
console.log('WebSocket 连接已关闭');
this.clearConnectTimeout();
this.clearHeartbeat();
this.isConnected = false;
this.setConnectionStatus('disconnected');
if (this.options.onClose) {
this.options.onClose();
}
if (this.options.autoReconnect) {
this.scheduleReconnect();
}
});
uni.onSocketError((err) => {
console.error('socket error', err);
this.clearConnectTimeout();
this.clearHeartbeat();
this.setConnectionStatus('disconnected');
this.isConnected = false;
this.showConnectionError();
if (this.options.onError) {
this.options.onError(err);
}
});
} catch (error) {
console.error('连接失败', error);
this.clearConnectTimeout();
this.clearHeartbeat();
this.setConnectionStatus('disconnected');
this.isConnected = false;
this.showConnectionError();
if (this.options.onError) {
this.options.onError(error);
}
if (this.options.autoReconnect) {
this.scheduleReconnect();
}
}
}
/**
* 设置连接状态
* @param {String} status - 连接状态:'connecting' | 'connected' | 'disconnected'
* @returns {void}
* @description 更新内部连接状态并触发状态变化回调
*/
setConnectionStatus(status) {
this.connectionStatus = status;
if (this.options.onStatusChange) {
this.options.onStatusChange(status);
}
}
/**
* 清除连接超时定时器
* @returns {void}
* @description 清理连接超时定时器,防止内存泄漏
*/
clearConnectTimeout() {
if (this.connectTimeoutTimer) {
clearTimeout(this.connectTimeoutTimer);
this.connectTimeoutTimer = null;
}
}
/**
* 显示连接错误提示
* @returns {void}
* @description 显示连接失败提示,避免频繁弹窗(使用 hasShownError 标记)
*/
showConnectionError() {
if (this.options.showError && !this.hasShownError) {
this.hasShownError = true;
uni.showToast({
title: '连接失败,将自动重试',
icon: 'none',
duration: 2000,
});
}
}
/**
* 安排重连(指数退避算法)
* @returns {void}
* @description 使用指数退避算法延迟重连,避免频繁尝试连接。重连延迟会逐渐增加,直到达到最大值
*/
scheduleReconnect() {
if (!this.options.autoReconnect) {
return;
}
// 检查是否超过最大重连次数
if (this.reconnectAttempts >= this.options.reconnectAttempts) {
console.warn('WebSocket 已达到最大重连次数,停止重连');
return;
}
// 清除之前的重连定时器
if (this.reconnectTimer) {
clearTimeout(this.reconnectTimer);
}
// 指数退避:延迟时间 = min(初始延迟 * 2^重连次数, 最大延迟)
const delay = Math.min(
this.currentReconnectDelay * Math.pow(2, this.reconnectAttempts),
this.options.maxReconnectDelay
);
console.log(`WebSocket 将在 ${delay}ms 后尝试重连(第 ${this.reconnectAttempts + 1} 次)`);
// 延迟重连
this.reconnectTimer = setTimeout(() => {
if (!this.isConnected && this.userId) {
this.reconnectAttempts++;
if (this.options.enablePerformanceMonitor) {
this.performanceMetrics.reconnectCount++;
this.updatePerformanceMetrics();
}
console.log(`[WebSocket] 尝试重新连接...(第 ${this.reconnectAttempts} 次,延迟 ${delay}ms)`);
this.connect();
}
}, delay);
}
/**
* 认证登录
* @returns {void}
* @description 向服务器发送认证信息。如果提供了自定义 buildAuthData 函数则使用它,否则使用默认格式
* @note 默认认证数据格式:{ type: 'login', user_id: userId, nickname: nickname, is_admin?: isAdmin }
*/
authenticate() {
if (!this.userId) {
console.warn('WebSocket 认证失败:userId 为空');
return;
}
// 如果提供了自定义认证数据构建函数,使用它
let authData;
if (this.options.buildAuthData) {
// 传递用户信息给自定义函数(统一使用驼峰命名)
authData = this.options.buildAuthData({
userId: this.userId, // 前端统一使用 userId
nickname: this.nickname,
isAdmin: this.isAdmin,
});
} else {
// 默认认证数据(发送给服务器时使用下划线命名,符合后端API规范)
authData = {
type: 'login',
user_id: this.userId, // 后端使用 user_id
nickname: this.nickname,
};
// 如果是管理员,添加 is_admin 字段
if (this.isAdmin) {
authData.is_admin = this.isAdmin; // 后端使用 is_admin
}
}
console.log('发送 WebSocket 认证信息:', authData);
this.sendSocketMessage(authData);
}
/**
* 发送 WebSocket 消息
* @param {Object} data - 要发送的消息对象
* @param {Boolean} urgent - 是否紧急消息(紧急消息不会进入队列),默认false
* @returns {Boolean} 是否成功发送或已加入队列
* @description 向服务器发送 WebSocket 消息。如果连接断开,消息会被加入队列,连接恢复后自动发送
*/
sendSocketMessage(data, urgent = false) {
// 如果是心跳消息,必须立即发送
if (data.type === 'ping' || data.type === 'heartbeat') {
urgent = true;
}
if (!this.isConnected) {
if (urgent) {
console.warn('WebSocket 未连接,无法发送紧急消息');
return false;
}
// 将消息加入队列
this.enqueueMessage(data);
return true;
}
try {
const sendTime = Date.now();
uni.sendSocketMessage({
data: JSON.stringify(data),
success: () => {
// 记录发送消息(性能监控)
if (this.options.enablePerformanceMonitor) {
this.performanceMetrics.totalMessagesSent++;
// 如果消息有 id,可以用于计算延迟
if (data.id) {
const latency = Date.now() - sendTime;
this.performanceMetrics.messageLatencies.push(latency);
// 只保留最近100条消息的延迟数据
if (this.performanceMetrics.messageLatencies.length > 100) {
this.performanceMetrics.messageLatencies.shift();
}
}
}
},
fail: (err) => {
console.error('WebSocket 发送消息失败:', err);
// 发送失败,如果不是紧急消息,加入队列
if (!urgent) {
this.enqueueMessage(data);
}
if (this.options.onError) {
this.options.onError(err);
}
},
});
return true;
} catch (error) {
console.error('WebSocket 发送消息异常:', error);
// 发送异常,如果不是紧急消息,加入队列
if (!urgent) {
this.enqueueMessage(data);
}
if (this.options.onError) {
this.options.onError(error);
}
return false;
}
}
/**
* 将消息加入队列
* @param {Object} data - 消息对象
* @returns {void}
* @description 当连接断开时,将消息加入队列,连接恢复后自动发送
*/
enqueueMessage(data) {
// 如果队列已满,移除最旧的消息
if (this.messageQueue.length >= this.maxMessageQueueSize) {
console.warn('消息队列已满,移除最旧的消息');
this.messageQueue.shift();
}
this.messageQueue.push({
data,
timestamp: Date.now(),
});
console.log(`消息已加入队列,当前队列长度: ${this.messageQueue.length}`);
}
/**
* 发送队列中的消息
* @returns {void}
* @description 连接恢复后,发送队列中缓存的消息
*/
flushMessageQueue() {
if (this.messageQueue.length === 0) {
return;
}
console.log(`开始发送队列中的 ${this.messageQueue.length} 条消息`);
const messages = [...this.messageQueue];
this.messageQueue = [];
// 逐条发送队列中的消息
messages.forEach((item, index) => {
setTimeout(() => {
this.sendSocketMessage(item.data, false);
}, index * 50); // 每条消息间隔50ms,避免服务器压力过大
});
}
/**
* 处理接收到的消息
* @param {Object} data - 接收到的消息对象(已解析的 JSON)
* @returns {void}
* @description 将接收到的消息传递给 onMessage 回调函数处理
*/
handleMessage(data) {
if (this.options.onMessage) {
this.options.onMessage(data, this);
}
}
/**
* 断开连接
* @returns {void}
* @description 断开 WebSocket 连接并清理所有资源
*/
disconnect() {
this.cleanup();
}
/**
* 启动心跳
* @returns {void}
* @description 定期发送心跳包,保持连接活跃
*/
startHeartbeat() {
this.clearHeartbeat();
// 立即发送一次心跳
this.sendHeartbeat();
// 设置定时心跳
this.heartbeatTimer = setInterval(() => {
if (this.isConnected) {
this.sendHeartbeat();
} else {
this.clearHeartbeat();
}
}, this.options.heartbeatInterval);
}
/**
* 发送心跳包
* @returns {void}
* @description 发送心跳消息,并设置超时检测
*/
sendHeartbeat() {
if (!this.isConnected) {
return;
}
console.log(`[WebSocket] 发送心跳包,等待 ${this.options.heartbeatTimeout}ms 响应`);
// 发送心跳消息
this.sendSocketMessage({
type: 'ping',
timestamp: Date.now(),
}, true); // 紧急消息,必须立即发送
// 设置心跳超时检测
this.clearHeartbeatTimeout();
this.heartbeatTimeoutTimer = setTimeout(() => {
this.heartbeatMissedCount++;
console.warn(`[WebSocket] 心跳超时(${this.options.heartbeatTimeout}ms),未响应次数: ${this.heartbeatMissedCount}`);
// 容错模式:如果服务器不支持心跳响应,允许一定次数的超时
if (this.options.heartbeatTolerant && this.heartbeatMissedCount < this.maxHeartbeatMissed) {
console.log(`[WebSocket] 心跳容错模式:允许继续连接(${this.heartbeatMissedCount}/${this.maxHeartbeatMissed})`);
return; // 不断开连接,继续等待
}
// 心跳超时,主动关闭连接,触发重连
console.warn(`[WebSocket] 心跳超时次数过多,断开连接`);
if (this.ws) {
uni.closeSocket();
this.ws = null;
}
this.isConnected = false;
this.setConnectionStatus('disconnected');
if (this.options.autoReconnect) {
this.scheduleReconnect();
}
}, this.options.heartbeatTimeout);
}
/**
* 处理心跳响应
* @returns {void}
* @description 收到服务器心跳响应后,清除超时定时器
*/
handleHeartbeatResponse() {
console.log('[WebSocket] 收到心跳响应');
this.clearHeartbeatTimeout();
this.heartbeatMissedCount = 0; // 重置未响应计数
}
/**
* 清除心跳相关定时器
* @returns {void}
* @description 清理心跳定时器和超时定时器
*/
clearHeartbeat() {
if (this.heartbeatTimer) {
clearInterval(this.heartbeatTimer);
this.heartbeatTimer = null;
}
this.clearHeartbeatTimeout();
}
/**
* 清除心跳超时定时器
* @returns {void}
* @description 清理心跳超时定时器
*/
clearHeartbeatTimeout() {
if (this.heartbeatTimeoutTimer) {
clearTimeout(this.heartbeatTimeoutTimer);
this.heartbeatTimeoutTimer = null;
}
}
/**
* 更新性能指标
* @returns {void}
* @description 计算并更新性能指标,触发回调
*/
updatePerformanceMetrics() {
if (!this.options.enablePerformanceMonitor) {
return;
}
// 计算连接持续时间
if (this.performanceMetrics.connectionStartTime) {
this.performanceMetrics.connectionUptime = Date.now() - this.performanceMetrics.connectionStartTime;
}
// 计算平均消息延迟
let avgLatency = 0;
if (this.performanceMetrics.messageLatencies.length > 0) {
const sum = this.performanceMetrics.messageLatencies.reduce((a, b) => a + b, 0);
avgLatency = sum / this.performanceMetrics.messageLatencies.length;
}
// 触发性能更新回调
if (this.options.onPerformanceUpdate) {
this.options.onPerformanceUpdate({
...this.performanceMetrics,
avgMessageLatency: avgLatency,
});
}
}
/**
* 获取性能指标
* @returns {Object} 性能指标对象
* @description 获取当前性能监控数据
*/
getPerformanceMetrics() {
// 计算平均消息延迟
let avgLatency = 0;
if (this.performanceMetrics.messageLatencies.length > 0) {
const sum = this.performanceMetrics.messageLatencies.reduce((a, b) => a + b, 0);
avgLatency = sum / this.performanceMetrics.messageLatencies.length;
}
return {
...this.performanceMetrics,
avgMessageLatency: avgLatency,
messageQueueSize: this.messageQueue.length,
reconnectAttempts: this.reconnectAttempts,
currentReconnectDelay: this.currentReconnectDelay,
};
}
/**
* 清理资源
* @returns {void}
* @description 清理所有定时器和 WebSocket 连接,重置连接状态。通常在页面卸载时调用
*/
cleanup() {
// 清除连接超时定时器
this.clearConnectTimeout();
// 清除心跳定时器
this.clearHeartbeat();
// 清除重连定时器
if (this.reconnectTimer) {
clearTimeout(this.reconnectTimer);
this.reconnectTimer = null;
}
// 关闭 WebSocket 连接
if (this.ws) {
uni.closeSocket();
this.ws = null;
}
// 重置连接状态
this.isConnected = false;
this.setConnectionStatus('disconnected');
// 清空消息队列
this.messageQueue = [];
}
/**
* 更新用户信息
* @param {Number|String} userId - 用户ID
* @param {String} nickname - 用户昵称
* @param {Boolean} isAdmin - 是否为管理员
* @returns {void}
* @description 更新用户信息,用于用户信息变更时同步更新 WebSocket 管理器中的用户信息
*/
updateUserInfo(userId, nickname, isAdmin) {
this.userId = userId;
this.nickname = nickname || '';
this.isAdmin = isAdmin || false;
}
}
export default WebSocketManager;
/utils/websocketService.js 文件内容
javascript
import Storage from '@/utils/storage.js';
import { BASE_URL } from '@/utils/config.js';
import WebSocketManager from '@/utils/websocket.js';
/**
* 全局 WebSocket 服务
* 统一管理 WebSocket 连接,避免多个页面重复创建连接
* 使用事件系统分发消息到各个页面
*/
class WebSocketService {
constructor() {
this.wsManager = null;
this.listeners = new Map(); // 存储各个页面的消息监听器
this.statusListeners = new Map(); // 存储连接状态监听器
this.isInitialized = false;
this.currentUserId = null;
this.currentUserInfo = null;
// 消息缓存:缓存重要消息,供后注册的页面获取
this.messageCache = new Map(); // key: 消息类型, value: { data, timestamp }
this.cacheMaxAge = 30000; // 缓存最大存活时间(30秒)
this.cacheableTypes = ['login_success', 'history', 'users_list']; // 需要缓存的消息类型
}
/**
* 初始化 WebSocket 服务
* @param {Object} userInfo - 用户信息
* @param {Object} options - 配置选项
*/
init(userInfo, options = {}) {
if (this.isInitialized && this.currentUserId === (userInfo.uid || userInfo.id || userInfo.user_id)) {
// 如果已经初始化且是同一个用户,直接返回
return;
}
// 如果已有连接,先清理
if (this.wsManager) {
this.wsManager.cleanup();
this.wsManager = null;
}
this.currentUserId = userInfo.uid || userInfo.id || userInfo.user_id;
this.currentUserInfo = userInfo;
if (!this.currentUserId) {
console.warn('[WebSocketService] 用户ID为空,无法初始化');
return;
}
const isAdmin = userInfo.identity === 2 || userInfo.is_admin || userInfo.isAdmin || false;
// 创建 WebSocket 管理器
this.wsManager = new WebSocketManager({
userId: this.currentUserId,
nickname: userInfo.nickname || userInfo.name || '',
isAdmin: isAdmin,
buildAuthData: (userInfo) => {
// 默认认证数据,各个页面可以自定义
return {
type: 'login',
user_id: userInfo.userId,
nickname: userInfo.nickname,
is_admin: userInfo.isAdmin,
};
},
onMessage: (data, manager) => {
this.handleMessage(data);
},
onStatusChange: (status) => {
this.handleStatusChange(status);
},
onConnected: () => {
this.handleConnected();
},
...options,
});
this.isInitialized = true;
}
/**
* 连接 WebSocket
*/
connect() {
if (!this.wsManager) {
console.warn('[WebSocketService] WebSocket 未初始化');
return;
}
if (!this.wsManager.isConnected) {
this.wsManager.connect();
}
}
/**
* 断开连接
*/
disconnect() {
if (this.wsManager) {
this.wsManager.cleanup();
this.wsManager = null;
}
this.isInitialized = false;
this.currentUserId = null;
this.currentUserInfo = null;
this.listeners.clear();
this.statusListeners.clear();
// 清除消息缓存
this.messageCache.clear();
}
/**
* 发送消息
* @param {Object} data - 消息数据
* @param {Boolean} urgent - 是否紧急
*/
sendMessage(data, urgent = false) {
if (!this.wsManager) {
console.warn('[WebSocketService] WebSocket 未初始化');
return false;
}
return this.wsManager.sendSocketMessage(data, urgent);
}
/**
* 注册消息监听器
* @param {String} pageId - 页面唯一标识
* @param {Function} callback - 消息处理回调
* @param {Boolean} receiveCached - 是否接收缓存的消息,默认true
*/
onMessage(pageId, callback, receiveCached = true) {
if (typeof callback !== 'function') {
console.error('[WebSocketService] 回调函数必须是函数类型');
return;
}
this.listeners.set(pageId, callback);
// 如果连接已建立,且允许接收缓存消息,立即分发缓存的重要消息
if (receiveCached && this.isConnected()) {
this.distributeCachedMessages(pageId, callback);
}
}
/**
* 分发缓存的消息给新注册的监听器
* @param {String} pageId - 页面标识
* @param {Function} callback - 回调函数
*/
distributeCachedMessages(pageId, callback) {
const now = Date.now();
this.messageCache.forEach((cached, type) => {
// 检查缓存是否过期
if (now - cached.timestamp > this.cacheMaxAge) {
this.messageCache.delete(type);
return;
}
// 分发缓存的消息
try {
callback(cached.data, this.wsManager);
} catch (error) {
console.error(`[WebSocketService] 分发缓存消息失败 (${type}):`, error);
}
});
}
/**
* 取消消息监听器
* @param {String} pageId - 页面唯一标识
*/
offMessage(pageId) {
this.listeners.delete(pageId);
}
/**
* 注册连接状态监听器
* @param {String} pageId - 页面唯一标识
* @param {Function} callback - 状态变化回调
*/
onStatusChange(pageId, callback) {
if (typeof callback !== 'function') {
console.error('[WebSocketService] 回调函数必须是函数类型');
return;
}
this.statusListeners.set(pageId, callback);
// 立即返回当前状态
if (this.wsManager) {
callback(this.wsManager.connectionStatus);
}
}
/**
* 取消连接状态监听器
* @param {String} pageId - 页面唯一标识
*/
offStatusChange(pageId) {
this.statusListeners.delete(pageId);
}
/**
* 处理接收到的消息
* @param {Object} data - 消息数据
*/
handleMessage(data) {
// 缓存重要消息(如果还没有监听器,或者需要缓存)
if (data && data.type && this.cacheableTypes.includes(data.type)) {
this.cacheMessage(data.type, data);
}
// 广播消息到所有监听器
this.listeners.forEach((callback, pageId) => {
try {
callback(data, this.wsManager);
} catch (error) {
console.error(`[WebSocketService] 页面 ${pageId} 消息处理失败:`, error);
}
});
}
/**
* 缓存消息
* @param {String} type - 消息类型
* @param {Object} data - 消息数据
*/
cacheMessage(type, data) {
// 只缓存重要消息类型
if (!this.cacheableTypes.includes(type)) {
return;
}
// 更新或添加缓存
this.messageCache.set(type, {
data: JSON.parse(JSON.stringify(data)), // 深拷贝,避免引用问题
timestamp: Date.now()
});
// 清理过期缓存
this.cleanExpiredCache();
}
/**
* 清理过期的缓存
*/
cleanExpiredCache() {
const now = Date.now();
const expiredTypes = [];
this.messageCache.forEach((cached, type) => {
if (now - cached.timestamp > this.cacheMaxAge) {
expiredTypes.push(type);
}
});
expiredTypes.forEach(type => {
this.messageCache.delete(type);
});
}
/**
* 获取缓存的消息
* @param {String} type - 消息类型,如果为空则返回所有缓存
* @returns {Object|Map} 缓存的消息
*/
getCachedMessage(type) {
if (type) {
const cached = this.messageCache.get(type);
if (cached && Date.now() - cached.timestamp <= this.cacheMaxAge) {
return cached.data;
}
return null;
}
return this.messageCache;
}
/**
* 清除缓存
* @param {String} type - 消息类型,如果为空则清除所有缓存
*/
clearCache(type) {
if (type) {
this.messageCache.delete(type);
} else {
this.messageCache.clear();
}
}
/**
* 处理连接状态变化
* @param {String} status - 连接状态
*/
handleStatusChange(status) {
// 广播状态变化到所有监听器
this.statusListeners.forEach((callback, pageId) => {
try {
callback(status);
} catch (error) {
console.error(`[WebSocketService] 页面 ${pageId} 状态处理失败:`, error);
}
});
}
/**
* 处理连接成功
*/
handleConnected() {
// 可以在这里处理连接成功后的逻辑
}
/**
* 获取连接状态
* @returns {String} 连接状态
*/
getStatus() {
return this.wsManager ? this.wsManager.connectionStatus : 'disconnected';
}
/**
* 是否已连接
* @returns {Boolean}
*/
isConnected() {
return this.wsManager ? this.wsManager.isConnected : false;
}
/**
* 更新用户信息
* @param {Object} userInfo - 用户信息
*/
updateUserInfo(userInfo) {
if (this.wsManager) {
const userId = userInfo.uid || userInfo.id || userInfo.user_id;
const nickname = userInfo.nickname || userInfo.name || '';
const isAdmin = userInfo.identity === 2 || userInfo.is_admin || userInfo.isAdmin || false;
this.wsManager.updateUserInfo(userId, nickname, isAdmin);
}
this.currentUserInfo = userInfo;
}
/**
* 设置自定义认证数据构建函数
* @param {Function} buildAuthData - 认证数据构建函数
*/
setBuildAuthData(buildAuthData) {
if (this.wsManager && typeof buildAuthData === 'function') {
this.wsManager.options.buildAuthData = buildAuthData;
}
}
}
// 创建单例
const websocketService = new WebSocketService();
export default websocketService;
App.vue 页面
javascript
import websocketService from "@/utils/websocketService.js";
// 初始化或更新全局 WebSocket 服务
websocketService.init(userInfo);
// 连接 WebSocket(延迟连接,避免影响页面加载)
setTimeout(() => {
websocketService.connect();
}, 1000);