diff --git a/src/api/contract.js b/src/api/contract.js index f72d3e2..c7418e4 100644 --- a/src/api/contract.js +++ b/src/api/contract.js @@ -11,14 +11,14 @@ class Contract { } static getMarketInfo(data) { - return server.get(`/contract/getMarketInfo`, {params:data}) + return server.get(`/contract/getMarketInfo`, {params:data,config:{loading:false}}) } /** * 获取合约市场 */ static getMarketList(data) { - return server.get('/contract/getMarketList', {params:data}) + return server.get('/contract/getMarketList', {params:data,config:{loading:false}}) } /** diff --git a/src/api/server/Socket.js b/src/api/server/Socket.js index 3e3cdea..6d18cf4 100644 --- a/src/api/server/Socket.js +++ b/src/api/server/Socket.js @@ -1,52 +1,217 @@ +// class Socket { + +// constructor(link, ...args) { +// // 初始化socket +// if (link.constructor === WebSocket) { +// this.socket = link; +// } else { +// this.socket = new WebSocket(link); +// } + +// // this.socket.binaryType = 'arraybuffer'; + +// this.doOpen(); + +// // 连接状态的标识符 +// this.readyState = this.socket.readyState; +// // 订阅/发布模型 +// this._events = { +// // 订阅的事件 : 发布的方法 + +// }; + +// // 定时验证的标识符 +// this.heartBeatTimer = null; + +// } + +// // 执行socket并发布事件 +// doOpen() { + +// this.afterOpenEmit = []; + +// // 执行socket连接 并初始化验证请求 +// this.socket.addEventListener("open", evt => this.onOpen(evt)); + +// // 接收socket数据 +// this.socket.addEventListener("message", evt => this.onMessage(evt)); + +// // 关闭socket连接 +// this.socket.addEventListener("close", evt => this.onClose(evt)); + +// // 请求发生错误 +// this.socket.addEventListener("error", err => this.onError(err)); + +// } + +// // 发布后通知订阅者 +// Notify(entry) { +// // 检查是否有订阅者 返回队列 +// const cbQueue = this._events[entry.Event]; +// if (cbQueue && cbQueue.length) { +// for (let callback of cbQueue) { +// if (callback instanceof Function) callback(entry.Data); +// } +// } +// } + +// // 请求数据的方法 +// onOpen(evt) { + +// // 每隔20s检查连接 +// // this.heartBeatTimer = setInterval(() => this.send({ +// // 'cmd': 'ping', +// // 'args': '' +// // }), 20000); + +// // 通知订阅 +// this.Notify({Event: 'open', Data : evt}); +// } + +// /** +// * 订阅所有的数据 +// * @param {array|object} datas 订阅参数集合 +// */ +// send(datas) { +// if (datas.constructor != Array) { +// datas = [datas]; +// } + +// for (let item of datas) { +// this.socket.send(JSON.stringify(item)); +// } +// } + + +// onMessage(evt) { + +// try { + +// // 解析推送的数据 +// const data = JSON.parse(evt.data); + +// // 通知订阅者 +// this.Notify({ +// Event: 'message', +// Data: data +// }); + +// } catch (err) { +// console.error(' >> Data parsing error:', err); + +// // 通知订阅者 +// this.Notify({ +// Event: 'error', +// Data: err +// }); +// } +// } + +// // 添加事件监听 +// on(name, handler) { +// this.subscribe(name, handler); +// } + +// // 取消订阅事件 +// off(name, handler) { +// this.unsubscribe(name, handler); +// } + +// // 订阅事件的方法 +// subscribe(name, handler) { +// if (this._events.hasOwnProperty(name)) { +// this._events[name].push(handler); // 追加事件 +// } else { +// this._events[name] = [handler]; // 添加事件 +// } +// } + +// // 取消订阅事件 +// unsubscribe(name, handler) { + +// let start = this._events[name].findIndex(item => item === handler); + +// // 删除该事件 +// this._events[name].splice(start, 1); + +// } + +// checkOpen() { +// return this.readyState >= 2; +// } + +// onClose(evt) { +// this.Notify({Event: 'close', Data : evt}); +// } + + +// onError(err) { +// this.Notify({Event: 'error', Data : err}); +// } + +// emit(data) { +// return new Promise((resolve) => { +// this.send(JSON.stringify(data)); +// this.on('message', function (data) { +// resolve(data); +// }); +// }); +// } + +// doClose() { +// this.socket.close(); +// } + +// destroy() { +// if (this.heartBeatTimer) { +// clearInterval(this.heartBeatTimer); +// this.heartBeatTimer = null; +// } +// this.doClose(); +// this._events = {}; +// this.readyState = 0; +// this.socket = null; +// } +// } + +// export default Socket + + + class Socket { constructor(link, ...args) { - // 初始化socket + // 修复1:确保 this.link 存的是字符串,方便后面断线重连 if (link.constructor === WebSocket) { this.socket = link; + this.link = link.url; } else { this.socket = new WebSocket(link); + this.link = link; } - // this.socket.binaryType = 'arraybuffer'; - this.doOpen(); - // 连接状态的标识符 this.readyState = this.socket.readyState; - // 订阅/发布模型 - this._events = { - // 订阅的事件 : 发布的方法 - - }; - - // 定时验证的标识符 + this._events = {}; + this.heartBeatTimer = null; - + + // 新增:重连相关的标识符 + this.reconnectTimer = null; + this.isReconnecting = false; + this.manualClose = false; // 判断是否是用户主动退出页面 } - // 执行socket并发布事件 doOpen() { - this.afterOpenEmit = []; - - // 执行socket连接 并初始化验证请求 this.socket.addEventListener("open", evt => this.onOpen(evt)); - - // 接收socket数据 this.socket.addEventListener("message", evt => this.onMessage(evt)); - - // 关闭socket连接 this.socket.addEventListener("close", evt => this.onClose(evt)); - - // 请求发生错误 this.socket.addEventListener("error", err => this.onError(err)); - } - // 发布后通知订阅者 Notify(entry) { - // 检查是否有订阅者 返回队列 const cbQueue = this._events[entry.Event]; if (cbQueue && cbQueue.length) { for (let callback of cbQueue) { @@ -55,118 +220,152 @@ class Socket { } } - // 请求数据的方法 onOpen(evt) { + // 连接成功,重置状态 + this.isReconnecting = false; + this.manualClose = false; + + // 每次连上先清空旧的心跳,防止重复 + if (this.heartBeatTimer) clearInterval(this.heartBeatTimer); + + // 每隔20s发送心跳 + this.heartBeatTimer = setInterval(() => { + this.send({ + 'cmd': 'ping', + 'args': '' + }); + }, 20000); - // 每隔20s检查连接 - // this.heartBeatTimer = setInterval(() => this.send({ - // 'cmd': 'ping', - // 'args': '' - // }), 20000); - - // 通知订阅 this.Notify({Event: 'open', Data : evt}); } - /** - * 订阅所有的数据 - * @param {array|object} datas 订阅参数集合 - */ send(datas) { if (datas.constructor != Array) { - datas = [datas]; + datas = [datas]; } for (let item of datas) { - this.socket.send(JSON.stringify(item)); + // 修复2:发送前必须检查状态,彻底解决 "CLOSING or CLOSED state" 报错! + if (this.socket && this.socket.readyState === 1) { + this.socket.send(JSON.stringify(item)); + } else { + console.warn("WebSocket未连接,已拦截发送请求"); + } } } - onMessage(evt) { - try { - - // 解析推送的数据 const data = JSON.parse(evt.data); - - // 通知订阅者 - this.Notify({ - Event: 'message', - Data: data - }); - + this.Notify({ Event: 'message', Data: data }); } catch (err) { console.error(' >> Data parsing error:', err); - - // 通知订阅者 - this.Notify({ - Event: 'error', - Data: err - }); + this.Notify({ Event: 'error', Data: err }); } } - // 添加事件监听 on(name, handler) { this.subscribe(name, handler); } - // 取消订阅事件 off(name, handler) { this.unsubscribe(name, handler); } - // 订阅事件的方法 subscribe(name, handler) { if (this._events.hasOwnProperty(name)) { - this._events[name].push(handler); // 追加事件 + this._events[name].push(handler); } else { - this._events[name] = [handler]; // 添加事件 + this._events[name] = [handler]; } } - // 取消订阅事件 unsubscribe(name, handler) { - + if (!this._events[name]) return; let start = this._events[name].findIndex(item => item === handler); - - // 删除该事件 - this._events[name].splice(start, 1); - + if (start > -1) { + this._events[name].splice(start, 1); + } } checkOpen() { return this.readyState >= 2; } + // 修复3:核心重连逻辑 + reconnect() { + // 如果是手动关闭(退出页面),或者正在重连中,则放弃重连 + if (this.manualClose || this.isReconnecting) return; + + this.isReconnecting = true; + + // 断网了,立刻停止发心跳 + if (this.heartBeatTimer) { + clearInterval(this.heartBeatTimer); + this.heartBeatTimer = null; + } + + console.log("WebSocket 掉线,3秒后尝试重新连接..."); + + this.reconnectTimer = setTimeout(() => { + try { + this.socket = new WebSocket(this.link); + this.doOpen(); + } catch (e) { + console.error("重连失败", e); + // 即使抛出异常也解除锁,等待下一次重连尝试 + this.isReconnecting = false; + this.reconnect(); + return; + } + }, 3000); + } + onClose(evt) { this.Notify({Event: 'close', Data : evt}); + // 触发关闭时,只要不是主动退出的,就去重连 + if (!this.manualClose) this.reconnect(); } - onError(err) { this.Notify({Event: 'error', Data : err}); + // 发生错误断开时,去重连 + if (!this.manualClose) this.reconnect(); } + // 修复4:解决内存泄漏,用完立刻注销 emit(data) { return new Promise((resolve) => { this.send(JSON.stringify(data)); - this.on('message', function (data) { - resolve(data); - }); + + const handler = (resData) => { + resolve(resData); + this.off('message', handler); // 立刻注销,释放内存 + }; + + this.on('message', handler); }); } doClose() { - this.socket.close(); + this.manualClose = true; // 标记为主动关闭,防止变成僵尸无限重连 + if (this.socket) { + this.socket.close(); + } } destroy() { + this.manualClose = true; + if (this.heartBeatTimer) { clearInterval(this.heartBeatTimer); this.heartBeatTimer = null; } + if (this.reconnectTimer) { + clearTimeout(this.reconnectTimer); + this.reconnectTimer = null; + } + this.doClose(); this._events = {}; this.readyState = 0; @@ -174,4 +373,4 @@ class Socket { } } -export default Socket +export default Socket; \ No newline at end of file diff --git a/src/views/contract/handicap.vue b/src/views/contract/handicap.vue index 4aa83d7..97144eb 100644 --- a/src/views/contract/handicap.vue +++ b/src/views/contract/handicap.vue @@ -140,6 +140,18 @@ export default { buyList: [], tradeList: [], newPriceObj: {}, + + // 保存事件回调,防止内存泄漏 + wsOpenHandler: null, + wsMessageHandler: null, + collapseHandler: null, + + // 防止接口无限密集重试的锁 + isRetrying: false, + + lastMessageTime: 0, // 最后一次收到数据的时间戳 + watchdogTimer: null, // 看门狗定时器 + watchdogTimeout: 5000 // 判定假死的时间:5秒(可根据你们后端的推送频率调整) }; }, props: { @@ -197,6 +209,7 @@ export default { if (this.symbol) { this.getMarketInfo(); this.linkSocket(); + this.startWatchdog(); } bus.$on('collapse', msg => { this.newPriceObj.price =this.symbol=='BTC'? (msg.close).toFixed(1):(msg.close).toFixed(3); @@ -213,25 +226,72 @@ export default { // this.$emit("input", this.newPriceObj); }); }, + beforeDestroy() { + this.unLink(this.symbol); + this.removeSocketListeners(); + this.stopWatchdog(); + }, // 获取盘口 methods: { parseTime: date.parseTime, omitTo: math.omitTo, + // getMarketInfo() { + // let data = { + // symbol: this.symbol + // }; + // Contract.getMarketInfo(data).then(res => { + // this.sellList = res.swapSellList; + // this.buyList = res.swapBuyList; + // this.tradeList = res.swapTradeList; + // // if(this.tradeList.length>0){ + // this.newPriceObj = this.tradeList[0]; + // // } + // this.$emit("input", this.newPriceObj); + // localStorage.setItem("price",this.newPriceObj.price) + // }); + // }, + + + // 修复:加入 .catch 捕获超时,并自动延迟重试 getMarketInfo() { - let data = { - symbol: this.symbol - }; - Contract.getMarketInfo(data).then(res => { - this.sellList = res.swapSellList; - this.buyList = res.swapBuyList; - this.tradeList = res.swapTradeList; - // if(this.tradeList.length>0){ - this.newPriceObj = this.tradeList[0]; - // } - this.$emit("input", this.newPriceObj); - localStorage.setItem("price",this.newPriceObj.price) - }); + if (!this.symbol) return; + + let data = { symbol: this.symbol }; + + Contract.getMarketInfo(data).then(res => { + this.isRetrying = false; // 成功后解除重试锁 + this.sellList = res.swapSellList; + this.buyList = res.swapBuyList; + this.tradeList = res.swapTradeList; + + if(this.tradeList && this.tradeList.length > 0){ + this.newPriceObj = this.tradeList[0]; + } + this.$emit("input", this.newPriceObj); + localStorage.setItem("price", this.newPriceObj.price); + + }).catch(err => { + // 🔥 捕获超时异常,防止程序崩溃 (Uncaught in promise) + console.error("盘口接口请求失败/超时,3秒后自动重试...", err); + + // 如果没有在重试中,就开启延迟重试 + if (!this.isRetrying) { + this.isRetrying = true; + setTimeout(() => { + this.isRetrying = false; + this.getMarketInfo(); // 重新发起请求 + + // 顺便检查一下 WS,如果断了,通过底层重连 + if (this.ws && !this.ws.checkOpen()) { + console.log("检测到网络较差,尝试重连 WebSocket..."); + this.ws.reconnect(); + } + }, 3000); // 延迟 3 秒重试,避免把服务器打死 + } + }); }, + + // 计算深度 getValue(amount) { const arr = this.buyListShow @@ -240,75 +300,183 @@ export default { let max = Math.max(...arr); return math.division(amount, max, 2) * 100; }, - // 连接socket - linkSocket() { - if (this.ws.socket.readyState == 1) { - this.ws.send({ - cmd: "sub", - msg: this.msg.buy - }); - this.ws.send({ - cmd: "sub", - msg: this.msg.sell - }); - this.ws.send({ - cmd: "sub", - msg: this.msg.trade - }); - } else { - this.ws.on("open", () => { - this.ws.send({ - cmd: "sub", - msg: this.msg.buy - }); - this.ws.send({ - cmd: "sub", - msg: this.msg.sell - }); - this.ws.send({ - cmd: "sub", - msg: this.msg.trade - }); - }); - } - this.ws.on("message", res => { - let { data, msg, code, sub, type, status, cmd } = res; - if (sub == this.msg.buy) { - this.buyList = data; - } else if (sub == this.msg.sell) { - this.sellList = data; - } else if (sub == this.msg.trade) { - // console.log('if (sub == this.msg.trade)', this.msg.trade) - this.tradeList.unshift(data); - this.tradeList.pop(); - // this.newPriceObj = data; - // this.$emit("input", this.newPriceObj); - } else if (type == "ping" ||cmd == "ping") { - this.ws.send({ - cmd: "pong" - }); - } - }); + + + // 封装发送订阅方法 + sendSubscribe() { + if (!this.ws || !this.ws.socket || this.ws.socket.readyState !== 1) return; + this.ws.send({ cmd: "sub", msg: this.msg.buy }); + this.ws.send({ cmd: "sub", msg: this.msg.sell }); + this.ws.send({ cmd: "sub", msg: this.msg.trade }); + }, + + // 清理事件监听器 + removeSocketListeners() { + if (this.wsOpenHandler && this.ws) { + this.ws.off("open", this.wsOpenHandler); + } + if (this.wsMessageHandler && this.ws) { + this.ws.off("message", this.wsMessageHandler); + } + }, + + // 强化版看门狗:直接暴力强杀并重启 WebSocket + startWatchdog() { + this.stopWatchdog(); + this.lastMessageTime = Date.now(); + + this.watchdogTimer = setInterval(() => { + const now = Date.now(); + if (now - this.lastMessageTime > this.watchdogTimeout) { + console.error(`[看门狗报警] 已经 ${this.watchdogTimeout/1000} 秒没收到推送了!判定为后端假死!正在强行掐断并新建 WebSocket...`); + + // 1. 保底先通过 HTTP 请求拉取一次最新数据,防止黑屏 + this.getMarketInfo(); + + // 2. 强行干预底层的 Socket 实例 + if (this.ws) { + // 强制关闭旧的底层的原生连接(必须强制,否则服务器以为你还连着) + if (this.ws.socket) { + this.ws.socket.close(); + } + + // 强制解除底层的重连锁,并调用我们封装好的 reconnect 暴力重启它! + this.ws.isReconnecting = false; + this.ws.manualClose = false; + this.ws.reconnect(); + } + + // 3. 喂一次狗,重置时间,防止在重连的过程中它疯狂重复杀连接 + this.lastMessageTime = Date.now(); + } + }, 5000); // 每 5 秒巡逻检查一次 + }, + + // 🔥 新增:停止看门狗 + stopWatchdog() { + if (this.watchdogTimer) { + clearInterval(this.watchdogTimer); + this.watchdogTimer = null; + } + }, + + // 完善的 WebSocket 绑定与重连机制 + linkSocket() { + if (!this.ws) return; + + this.removeSocketListeners(); + + // 监听 open 事件(无论是初次连接还是断网重连,都会触发这里) + this.wsOpenHandler = () => { + this.sendSubscribe(); + }; + this.ws.on("open", this.wsOpenHandler); + + // 监听消息 + this.wsMessageHandler = res => { + let { data, msg, code, sub, type, status, cmd } = res; + + if (sub == this.msg.buy || sub == this.msg.sell || sub == this.msg.trade) { + this.lastMessageTime = Date.now(); + } + + if (sub == this.msg.buy) { + this.buyList = data; + } else if (sub == this.msg.sell) { + this.sellList = data; + } else if (sub == this.msg.trade) { + this.tradeList.unshift(data); + this.tradeList.pop(); + } else if (type == "ping" || cmd == "ping") { + this.ws.send({ cmd: "pong" }); + } + }; + this.ws.on("message", this.wsMessageHandler); + + // 如果当前已经是连接状态,立刻发送一次 + if (this.ws.socket && this.ws.socket.readyState === 1) { + this.sendSubscribe(); + } + this.startWatchdog(); // 确保看门狗在跑 + }, + + unLink(symbol) { + if (!this.ws || !this.ws.socket || this.ws.socket.readyState !== 1) return; + this.ws.send({ cmd: "unsub", msg: `swapBuyList_${symbol}` }); + this.ws.send({ cmd: "unsub", msg: `swapSellList_${symbol}` }); + this.ws.send({ cmd: "unsub", msg: `swapTradeList_${symbol}` }); + } + + + // // 连接socket + // linkSocket() { + // if (this.ws.socket.readyState == 1) { + // this.ws.send({ + // cmd: "sub", + // msg: this.msg.buy + // }); + // this.ws.send({ + // cmd: "sub", + // msg: this.msg.sell + // }); + // this.ws.send({ + // cmd: "sub", + // msg: this.msg.trade + // }); + // } else { + // this.ws.on("open", () => { + // this.ws.send({ + // cmd: "sub", + // msg: this.msg.buy + // }); + // this.ws.send({ + // cmd: "sub", + // msg: this.msg.sell + // }); + // this.ws.send({ + // cmd: "sub", + // msg: this.msg.trade + // }); + // }); + // } + // this.ws.on("message", res => { + // let { data, msg, code, sub, type, status, cmd } = res; + // if (sub == this.msg.buy) { + // this.buyList = data; + // } else if (sub == this.msg.sell) { + // this.sellList = data; + // } else if (sub == this.msg.trade) { + // // console.log('if (sub == this.msg.trade)', this.msg.trade) + // this.tradeList.unshift(data); + // this.tradeList.pop(); + // // this.newPriceObj = data; + // // this.$emit("input", this.newPriceObj); + // } else if (type == "ping" ||cmd == "ping") { + // this.ws.send({ + // cmd: "pong" + // }); + // } + // }); - }, - // 取消订阅 - unLink(symbol) { - // 取消买线 - this.ws.send({ - cmd: "unsub", - msg: `swapBuyList_${symbol}` - }); - // 取消卖线 - this.ws.send({ - cmd: "unsub", - msg: `swapSellList_${symbol}` - }); - // 取消成交 - this.ws.send({ - cmd: "unsub", - msg: `swapTradeList_${symbol}` - }); - } + // }, + // // 取消订阅 + // unLink(symbol) { + // // 取消买线 + // this.ws.send({ + // cmd: "unsub", + // msg: `swapBuyList_${symbol}` + // }); + // // 取消卖线 + // this.ws.send({ + // cmd: "unsub", + // msg: `swapSellList_${symbol}` + // }); + // // 取消成交 + // this.ws.send({ + // cmd: "unsub", + // msg: `swapTradeList_${symbol}` + // }); + // } } }; diff --git a/src/views/contract/index.vue b/src/views/contract/index.vue index 4b7b2a2..c9ffbf0 100644 --- a/src/views/contract/index.vue +++ b/src/views/contract/index.vue @@ -59,7 +59,8 @@ class="w-7/24 " :class="item.increase < 0 ? 'decreace' : 'increace'" > - {{ item.price }} + + {{item.symbol == activeSymbol ? price1 : item.price}}