feat(sdk): 升级HaveADrink SDK并集成邀请系统
- 将HaveADrink依赖从1.0.8升级至1.0.12版本 - 集成邀请码功能,新增common/invite.js处理邀请链路 - 实现发送好友请求返回邀请码的新流程 - 添加删除动态功能,新增DeleteFeed接口 - 集成UniAppWebSocket适配器,重构WebSocket连接管理 - 优化好友请求支持关键词搜索功能 - 在FeedCard组件中添加删除按钮和分享按钮 - 更新SDK类型定义文件以匹配新接口规范
This commit is contained in:
+16
-8
@@ -2,11 +2,12 @@
|
||||
* 基于 HaveADrink SDK 封装后端接口
|
||||
*/
|
||||
import HaveADrink from 'HaveADrink'
|
||||
import UniAppWebSocket from './uni-websocket'
|
||||
|
||||
// ==========================================
|
||||
// 后端服务地址(切换环境只需改这里)
|
||||
// ==========================================
|
||||
const API_HOST = 'https://dev.wash-painting.cn'
|
||||
export const API_HOST = 'https://dev.wash-painting.cn'
|
||||
|
||||
// ==========================================
|
||||
// HTTP 请求函数(供 SDK 内部调用)
|
||||
@@ -113,6 +114,7 @@ const client = new HaveADrink({
|
||||
host: API_HOST,
|
||||
http_request: httpRequest,
|
||||
upload: uploadRequest,
|
||||
websocket: UniAppWebSocket,
|
||||
token: uni.getStorageSync('auth_token') || ''
|
||||
})
|
||||
|
||||
@@ -186,6 +188,12 @@ export async function UnlikeFeed({ feedId }) {
|
||||
return { data: res }
|
||||
}
|
||||
|
||||
/** 删除动态(仅本人可删,后端会校验归属) */
|
||||
export async function DeleteFeed({ feedId }) {
|
||||
const res = await client.DeleteFeed({ id: feedId })
|
||||
return { data: res }
|
||||
}
|
||||
|
||||
/** 获取动态评论 */
|
||||
export async function GetFeedComments({ feedId }) {
|
||||
const res = await client.GetFeedComments({ id: feedId })
|
||||
@@ -210,19 +218,19 @@ export async function GetFriends({ keyword = '' } = {}) {
|
||||
return { data: res }
|
||||
}
|
||||
|
||||
/** 获取好友请求 */
|
||||
export async function GetFriendRequests() {
|
||||
const res = await client.GetFriendRequests({})
|
||||
/** 获取好友请求(支持关键词搜索) */
|
||||
export async function GetFriendRequests({ keyword = '' } = {}) {
|
||||
const res = await client.GetFriendRequests({ keyword })
|
||||
return { data: res }
|
||||
}
|
||||
|
||||
/** 发送好友请求 */
|
||||
export async function SendFriendRequest({ userId, message = '' }) {
|
||||
const res = await client.SendFriendRequest({ userId, message })
|
||||
/** 发送好友请求(创建邀请,SDK v1.0.12:无需 userId,响应返回 inviteCode 邀请码) */
|
||||
export async function SendFriendRequest({ message = '' } = {}) {
|
||||
const res = await client.SendFriendRequest({ message })
|
||||
return { data: res }
|
||||
}
|
||||
|
||||
/** 接受好友请求 */
|
||||
/** 接受好友请求(id 为好友请求ID,即邀请码 inviteCode) */
|
||||
export async function AcceptFriendRequest({ requestId }) {
|
||||
const res = await client.AcceptFriendRequest({ id: requestId })
|
||||
return { data: res }
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
/* 碰盏日记 - 邀请链路处理(邀请码模式,SDK v1.0.12)
|
||||
*
|
||||
* 后端规则:每次邀请都生成新的邀请码(一次性,不复用)
|
||||
*
|
||||
* 新流程:
|
||||
* 1. 邀请人调用 SendFriendRequest({ message }) 创建邀请 → 后端返回新的 inviteCode(好友请求ID)
|
||||
* 2. 分享链接携带 inviteCode、inviterNick;每次分享后重新生成新码供下次使用
|
||||
* 3. 被邀请人打开小程序 → index.vue / friends.vue onLoad 收到 inviteCode → savePendingInvite 暂存
|
||||
* 4. 被邀请人登录后调用 AcceptFriendRequest({ id: inviteCode }) 接受邀请,双方成为酒友
|
||||
*/
|
||||
|
||||
import { SendFriendRequest, AcceptFriendRequest } from './api'
|
||||
|
||||
const INVITE_KEY = 'pending_invite' // 我收到的邀请(待接受)
|
||||
|
||||
// 内存记录最近一次自己生成的邀请码(仅用于过滤自己点开自己的分享)
|
||||
let myLastCode = ''
|
||||
|
||||
// 清理旧版本遗留的永久缓存键(旧方案曾永久缓存邀请码,新方案每次生成新码)
|
||||
try { uni.removeStorageSync('my_invite_code') } catch (e) { /* ignore */ }
|
||||
|
||||
/**
|
||||
* 生成新的邀请码(用于分享)
|
||||
* 后端每次邀请都生成新码,故不做持久化缓存,每次调用都创建新邀请
|
||||
* @returns {Promise<string>} 邀请码,未登录或失败返回空字符串
|
||||
*/
|
||||
export async function getMyInviteCode() {
|
||||
if (uni.getStorageSync('is_logged_in') !== 'true') return ''
|
||||
try {
|
||||
const res = await SendFriendRequest({ message: '邀请你成为酒友,一起记录饮酒生活 🍻' })
|
||||
const code = res.data && res.data.inviteCode
|
||||
if (code !== undefined && code !== null && code !== '') {
|
||||
myLastCode = String(code)
|
||||
return myLastCode
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('[invite] 创建邀请码失败', e)
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
/**
|
||||
* 暂存收到的邀请(由分享落地页 onLoad 调用)
|
||||
* @param {string|number} inviteCode - 邀请码(好友请求ID)
|
||||
* @param {string} inviterNick - 邀请人昵称(用于提示文案)
|
||||
*/
|
||||
export function savePendingInvite(inviteCode, inviterNick) {
|
||||
if (!inviteCode) return
|
||||
try {
|
||||
// 自己分享给自己的邀请码,不处理(同一会话内自己点开自己的分享卡片)
|
||||
if (myLastCode && String(myLastCode) === String(inviteCode)) return
|
||||
uni.setStorageSync(INVITE_KEY, JSON.stringify({
|
||||
inviteCode: String(inviteCode),
|
||||
// uni-app onLoad options 已自动解码,此处直接存储
|
||||
inviterNick: inviterNick || ''
|
||||
}))
|
||||
} catch (e) { /* ignore */ }
|
||||
}
|
||||
|
||||
/**
|
||||
* 接受暂存的邀请(已登录时调用)
|
||||
* 用邀请码调用 AcceptFriendRequest,成功后清除暂存
|
||||
* @returns {Promise<boolean>} 是否成功接受
|
||||
*/
|
||||
export async function handlePendingInvite() {
|
||||
// 未登录时不处理,等登录完成后再调用
|
||||
if (uni.getStorageSync('is_logged_in') !== 'true') return false
|
||||
let invite = null
|
||||
try {
|
||||
invite = JSON.parse(uni.getStorageSync(INVITE_KEY) || '')
|
||||
} catch (e) { /* ignore */ }
|
||||
if (!invite || !invite.inviteCode) return false
|
||||
|
||||
try {
|
||||
await AcceptFriendRequest({ requestId: invite.inviteCode })
|
||||
uni.removeStorageSync(INVITE_KEY)
|
||||
const nick = invite.inviterNick || '邀请人'
|
||||
uni.showToast({ title: `已接受邀请,和「${nick}」成为酒友 🍻`, icon: 'none', duration: 2500 })
|
||||
return true
|
||||
} catch (e) {
|
||||
// 后端可能返回"已是好友/邀请已失效"等业务失败,同样清除暂存避免重复提示
|
||||
uni.removeStorageSync(INVITE_KEY)
|
||||
console.warn('[invite] 接受邀请失败', e)
|
||||
return false
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,156 @@
|
||||
/* 碰盏日记 - UniAppWebSocket 适配器
|
||||
* 将 uni-app 的 uni.connectSocket 适配为 HaveADrink SDK WebsocketInterface
|
||||
* (标准 WebSocket 风格:constructor(host)、onopen/onmessage/onclose/onerror、send、close)
|
||||
* 用法:在 SDK 初始化时传入 websocket: UniAppWebSocket
|
||||
*/
|
||||
|
||||
export default class UniAppWebSocket {
|
||||
/**
|
||||
* @param {string} host - 完整 ws/wss 连接地址(SDK 已处理 https→wss 协议转换)
|
||||
*/
|
||||
constructor(host) {
|
||||
this.url = host
|
||||
// 可赋值的事件回调(SDK ChatWebSocketConn.connect() 中直接赋值)
|
||||
this.onopen = null
|
||||
this.onmessage = null
|
||||
this.onclose = null
|
||||
this.onerror = null
|
||||
// 连接状态: connecting | open | closed
|
||||
this.readyState = 0
|
||||
this._sendQueue = []
|
||||
this._closedByUser = false
|
||||
|
||||
this._connect()
|
||||
}
|
||||
|
||||
_connect() {
|
||||
// 微信小程序下直接用 wx.connectSocket:
|
||||
// uni 的 API 运行时(uni.api.esm.js)会向 connectSocket 注入 success/fail/complete 回调,
|
||||
// 而微信规则是“传入回调后不再返回 SocketTask”,导致 uni.connectSocket 永远拿不到 task。
|
||||
// wx.connectSocket 不传回调时正常返回 SocketTask,避免降级到全局 socket API
|
||||
// (全局 API 重连时会重复注册监听器,引发重连风暴)
|
||||
console.log('[UniAppWebSocket] 发起连接:', this.url)
|
||||
const rawConnect = (typeof wx !== 'undefined' && typeof wx.connectSocket === 'function')
|
||||
? wx.connectSocket.bind(wx)
|
||||
: uni.connectSocket
|
||||
const task = rawConnect({ url: this.url })
|
||||
|
||||
// 部分平台 uni.connectSocket 无返回值,降级为全局 API 监听
|
||||
if (task && typeof task.onOpen === 'function') {
|
||||
this.socketTask = task
|
||||
|
||||
task.onOpen((res) => {
|
||||
console.log('[UniAppWebSocket] 连接已打开(open)')
|
||||
this.readyState = 1
|
||||
this._flush()
|
||||
if (typeof this.onopen === 'function') {
|
||||
this.onopen({ type: 'open' })
|
||||
}
|
||||
})
|
||||
|
||||
task.onMessage((res) => {
|
||||
if (typeof this.onmessage === 'function') {
|
||||
// 与浏览器 MessageEvent 对齐:消息内容放在 event.data
|
||||
this.onmessage({ type: 'message', data: res.data })
|
||||
}
|
||||
})
|
||||
|
||||
task.onClose((event) => {
|
||||
// 诊断:打印关闭码/原因,用于判断是服务端拒连还是网络问题
|
||||
// 常见 code:1006 异常断开、1000 正常关闭;reason 由服务端给出
|
||||
console.warn('[UniAppWebSocket] 连接被关闭(close) code:', event && event.code,
|
||||
'reason:', event && event.reason)
|
||||
this.readyState = 3
|
||||
if (!this._closedByUser && typeof this.onclose === 'function') {
|
||||
this.onclose(Object.assign({ type: 'close' }, event || {}))
|
||||
}
|
||||
})
|
||||
|
||||
task.onError((err) => {
|
||||
// 诊断:微信会在 errMsg 中给出具体原因(域名不在合法列表、TLS 失败等)
|
||||
console.warn('[UniAppWebSocket] 连接错误(error):', err && (err.errMsg || err.message || JSON.stringify(err)))
|
||||
if (typeof this.onerror === 'function') {
|
||||
this.onerror(Object.assign({ type: 'error' }, err || {}))
|
||||
}
|
||||
})
|
||||
} else {
|
||||
console.warn('[UniAppWebSocket] 未获得 SocketTask,降级到全局 socket API')
|
||||
// 兜底:旧版小程序全局 socket API
|
||||
uni.onSocketOpen(() => {
|
||||
this.readyState = 1
|
||||
this._flush()
|
||||
if (typeof this.onopen === 'function') {
|
||||
this.onopen({ type: 'open' })
|
||||
}
|
||||
})
|
||||
uni.onSocketMessage((res) => {
|
||||
if (typeof this.onmessage === 'function') {
|
||||
this.onmessage({ type: 'message', data: res.data })
|
||||
}
|
||||
})
|
||||
uni.onSocketClose((event) => {
|
||||
this.readyState = 3
|
||||
if (!this._closedByUser && typeof this.onclose === 'function') {
|
||||
this.onclose(Object.assign({ type: 'close' }, event || {}))
|
||||
}
|
||||
})
|
||||
uni.onSocketError((err) => {
|
||||
if (typeof this.onerror === 'function') {
|
||||
this.onerror(Object.assign({ type: 'error' }, err || {}))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送数据(与浏览器 WebSocket.send 签名一致)
|
||||
* @param {string} data
|
||||
*/
|
||||
send(data) {
|
||||
if (this.readyState !== 1) {
|
||||
// 连接未就绪时暂存,连接建立后补发
|
||||
this._sendQueue.push(data)
|
||||
return
|
||||
}
|
||||
if (this.socketTask) {
|
||||
this.socketTask.send({
|
||||
data,
|
||||
fail: (err) => {
|
||||
console.warn('[UniAppWebSocket] 发送失败', err)
|
||||
}
|
||||
})
|
||||
} else {
|
||||
// 兜底:旧版小程序全局 socket API
|
||||
uni.sendSocketMessage({
|
||||
data,
|
||||
fail: (err) => {
|
||||
console.warn('[UniAppWebSocket] 发送失败', err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/** 主动关闭连接(主动关闭不再触发 onclose,由 SDK 自行控制重连) */
|
||||
close() {
|
||||
this._closedByUser = true
|
||||
this.readyState = 3
|
||||
if (this.socketTask) {
|
||||
this.socketTask.close({
|
||||
complete: () => {}
|
||||
})
|
||||
this.socketTask = null
|
||||
} else {
|
||||
uni.closeSocket({
|
||||
complete: () => {}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/** 连接建立后补发暂存消息 */
|
||||
_flush() {
|
||||
if (!this._sendQueue.length) return
|
||||
const queue = [...this._sendQueue]
|
||||
this._sendQueue = []
|
||||
queue.forEach((data) => this.send(data))
|
||||
}
|
||||
}
|
||||
+115
-145
@@ -1,10 +1,34 @@
|
||||
/* 碰盏日记 - WebSocket 管理器
|
||||
* 单例模式,负责私信实时通信
|
||||
* 协议格式与 HaveADrink SDK ChatWebSocket 一致:{ mesgType, data }
|
||||
* 支持心跳、自动重连、消息队列
|
||||
* 基于 HaveADrink SDK 的 ChatWebSocket(ChatWebSocketConn)实现:
|
||||
* - 底层 socket 通过 UniAppWebSocket 适配器注入(common/uni-websocket.js)
|
||||
* - 连接地址、token query、指数退避重连均由 SDK 管理
|
||||
* - 消息协议:{ mesgType, data }
|
||||
* 对外保持 on/off/send/connect/disconnect/userId 接口,页面层无感知
|
||||
*/
|
||||
|
||||
const WS_HOST = 'wss://dev.wash-painting.cn/api/have_a_drink/v1/chat/chat'
|
||||
import client, { API_HOST } from './api'
|
||||
// SDK 具名导出的连接类(绕开 client.ChatWebSocket 方法内部使用的浏览器 API)
|
||||
import { ChatWebSocketConn } from 'HaveADrink'
|
||||
|
||||
/**
|
||||
* 自行解析 host 并构造 ChatWebSocketConn
|
||||
* SDK 的 client.ChatWebSocket() 内部使用 new URL() / new URLSearchParams(),
|
||||
* 微信小程序沙箱中裸标识符 URL 解析到不可构造的空壳,全局 polyfill 无法生效,
|
||||
* 因此这里手动完成同样的逻辑:https -> wss,query 键值对拼接
|
||||
* @param {object} input - { token }
|
||||
* @returns {ChatWebSocketConn}
|
||||
*/
|
||||
function createChatConn(input) {
|
||||
const m = String(API_HOST).match(/^(https?|wss?):\/\/([^/?#\s]+)/i)
|
||||
const scheme = m && m[1].toLowerCase() === 'https' ? 'wss' : 'ws'
|
||||
const host = `${scheme}://${m ? m[2] : API_HOST}`
|
||||
const query = Object.keys(input || {})
|
||||
.filter((k) => input[k] !== '' && input[k] !== null && input[k] !== undefined)
|
||||
.map((k) => `${encodeURIComponent(k)}=${encodeURIComponent(input[k])}`)
|
||||
.join('&')
|
||||
return new ChatWebSocketConn(host, query)
|
||||
}
|
||||
|
||||
// 客户端消息类型(与 SDK ClientMesgType 枚举值一致)
|
||||
const ClientMesgType = {
|
||||
@@ -14,42 +38,26 @@ const ClientMesgType = {
|
||||
Ping: 'ping'
|
||||
}
|
||||
|
||||
// 服务端消息类型(与 SDK ServerMesgType 枚举值一致)
|
||||
const ServerMesgType = {
|
||||
Chat: 'chat',
|
||||
Typing: 'typing',
|
||||
Read: 'read',
|
||||
Ack: 'ack',
|
||||
Pong: 'pong',
|
||||
Connected: 'connected',
|
||||
Kicked: 'kicked'
|
||||
}
|
||||
|
||||
class WebSocketManager {
|
||||
constructor() {
|
||||
this.socketTask = null
|
||||
this.conn = null // SDK ChatWebSocketConn 实例
|
||||
this.isConnected = false
|
||||
this.isConnecting = false
|
||||
this.listeners = {} // 事件订阅 { event: [callbacks] }
|
||||
this.messageQueue = [] // 断线期间暂存消息
|
||||
this.messageQueue = [] // 断线期间暂存的消息 { mesgType, data }
|
||||
this.heartbeatTimer = null
|
||||
this.heartbeatTimeout = null
|
||||
this.reconnectTimer = null
|
||||
this.reconnectAttempts = 0
|
||||
this.maxReconnectDelay = 30000
|
||||
this.heartbeatInterval = 30000 // 30s 发一次 ping
|
||||
this.heartbeatWait = 60000 // 60s 无 pong 则重连
|
||||
this.manualClose = false
|
||||
this.token = ''
|
||||
this.userId = '' // 连接成功后服务端返回的用户ID
|
||||
this.userId = '' // 服务端 connected 事件返回的用户ID
|
||||
}
|
||||
|
||||
/**
|
||||
* 建立 WebSocket 连接
|
||||
* 建立 WebSocket 连接(走 SDK ChatWebSocket)
|
||||
* @param {string} token - 用户认证 token
|
||||
*/
|
||||
connect(token) {
|
||||
if (this.isConnected || this.isConnecting) return
|
||||
if (this.conn || this.isConnecting) return
|
||||
this.token = token || uni.getStorageSync('auth_token') || ''
|
||||
if (!this.token) {
|
||||
console.warn('[WS] 无 token,跳过连接')
|
||||
@@ -57,65 +65,80 @@ class WebSocketManager {
|
||||
}
|
||||
this.manualClose = false
|
||||
this.isConnecting = true
|
||||
console.log('[WS] 正在连接(SDK ChatWebSocket)...')
|
||||
|
||||
const url = WS_HOST
|
||||
console.log('[WS] 正在连接...')
|
||||
// SDK 内部:new websocket(`${host}/api/have_a_drink/v1/chat/chat?token=xxx`)
|
||||
// websocket 即注入的 UniAppWebSocket 适配器,重连由 SDK 指数退避管理
|
||||
// 注意:不用 client.ChatWebSocket()(其内部 new URL 在小程序沙箱不可用),
|
||||
// 直接用 SDK 导出的 ChatWebSocketConn 构造,等价实现
|
||||
const conn = createChatConn({ token: this.token })
|
||||
|
||||
this.socketTask = uni.connectSocket({
|
||||
url,
|
||||
header: {
|
||||
'Authorization': `Bearer ${this.token}`
|
||||
},
|
||||
complete: () => {}
|
||||
})
|
||||
|
||||
this.socketTask.onOpen(() => {
|
||||
conn.onopen = () => {
|
||||
console.log('[WS] 连接成功')
|
||||
this.isConnected = true
|
||||
this.isConnecting = false
|
||||
this.reconnectAttempts = 0
|
||||
this._startHeartbeat()
|
||||
this._flushQueue()
|
||||
this._emit('connect', {})
|
||||
})
|
||||
}
|
||||
|
||||
this.socketTask.onMessage((res) => {
|
||||
try {
|
||||
const msg = JSON.parse(res.data)
|
||||
this._handleMessage(msg)
|
||||
} catch (e) {
|
||||
console.warn('[WS] 消息解析失败', res.data)
|
||||
}
|
||||
})
|
||||
|
||||
this.socketTask.onClose(() => {
|
||||
conn.onclose = () => {
|
||||
console.log('[WS] 连接关闭')
|
||||
this.isConnected = false
|
||||
this.isConnecting = false
|
||||
this._stopHeartbeat()
|
||||
this._emit('disconnect', {})
|
||||
if (!this.manualClose) {
|
||||
this._scheduleReconnect()
|
||||
}
|
||||
})
|
||||
// 非主动关闭时,SDK 已自动安排指数退避重连,无需手动处理
|
||||
}
|
||||
|
||||
this.socketTask.onError((err) => {
|
||||
conn.onerror = (err) => {
|
||||
console.warn('[WS] 连接错误', err)
|
||||
this.isConnected = false
|
||||
this.isConnecting = false
|
||||
})
|
||||
}
|
||||
|
||||
// ===== 服务端协议消息(SDK 已解析 { mesgType, data }) =====
|
||||
conn.onServerChatData = (data) => {
|
||||
this._emit('message', data)
|
||||
}
|
||||
|
||||
conn.onServerTypingData = (data) => {
|
||||
this._emit('typing', data)
|
||||
}
|
||||
|
||||
conn.onServerReadData = (data) => {
|
||||
this._emit('read', data)
|
||||
}
|
||||
|
||||
conn.onServerAckData = (data) => {
|
||||
this._emit('ack', data)
|
||||
}
|
||||
|
||||
conn.onServerConnectedData = (data) => {
|
||||
// 服务端验证 token 成功后推送,携带 userId
|
||||
this.userId = data && data.userId ? data.userId : ''
|
||||
console.log('[WS] 服务端确认连接, userId:', this.userId)
|
||||
this._emit('connected', data)
|
||||
}
|
||||
|
||||
conn.onServerKickedData = (data) => {
|
||||
console.warn('[WS] 被踢下线')
|
||||
this._emit('kicked', data)
|
||||
this.disconnect()
|
||||
}
|
||||
|
||||
this.conn = conn
|
||||
}
|
||||
|
||||
/** 主动断开连接 */
|
||||
/** 主动断开连接(SDK close() 会关闭重连标志并断开 socket) */
|
||||
disconnect() {
|
||||
this.manualClose = true
|
||||
this._stopHeartbeat()
|
||||
this._clearReconnect()
|
||||
if (this.socketTask) {
|
||||
this.socketTask.close()
|
||||
this.socketTask = null
|
||||
if (this.conn) {
|
||||
this.conn.close()
|
||||
this.conn = null
|
||||
}
|
||||
this.isConnected = false
|
||||
this.isConnecting = false
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -124,17 +147,16 @@ class WebSocketManager {
|
||||
* @param {object} data - 消息数据
|
||||
*/
|
||||
send(mesgType, data = {}) {
|
||||
const payload = JSON.stringify({ mesgType, data })
|
||||
if (this.isConnected && this.socketTask) {
|
||||
this.socketTask.send({ data: payload })
|
||||
if (this.isConnected && this.conn) {
|
||||
this._write(mesgType, data)
|
||||
} else {
|
||||
// 断线暂存队列(ping 不暂存)
|
||||
if (mesgType !== ClientMesgType.Ping) {
|
||||
this.messageQueue.push(payload)
|
||||
this.messageQueue.push({ mesgType, data })
|
||||
}
|
||||
// 尝试重连
|
||||
if (!this.isConnecting && !this.manualClose) {
|
||||
this._scheduleReconnect()
|
||||
// 未连接时触发(重)连接
|
||||
if (!this.conn && !this.manualClose) {
|
||||
this.connect(this.token)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -167,38 +189,31 @@ class WebSocketManager {
|
||||
|
||||
// ========== 内部方法 ==========
|
||||
|
||||
/** 处理收到的消息(SDK ServerMesgType 协议) */
|
||||
_handleMessage(msg) {
|
||||
const { mesgType, data } = msg
|
||||
switch (mesgType) {
|
||||
case ServerMesgType.Chat:
|
||||
this._emit('message', data)
|
||||
break
|
||||
case ServerMesgType.Typing:
|
||||
this._emit('typing', data)
|
||||
break
|
||||
case ServerMesgType.Read:
|
||||
this._emit('read', data)
|
||||
break
|
||||
case ServerMesgType.Ack:
|
||||
this._emit('ack', data)
|
||||
break
|
||||
case ServerMesgType.Pong:
|
||||
this._onPong()
|
||||
break
|
||||
case ServerMesgType.Connected:
|
||||
// 服务端验证 token 成功后推送,携带 userId
|
||||
this.userId = data && data.userId ? data.userId : ''
|
||||
console.log('[WS] 服务端确认连接, userId:', this.userId)
|
||||
this._emit('connected', data)
|
||||
break
|
||||
case ServerMesgType.Kicked:
|
||||
console.warn('[WS] 被踢下线')
|
||||
this._emit('kicked', data)
|
||||
this.disconnect()
|
||||
break
|
||||
default:
|
||||
console.log('[WS] 未知消息类型', mesgType)
|
||||
/** 通过 SDK writeClient* 方法写入消息 */
|
||||
_write(mesgType, data) {
|
||||
try {
|
||||
switch (mesgType) {
|
||||
case ClientMesgType.Chat:
|
||||
this.conn.writeClientChatData(data)
|
||||
break
|
||||
case ClientMesgType.Typing:
|
||||
this.conn.writeClientTypingData(data)
|
||||
break
|
||||
case ClientMesgType.Read:
|
||||
this.conn.writeClientReadData(data)
|
||||
break
|
||||
case ClientMesgType.Ping:
|
||||
this.conn.writeClientPingData(data)
|
||||
break
|
||||
default:
|
||||
console.warn('[WS] 未知发送类型', mesgType)
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('[WS] 发送异常', e)
|
||||
// 发送失败时暂存,待重连后补发
|
||||
if (mesgType !== ClientMesgType.Ping) {
|
||||
this.messageQueue.push({ mesgType, data })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -212,61 +227,20 @@ class WebSocketManager {
|
||||
}
|
||||
}
|
||||
|
||||
/** 启动心跳 */
|
||||
/** 启动心跳(保活,防运营商/小程序后台掐连接) */
|
||||
_startHeartbeat() {
|
||||
this._stopHeartbeat()
|
||||
this.heartbeatTimer = setInterval(() => {
|
||||
this.send(ClientMesgType.Ping, {})
|
||||
// 设置超时检测
|
||||
this.heartbeatTimeout = setTimeout(() => {
|
||||
console.warn('[WS] 心跳超时,触发重连')
|
||||
if (this.socketTask) {
|
||||
this.socketTask.close()
|
||||
}
|
||||
}, this.heartbeatWait)
|
||||
}, this.heartbeatInterval)
|
||||
}
|
||||
|
||||
/** 收到 pong,清除超时 */
|
||||
_onPong() {
|
||||
if (this.heartbeatTimeout) {
|
||||
clearTimeout(this.heartbeatTimeout)
|
||||
this.heartbeatTimeout = null
|
||||
}
|
||||
}
|
||||
|
||||
/** 停止心跳 */
|
||||
_stopHeartbeat() {
|
||||
if (this.heartbeatTimer) {
|
||||
clearInterval(this.heartbeatTimer)
|
||||
this.heartbeatTimer = null
|
||||
}
|
||||
if (this.heartbeatTimeout) {
|
||||
clearTimeout(this.heartbeatTimeout)
|
||||
this.heartbeatTimeout = null
|
||||
}
|
||||
}
|
||||
|
||||
/** 指数退避重连 */
|
||||
_scheduleReconnect() {
|
||||
if (this.manualClose || this.reconnectTimer) return
|
||||
const delay = Math.min(1000 * Math.pow(2, this.reconnectAttempts), this.maxReconnectDelay)
|
||||
this.reconnectAttempts++
|
||||
console.log(`[WS] ${delay}ms 后尝试第 ${this.reconnectAttempts} 次重连`)
|
||||
this.reconnectTimer = setTimeout(() => {
|
||||
this.reconnectTimer = null
|
||||
this.isConnecting = false
|
||||
this.connect(this.token)
|
||||
}, delay)
|
||||
}
|
||||
|
||||
/** 清除重连计时器 */
|
||||
_clearReconnect() {
|
||||
if (this.reconnectTimer) {
|
||||
clearTimeout(this.reconnectTimer)
|
||||
this.reconnectTimer = null
|
||||
}
|
||||
this.reconnectAttempts = 0
|
||||
}
|
||||
|
||||
/** 重连成功后重发队列消息 */
|
||||
@@ -275,11 +249,7 @@ class WebSocketManager {
|
||||
console.log(`[WS] 重发 ${this.messageQueue.length} 条暂存消息`)
|
||||
const queue = [...this.messageQueue]
|
||||
this.messageQueue = []
|
||||
queue.forEach(payload => {
|
||||
if (this.socketTask && this.isConnected) {
|
||||
this.socketTask.send({ data: payload })
|
||||
}
|
||||
})
|
||||
queue.forEach(({ mesgType, data }) => this._write(mesgType, data))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user