# 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!
← tailwindcss git →