小程序WebSocket实时通信全解析

WebSocket 功能说明文档

目录


功能概述

WebSocket 功能为小程序提供实时通信能力,支持以下功能:

  1. 聊天室功能:多人在线实时聊天,支持文本和图片消息
  2. 私信功能:用户之间的点对点私信聊天
  3. 自动重连:网络断开时自动重连,支持指数退避策略
  4. 心跳机制:定期发送心跳包,保持连接活跃
  5. 消息队列:连接断开时缓存消息,连接恢复后自动发送
  6. 消息缓存:缓存重要消息,供后注册的页面获取
  7. 性能监控:实时监控连接性能指标

架构设计

三层架构

核心层 (WebSocketManager)
服务层 (WebSocketService)
页面层 (Vue Components)
chatroom.vue

聊天室页面
myMessageDetail.vue

私信详情页
myMessage.vue

消息列表页
统一连接管理

单例模式
消息分发

观察者模式
消息缓存

30秒有效期
连接管理
自动重连

指数退避
心跳机制
消息队列

设计特点

  1. 单例模式:全局只有一个 WebSocket 连接,避免资源浪费
  2. 事件分发:使用观察者模式,将消息分发到各个页面
  3. 消息缓存:重要消息自动缓存,新页面注册时可立即获取
  4. 自动重连:支持指数退避重连策略,避免频繁重连
  5. 容错机制:心跳容错、消息队列、连接超时等

核心组件

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: 状态监听器 Map
  • messageCache: 消息缓存 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_successhistoryusers_list)会自动缓存 30 秒。新页面注册时,如果连接已建立,会自动分发缓存的消息。

4. 认证数据自定义

不同页面可能需要不同的认证数据:

  • 聊天室 :需要 is_admin 字段
  • 私信 :需要 nickname 字段

使用 setBuildAuthData() 设置自定义认证函数。

5. 连接状态检查

发送消息前,建议检查连接状态。

6. 自动重连

WebSocket 支持自动重连,使用指数退避策略:

  • 初始延迟:3 秒
  • 最大延迟:30 秒
  • 重连次数:无限(默认)

7. 心跳机制

心跳机制用于保持连接活跃:

  • 心跳间隔:60 秒(默认)
  • 心跳超时:30 秒(默认)
  • 容错模式:开启(默认)

如果服务器不支持心跳响应,容错模式会允许一定次数的心跳超时,不会立即断开连接。

8. 消息队列

连接断开时,消息会自动进入队列。连接恢复后,队列中的消息会自动发送。队列最大长度为 100,超出限制会移除最旧的消息。

9. 性能监控

性能监控默认开启,可以获取以下指标:

  • 连接耗时
  • 重连次数
  • 发送/接收消息数
  • 消息延迟
  • 连接持续时间

10. 错误处理

建议在消息处理函数中添加错误处理。


常见问题

Q: 为什么页面注册后收不到消息?

A: 检查以下几点:

  1. 是否在 onUnload 中取消了监听
  2. pageId 是否唯一
  3. 连接是否已建立(websocketService.isConnected()
  4. 消息类型是否正确

Q: 如何获取历史消息?

A: 有两种方式:

  1. login_success 消息中可能包含历史消息
  2. 连接成功后,发送 { type: 'get_history' } 请求

Q: 如何判断连接状态?

A: 使用 websocketService.isConnected() 或注册状态监听。

Q: 消息发送失败怎么办?

A: 检查以下几点:

  1. 连接是否已建立
  2. 消息格式是否正确
  3. 查看控制台错误信息

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);

小程序WebSocket实时通信vue代码

相关推荐
代码游侠2 小时前
学习笔记——TCP 传输控制协议
linux·网络·笔记·网络协议·学习·tcp/ip
风月歌2 小时前
基于小程序的超市购物系统设计与实现源码(java+小程序+mysql+vue+文档)
java·mysql·微信小程序·小程序·毕业设计·源码
小离a_a2 小时前
uniapp微信小程序实现拍照加水印,水印上添加当前时间,当前地点等信息,地点逆解析使用的是高德地图
微信小程序·小程序·uni-app
鲁Q同志2 小时前
微信小程序树形选择组件
微信小程序·小程序
2501_921649492 小时前
股票 API 对接, 接入德国法兰克福交易所(FWB/Xetra)实现量化分析
后端·python·websocket·金融·区块链
huangql5202 小时前
HTTP超文本传输协议:互联网的统一语言
网络·网络协议·http
小疆智控2 小时前
低延迟高同步!Profinet转EtherCAT,蒸汽转化装置技术突破新标杆
网络·网络协议
_F_y3 小时前
应用层协议HTTP
网络·网络协议·http
lkbhua莱克瓦243 小时前
TCP通信练习1——多发多收
java·开发语言·网络·网络协议·tcp/ip·tcp练习