265 lines
6.6 KiB
JavaScript
265 lines
6.6 KiB
JavaScript
/**
|
||
* 用户 WebSocket 单例:登录后全局一条连接,按 event 分发
|
||
* - device_data → appWs:deviceData(设备详情订阅读取)
|
||
* - device_online_status → appWs:deviceOnlineStatus(设备列表等)
|
||
* - notice → appWs:notice(通知 tab)
|
||
* 与首页 DeviceTab 列表批量 subscribe / unsubscribe 及 deviceUserWs mixin 协议一致
|
||
*/
|
||
// 与 deviceUserWs 同址,改一处即可
|
||
const WS_USER_URL = 'wss://eguo.chuantewulian.cn/prod-api/ws/user'
|
||
// const WS_USER_URL = 'ws://192.168.1.5:4601/ws/user'
|
||
|
||
let _task = null
|
||
let _socketOpen = false
|
||
let _reconnectTimer = null
|
||
let _reconnectAttempts = 0
|
||
const _maxAttempts = 5
|
||
let _reconnectInterval = 3000
|
||
let _appActive = true
|
||
|
||
function isMiniProgramPlatform() {
|
||
try {
|
||
if (typeof process !== 'undefined' && process.env && process.env.UNI_PLATFORM) {
|
||
return String(process.env.UNI_PLATFORM).indexOf('mp-') === 0
|
||
}
|
||
} catch (e) {}
|
||
if (typeof __wxConfig !== 'undefined' && __wxConfig != null) {
|
||
return true
|
||
}
|
||
return false
|
||
}
|
||
|
||
/**
|
||
* 与 common/http.interceptor.js buildUrlWithLang 一致:长连接 query 用服务端约定的 locale(如中文为 zh_CN 而非 zh)
|
||
*/
|
||
function wsLangQueryValue(storedLang) {
|
||
const s = String(storedLang || 'en')
|
||
switch (s) {
|
||
case 'ru':
|
||
return 'ru_RU'
|
||
case 'zh':
|
||
return 'zh_CN'
|
||
case 'ja':
|
||
return 'ja_JP'
|
||
case 'en':
|
||
return 'en_US'
|
||
default:
|
||
return s
|
||
}
|
||
}
|
||
|
||
function _buildUrl() {
|
||
let url = WS_USER_URL
|
||
const token = uni.getStorageSync('token') || ''
|
||
const lang = wsLangQueryValue(uni.getStorageSync('language') || 'en')
|
||
const q = []
|
||
if (token) {
|
||
q.push('token=' + encodeURIComponent(token))
|
||
}
|
||
q.push('lang=' + encodeURIComponent(lang))
|
||
return url + '?' + q.join('&')
|
||
}
|
||
|
||
function _getConnectOptions() {
|
||
const url = _buildUrl()
|
||
const token = uni.getStorageSync('token') || ''
|
||
if (isMiniProgramPlatform()) {
|
||
return { url }
|
||
}
|
||
return {
|
||
url,
|
||
header: {
|
||
'content-type': 'application/json;charset=UTF-8',
|
||
Authorization: token,
|
||
},
|
||
}
|
||
}
|
||
|
||
function _isTaskReady() {
|
||
if (!_task || typeof _task.send !== 'function') return false
|
||
if (typeof _task.readyState === 'number') {
|
||
return _task.readyState === 1
|
||
}
|
||
if (typeof _task.readyState === 'string') {
|
||
return String(_task.readyState).toUpperCase() === 'OPEN'
|
||
}
|
||
return false
|
||
}
|
||
|
||
function _clearReconnectTimer() {
|
||
if (_reconnectTimer != null) {
|
||
clearTimeout(_reconnectTimer)
|
||
_reconnectTimer = null
|
||
}
|
||
}
|
||
|
||
function _scheduleReconnect() {
|
||
if (!_appActive) return
|
||
if (!uni.getStorageSync('token')) return
|
||
if (_reconnectAttempts >= _maxAttempts) {
|
||
console.log('[appUserWs] 已达最大重连次数,停止')
|
||
return
|
||
}
|
||
_reconnectAttempts++
|
||
const delay = _reconnectInterval
|
||
console.log(`[appUserWs] 第 ${_reconnectAttempts} 次重连,约 ${delay / 1000}s 后…`)
|
||
_clearReconnectTimer()
|
||
_reconnectTimer = setTimeout(() => {
|
||
_reconnectTimer = null
|
||
connectIfLoggedIn(true)
|
||
}, delay)
|
||
_reconnectInterval = Math.min(_reconnectInterval * 2, 30000)
|
||
}
|
||
|
||
function _onMessage(res) {
|
||
let msg
|
||
try {
|
||
const raw = res && res.data !== undefined && res.data !== null ? res.data : res
|
||
msg = typeof raw === 'string' ? JSON.parse(raw) : raw
|
||
} catch (e) {
|
||
console.log('[appUserWs] 消息解析失败', res)
|
||
return
|
||
}
|
||
if (!msg || typeof msg !== 'object') return
|
||
const event = msg.event
|
||
if (event === 'device_data') {
|
||
uni.$emit('appWs:deviceData', msg)
|
||
return
|
||
}
|
||
if (event === 'device_online_status') {
|
||
uni.$emit('appWs:deviceOnlineStatus', msg)
|
||
return
|
||
}
|
||
if (event === 'notice') {
|
||
uni.$emit('appWs:notice', msg)
|
||
return
|
||
}
|
||
console.log('[appUserWs] 未处理 event:', event, msg)
|
||
}
|
||
|
||
function _doConnect(isReconnect) {
|
||
if (!uni.getStorageSync('token')) {
|
||
return
|
||
}
|
||
_clearReconnectTimer()
|
||
if (_task) {
|
||
try {
|
||
_task.close({})
|
||
} catch (e) {}
|
||
_task = null
|
||
}
|
||
_socketOpen = false
|
||
const opts = _getConnectOptions()
|
||
console.log('[appUserWs] 连接中…', opts.url)
|
||
_task = uni.connectSocket({
|
||
...opts,
|
||
success: () => {},
|
||
fail: (err) => {
|
||
console.error('[appUserWs] connectSocket 失败', err)
|
||
_task = null
|
||
_socketOpen = false
|
||
_scheduleReconnect()
|
||
},
|
||
})
|
||
if (!_task || !_task.onOpen) return
|
||
// 用闭包绑定当前条连接:切换语言/重连时旧 socket 的 onClose 晚于新 socket 创建,否则会误把 _task 置空并打挂新线
|
||
const sock = _task
|
||
_task.onOpen(() => {
|
||
if (_task !== sock) return
|
||
_reconnectAttempts = 0
|
||
_reconnectInterval = 3000
|
||
_socketOpen = true
|
||
console.log('[appUserWs] 已打开', opts.url)
|
||
uni.$emit('appWs:opened', {})
|
||
})
|
||
_task.onMessage(_onMessage)
|
||
_task.onError((err) => {
|
||
if (_task !== sock) return
|
||
console.error('[appUserWs] onError', err)
|
||
_socketOpen = false
|
||
_scheduleReconnect()
|
||
})
|
||
_task.onClose((res) => {
|
||
if (_task !== sock) {
|
||
// 旧线关闭,主连接已交给新 task,不清理、不重连
|
||
return
|
||
}
|
||
console.log('[appUserWs] onClose', res)
|
||
_socketOpen = false
|
||
_task = null
|
||
if (_appActive && uni.getStorageSync('token')) {
|
||
_scheduleReconnect()
|
||
}
|
||
})
|
||
}
|
||
|
||
/**
|
||
* 有 token 时建立/保持连接;无 token 时断开
|
||
* @param {boolean} [forceReconnect] 为 true 时强制走完整建连(重连场景)
|
||
*/
|
||
export function connectIfLoggedIn(forceReconnect) {
|
||
if (!uni.getStorageSync('token')) {
|
||
disconnect()
|
||
return
|
||
}
|
||
_appActive = true
|
||
// 已连接时直接返回,勿 emit appWs:opened,否则监听方会误以为是「新连接」反复触发表层逻辑(如列表多次退订/订阅)
|
||
if (!forceReconnect && _socketOpen && _task && _isTaskReady()) {
|
||
return
|
||
}
|
||
_doConnect()
|
||
}
|
||
|
||
export function disconnect() {
|
||
_appActive = false
|
||
_clearReconnectTimer()
|
||
_reconnectAttempts = 0
|
||
_reconnectInterval = 3000
|
||
_socketOpen = false
|
||
if (_task) {
|
||
try {
|
||
_task.close({ success: () => {}, fail: () => {} })
|
||
} catch (e) {}
|
||
_task = null
|
||
}
|
||
}
|
||
|
||
export function isOpen() {
|
||
return _socketOpen && _isTaskReady()
|
||
}
|
||
|
||
/**
|
||
* 发送 JSON(subscribe / unsubscribe 等)
|
||
*/
|
||
export function sendJson(obj, label) {
|
||
if (!_isTaskReady()) {
|
||
console.warn('[appUserWs] 未就绪,跳过发送', label, obj)
|
||
return
|
||
}
|
||
try {
|
||
_task.send({
|
||
data: JSON.stringify(obj),
|
||
success: () => {
|
||
if (label) console.log('[appUserWs] 发送', label, obj)
|
||
},
|
||
fail: (err) => {
|
||
console.error('[appUserWs] 发送失败', err, obj)
|
||
},
|
||
})
|
||
} catch (e) {
|
||
console.error('[appUserWs] send 异常', e)
|
||
}
|
||
}
|
||
|
||
export function setAppActive(active) {
|
||
_appActive = active !== false
|
||
}
|
||
|
||
/**
|
||
* 语言变更后重连:URL 的 lang 与 Storage 一致,需完整断开再建连,请保证先 setStorageSync('language', …)
|
||
* 与 connectIfLoggedIn(true) 等价,命名便于在 i18n 等处明确语义
|
||
*/
|
||
export function reconnectUserWsForLanguage() {
|
||
connectIfLoggedIn(true)
|
||
}
|