# uniapp socket wss

在 UniApp 中实现登录后建立连接、发送聊天消息、实时位置更新、心跳检测等功能,使用 WebSocket 是合适的方案。为了保证客户端与服务端能够有效通信,建议你自定义统一的消息格式规范,下面是一套比较常见且实用的设计方案:


# 🔧 一、统一消息格式结构

每条消息都应该是一个 JSON 字符串,结构如下:

{
  "wss_type": "string",       // 消息类型,例如 login、chat、location、ping 等
  "wss_data": { ... },        // 具体数据内容,结构根据 wss_type 而定
  "clientId": "string",   // 客户端唯一标识(可选)
  "timestamp": 1234567890 // 发送时间戳(可选)
}
✅ Copy success!

你在代码里使用时一般是对象形式:

let message = {
  wss_type: 'chat', 
  wss_data: {
    from: 'user123',
    to: 'user456',
    content: '你好',
  },
  clientId: 'user123',
  timestamp: Date.now()
}

socket.send(JSON.stringify(message))
✅ Copy success!

# 📚 二、建议的 wss_type 类型枚举

wss_type 说明 data 示例
login 登录后建立连接 { token: 'abc123' }
chat 聊天消息 { from: 'id1', to: 'id2', content: '你好' }
location 实时位置更新 { lat: 39.123, lng: 117.456, speed: 10 }
ping 心跳检测 {}
pong 心跳回应(可选) {}
ack 消息接收回执(可选) { msgId: 'uuid' }
error 错误反馈(可选) { code: 401, message: 'token失效' }
sys 系统消息通知 { content: '欢迎登录' }

# 🧩 三、客户端心跳设计

你可以每隔一段时间(如30秒)向服务器发送一条 ping 消息:

setInterval(() => {
  socket.send(JSON.stringify({
    wss_type: 'ping',
    wss_data: {},
    clientId: userId,
    timestamp: Date.now()
  }))
}, 30000)
✅ Copy success!

服务端可以回一条 pong 或者直接以此判断客户端在线。


# 🗺️ 四、位置更新设计

位置变化时,发送 location 类型消息:

socket.send(JSON.stringify({
  wss_type: 'location',
  wss_data: {
    lat: 39.123,
    lng: 117.456,
    speed: 5.5
  },
  clientId: userId,
  timestamp: Date.now()
}))
✅ Copy success!

你可以设置一个定时器或监听 watchPosition 来触发。


# 🧪 五、服务端约定建议

  • 服务端返回的数据也建议使用相同格式
  • 可以增加 msgId 字段标记每条消息
  • 消息结构里最好加版本号 version(预留字段)
  • 加密传输敏感数据(如使用 HTTPS + WSS)

# ✅ 小结:推荐结构

{
  wss_type: 'chat' | 'login' | 'location' | 'ping' | 'ack' | 'error' | ...,
  wss_data: {},             // 根据 type 不同而变化
  clientId: 'xxx',      // 客户端唯一 ID(可选)
  timestamp: 1234567890,
  msgId: 'uuid-xxxxxx'  // 消息唯一 ID(可选)
}
✅ Copy success!

=============================================================

1.心跳检测(3秒 5秒  3000+1*2000 )达到60秒 反复执行。
2.断线重连 防止重复重连,增加锁的机制
3.每次打开app.vue show方法时 自动检测后台最后一次推送的时间,如果超过指定时间的*2倍 ,则强制重连【假连接时】
4.每个tabbar也增加防止掉线的检测
 this.$nextTick(()=>{
			this.$common.commonCheckWssStatusRe(this.$wsService);
		})

      let params = {
					cmd: "login",
					data: {
						token: result.data.token
					}
				}
				
				// ✅ 检查 WebSocket 是否已初始化
				if (this.$wsService) {
					if (!this.$wsService.socket) {
						this.$wsService.connect()
					} else {
						let socket = this.$wsService
						socket?.send(JSON.stringify(params))
					}
				} else {
					console.warn('⚠️ WebSocket 服务未初始化')
				}
✅ Copy success!

// 在 Vue 实例上添加全局 WebSocket 服务属性 app.config.globalProperties.$wsService = socket;

uni.$wsService = socket

App.vue


      onLaunch: async function() {
        // 启动全局调度器(只会启动一次)
				if (this.$wsService) {
					scheduler.setWsService(this.$wsService)
				}
				scheduler.start()
      },
      onShow: function() {
				console.log('[APP]__[App.vue]___________________________onShow')
				
				//	[全局定时器]==============================================================start
				// 检测定时器是否失效并自动恢复,确保还在运行
				if (this.$wsService) {
					scheduler.setWsService(this.$wsService)
				}

✅ Copy success!

common.js


/**
	 * 检查是否超时  超时的话重连
	 */
	static commonCheckWssStatusRe(ws) {
		// console.log('检查是否超时  超时的话重连____________________________start')
		try {
			
			const token = uni.getStorageSync('token')
			if (!token) {
				return
			}

			const now = Date.now()
			//	================================
			// ✅ 防抖:5秒内只执行一次
			const lastCheck = uni.getStorageSync('wss_last_check_time') || 0
			if (now - lastCheck < 6000) {
				console.log('⏱️ 检测过于频繁,跳过')
				return
			}
			uni.setStorageSync('wss_last_check_time', now)
			//	================================

			const lastTime = uni.getStorageSync('wss_msg_last_time_int')

			if (!lastTime) {
				// console.log('⚠️ 没有心跳记录,跳过检测')
				return
			}

			const diff = now - lastTime

			// ⚠️ 阈值:建议 2倍心跳时间(你是40秒 → 80秒)
			const MAX_TIMEOUT = 80000

			console.log(`🧪 WS检测:间隔 ${diff} ms`)

			// 获取全局 websocket
			if (!ws) {
				console.log('⚠️ ws实例不存在')
				return
			}
			
			// console.log('检查是否超时  超时的话重连____________________________v1')

			// 🚨 超时:强制重连
			if (diff > MAX_TIMEOUT) {
				console.log('💥 超过80秒无消息,强制重连')
				
				// ⚠️ 关键:先关闭再连(避免脏连接)
				ws.close()

				setTimeout(() => {
					ws.connect()
				}, 600)

				return
			}

		} catch (e) {
			console.error('❌ 检测异常', e)
		}

	}
}

✅ Copy success!

      methods: {
				/**
				 * 检查是否超时  超时的话重连
				 */
				commonCheckWssStatusRe(ws) {
					console.log('v2检查是否超时  超时的话重连____________________________start')
					try {
						
						const token = uni.getStorageSync('token')
						if (!token) {
							return
						}
				
						const now = Date.now()
						//	================================
						// ✅ 防抖:5秒内只执行一次
						// const lastCheck = uni.getStorageSync('wss_last_check_time') || 0
						// if (now - lastCheck < 6000) {
							// console.log('⏱️ 检测过于频繁,跳过')
							// return
						// }
						uni.setStorageSync('wss_last_check_time', now)
						uni.setStorageSync('app_show_run_time', now)
						//	================================
				
						const lastTime = uni.getStorageSync('wss_msg_last_time_int')
				
						if (!lastTime) {
							console.log('v2⚠️ 没有心跳记录,跳过检测')
							return
						}
				
						const diff = now - lastTime
				
						// ⚠️ 阈值:建议 2倍心跳时间(你是40秒 → 80秒)
						const MAX_TIMEOUT = 80000
				
						console.log(`v2🧪 WS检测:间隔 ${diff} ms`)
				
						// 获取全局 websocket
						if (!ws) {
							console.log('v2⚠️ ws实例不存在')
							return
						}
						
						console.log('v2检查是否超时  超时的话重连____________________________v1')
				
						// 🚨 超时:强制重连
						if (diff > MAX_TIMEOUT) {
							console.log('v2💥 超过80秒无消息,强制重连')
							
							// ⚠️ 关键:先关闭再连(避免脏连接)
							ws.close()
							
							setTimeout(() => {
								ws.connect()
							}, 700)
				
							return
						}
				
					} catch (e) {
						console.error('❌ 检测异常', e)
					}
				
				}
			}

✅ Copy success!

websocket.js


class websocketUtil {
	constructor(url, heartbeatInterval = 40000) {
		this.url = url
		this.socket = null;
		
		// 心跳
		this.heartbeatInterval = heartbeatInterval ? heartbeatInterval : 40000; // 心跳间隔
		this.heartbeatTimer = null
		this.lastPongTime = null;   // 最近一次收到服务端 pong 的时间
		
		// 状态控制(核心)
		this.isConnected = false      // 是否已连接
		this.isConnecting = false     // 是否连接中(防重复连接)
		this.lockReconnect = false    // 重连锁
		// this.isConnect = false; //连接标识 避免重复连接
		
		// 重连控制
		this.reconnectTimer = null
		this.reconnectCount = 0
		
		// 事件
		this.eventCallbacks = {}; // 事件回调
		
		// 登录控制
		this.isLogin = false;		//	false=不需要登录  true=需要send登录信息

		
		this.heartbeatTimeout = null; // 心跳超时
		this.againNum = 0 // 重连次数
		this.reconnectTimeOut = null //重连之后多久再次重连
		
		try {
			return this.connect()
		} catch (e) {
			console.log('catch');
			this.lockReconnect = false			//	false=可以重连
			this.isConnected = false			//	false= 非连接状态
			this.isConnecting = false			//	false=非连接中
			
			this.reconnect();
		}

	}
	
	saveErrorLog(params) {
		try {
			let list = uni.getStorageSync('err_log_list') || []
			if (!Array.isArray(list)) list = []
	
			const now = new Date()
			const add_time = now.toISOString().replace('T', ' ').slice(0, 19)
	
			list.unshift({
				...params,
				add_time
			})
	
			if (list.length > 30) {
				list = list.slice(0, 30)
			}
	
			uni.setStorageSync('err_log_list', list)
		} catch (e) {
			// console.error('错误日志写入失败', e)
		}
	}

	// 进入这个页面的时候创建websocket连接【整个页面随时使用】
	connect() {
		console.log('wss_connect__创建websocket连接______________________________________________start')
		
		console.log(this.socket)
		console.log('当前socket状态:', this.socket?.readyState)
		if (this.isConnecting || this.isConnected) {
			console.log('wss_connect__创建websocket连接__________________________⚠️ 已连接或连接中,跳过____________________stop')
			return
		}
		console.log('🚀 开始连接 WebSocket...')
		
		// 连接中
		this.isConnecting = true
		
		// if (this.socket && this.socket.readyState === 1) {
		// 	//	如果已经连接 并且状态 == 1
		// 	// 0 → 连接中
		// 	// 1 → 正常
		// 	// 2 → 正在关闭
		// 	// 3 → 已关闭
		// 	console.log('connect====connect1')
		// 	console.log(this.socket.readyState)
		// 	this.saveErrorLog({
		// 		'wss_lick_status':this.socket.readyState
		// 	})
		// 	console.log('connect====connect2')
			
		// 	console.log('wss_connect__创建websocket连接______________________________________________end')
		// 	return ;
		// }
		console.log('wss_connect__创建websocket连接______________________________________________end')

		this.socket = uni.connectSocket({
			url: this.url,
			success: () => {
				// console.log("正准备建立websocket中...");
				// 返回实例
				return this.socket
			},
		});
		
		// ✅ 连接成功
		this.socket.onOpen((res) => {
			console.log('✅ WebSocket连接成功')
			
			this.isConnected = true
			this.isConnecting = false
			this.lockReconnect = false
			this.reconnectCount = 0

			this.startHeartbeat(); // 连接打开后启动心跳
			
			// 重连方法清除
			// if (this.reconnectTimeOut) {
			// 	clearTimeout(this.reconnectTimeOut); // 重连方法清除
			// 	this.reconnectTimeOut = null
			// }
			// 如果需要登录
			if (this.isLogin) {
				this.sendLogin()
			}
		})
		
		// ✅ 收消息
		this.socket.onMessage((event) => {
			// console.log('WebSocket message received:', event.data);
			this.handleMessage(event.data);
		})
		
		// ❌ 关闭
		// 这里仅是事件监听【如果socket关闭了会执行】
		this.socket.onClose(() => {
			console.log('❌ WebSocket关闭___onClose')
			// this.isConnect = false
			this.isConnected = false
			this.isConnecting = false

			this.stopHeartbeat(); // 连接关闭后停止心跳
			this.reconnect();
		})
		
		// ❌ 错误
		this.socket.onError((err) => {
			console.log('❌ WebSocket错误', err)

			this.isConnected = false
			this.isConnecting = false

			this.reconnect()
		})
		
	}
	
	/* ======================== 核心:重连 ======================== */
	reconnect() {
		console.log('======================== 核心:重连 ========================start')
		
		if (this.lockReconnect) {
			console.log('======================== 核心:重连 ================锁定中========lockReconnect='+this.lockReconnect)
			return
		}
		
		//	
		this.lockReconnect = true
		
		// 指数退避(最大60秒)
		let delay = Math.min(3000 + this.reconnectCount * 2000, 60000)

		console.log(`🔁 第${this.reconnectCount + 1}次重连,${delay}ms后执行_`)
		
		this.reconnectTimer = setTimeout(() => {
			console.log('重连中。。。reconnectCount='+this.reconnectCount)
			this.connect()
			this.reconnectCount++
			this.lockReconnect = false
		}, delay)
	}
	
	/* ======================== 心跳 ======================== */
	startHeartbeat() {
		this.stopHeartbeat()
		
		this.lastPongTime = Date.now()
		
		this.heartbeatTimer = setInterval(() => {
			//	console.log('===心跳检测=send==heartbeatInterval='+this.heartbeatInterval)
			// 发送心跳
			this.send(JSON.stringify({
				cmd: 'heartbeat'
			}));
			
			// ⚠️ 如果超过2个周期没收到pong,强制重连 检测是否超时
			this.checkHeartbeat()
			//
			
		}, this.heartbeatInterval)
	}
	
	// 停止心跳
	stopHeartbeat() {
		if (this.heartbeatTimer) {
			clearInterval(this.heartbeatTimer);
			this.heartbeatTimer = null;
		}
	}
	
	/**
	 * ⚠️ 如果超过2个周期没收到pong,强制重连
	 */
	checkHeartbeat() {
	  let now = Date.now()
	  //	最近一次收到服务端 pong 的时间   && 当前时间减去 - 最后一次接口通的时间   强调这是「时间差」。
	  if (now - this.lastPongTime > this.heartbeatInterval * 2) {
		console.log('💔 心跳超时,关闭连接')
		
		
		this.socket && this.socket.close()
		// let token = uni.getStorageSync('token')
		
		// if (token) {
		// 	//	需要发送登录信息
		// 	this.isLogin = true
		// }
		
		// this.reconnect()
	  }
	}
	
	/* ======================== 收消息 处理接收到的消息======================== */
	handleMessage(message) {
		try {
			const data = this.parseMessage(message);
			if (!data) {
				return;
			}
			
			//	更新心跳时间
			this.lastPongTime = Date.now()
			
			if (this.isLogin == true) {
				this.isLogin = false
				//	需要登录
				this.sendLogin()
			}
			
			uni.setStorageSync('wss_msg_last_time_int',this.lastPongTime)
			uni.setStorageSync('wss_msg_last_time_msg',this.getNowTime())
	
			console.log('最后一次接收接口推送的事件:lastPongTime='+this.getNowTime())
			// uni.$u.toast('接收到ws推送的事件'+data.msg_type)
			
			// console.log(data, 'notify_chat')
			// 根据消息类型分发处理
			if (data.type && this.eventCallbacks[data.type]) {
				this.eventCallbacks[data.type].forEach(callback => callback(data.payload));
			} else {
				if (data.msg_type == 9 || data.msg_type == 11 || data.msg_type == 12) {
					uni.$emit('notifyChatMessage', data); // 消息监听
				} else {
					if (data.msg_type === 1) {
						// console.log('====wss心跳健康监测====start')
						// console.log(this.lastPongTime,'=lastPongTime')
						// console.log('====wss心跳健康监测====end')
					}
					
					this.emit('message', data); // 如果没有特定类型的回调,则触发通用消息事件
				}
	
			}
		} catch (error) {
			console.error('Error parsing WebSocket message:', error);
		}
	}
	
	parseMessage(message) {
		if (message == null) return null;
		
		if (typeof message === 'object') {
			return message;
		}
		
		if (typeof message !== 'string') {
			console.warn('Unsupported WebSocket message type:', typeof message);
			return null;
		}
		
		try {
			return JSON.parse(message);
		} catch (primaryError) {
			try {
				return JSON.parse(this.decodeUnicode(message));
			} catch (secondaryError) {
				console.error('Failed to parse message:', message);
				console.error('Primary parse error:', primaryError);
				console.error('Secondary parse error:', secondaryError);
				return null;
			}
		}
	}
	
	/* ======================== 发送消息======================== */
	// send(data) {
	// 	if (!this.socket || this.socket.readyState !== 1) {
	// 		console.log('⚠️ 未连接,触发重连')

	// 		this.reconnect()
	// 		return
	// 	}

	// 	this.socket.send({
	// 		data
	// 	})
	// }
	
	async send(value) {
		await new Promise((resolve) => {
			setTimeout(() => {
				resolve();
			}, 1000);
		});
		
		if (!this.socket || this.socket.readyState !== 1) {
			console.log('⚠️ 未连接,触发重连')

			this.reconnect()
			return
		}
		
		try {
			// 注:只有连接正常打开中,才能正常成功发送消息
			this.socket.send({
				data: value,
				async success() {
					// console.log("消息发送成功",value);
				},
			});
		} catch (errData) {
			uni.$u.toast('send发送失败')
			// this.isConnect = false
			// this.reconnect();
		}
		
	}
	
	/* ======================== 登录 ======================== */
	sendLogin() {
		// socket重连-----------------------
		let token = uni.getStorageSync('token')
		
		if (!token) return;
		
		let params = {
			cmd: "login",
			data: {
				token: token
			}
		}
		
		this.send(JSON.stringify(params))
	
	// 	if (!this.socket) {
	// 		// uni.$u.toast('断开链接001')
	// 		// this.connect()
	// 	} else {
	// 		// uni.$u.toast('发送login参数')
	// 		this.socket.send(JSON.stringify(params))
	// 	}
		
	// 	this.isLogin = false
		
		// setTimeout(function() {
		// 	uni.$u.toast('关闭:isLogin')
		// },2000)
		// ------------------------------------------
	}
	
	/* ======================== 监听事件 ======================== */
	on(event, callback) {
		if (!this.eventCallbacks[event]) {
			this.eventCallbacks[event] = [];
		}
		this.eventCallbacks[event].push(callback);
	}
	
	/* ======================== 触发事件 ======================== */
	emit(event, ...args) {
		if (this.eventCallbacks[event]) {
			this.eventCallbacks[event].forEach(callback => callback(...args));
		}
	}
	
	// 连接销毁
	close() {
		console.log('🛑 手动关闭连接')
		
		this.lockReconnect = true

		if (this.reconnectTimer) {
			clearTimeout(this.reconnectTimer)
			this.reconnectTimer = null
		}

		this.stopHeartbeat()

		if (this.socket) {
			this.socket.close()
			this.socket = null
		}

		this.isConnected = false
		this.isConnecting = false
		
		
		// this.isConnect = true
		// if (this.socket) {
		// 	uni.closeSocket();
		// 	this.socket = null;
		// 	this.stopHeartbeat();
		// }
	}
	
	
	
	
	
	
	
	//	===============================================================================================
	
	
	
	
	getNowTime() {
	    const now = new Date();
	    
	    const year = now.getFullYear();
	    const month = String(now.getMonth() + 1).padStart(2, '0');
	    const day = String(now.getDate()).padStart(2, '0');
	    const hours = String(now.getHours()).padStart(2, '0');
	    const minutes = String(now.getMinutes()).padStart(2, '0');
	    const seconds = String(now.getSeconds()).padStart(2, '0');
	    
	    return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
	}

	// 解码
	decodeUnicode(str) {
		str = str.replace(/\\/g, "%");
		//转换中文
		str = unescape(str);
		//将其他受影响的转换回原来
		str = str.replace(/%/g, "\\");
		//对网址的链接进行处理
		str = str.replace(/\\/g, "");
		return str;
	}

	
	
}

export default websocketUtil


✅ Copy success!