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 3f2e563..23a2fcb 100644
--- a/src/views/contract/handicap.vue
+++ b/src/views/contract/handicap.vue
@@ -136,6 +136,18 @@ export default {
buyList: [],
tradeList: [],
newPriceObj: {},
+
+ // 保存事件回调,防止内存泄漏
+ wsOpenHandler: null,
+ wsMessageHandler: null,
+ collapseHandler: null,
+
+ // 防止接口无限密集重试的锁
+ isRetrying: false,
+
+ lastMessageTime: 0, // 最后一次收到数据的时间戳
+ watchdogTimer: null, // 看门狗定时器
+ watchdogTimeout: 5000 // 判定假死的时间:5秒(可根据你们后端的推送频率调整)
};
},
props: {
@@ -194,6 +206,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);
@@ -201,23 +214,70 @@ export default {
localStorage.setItem("price",this.newPriceObj.price)
});
},
+ 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;
- this.newPriceObj = this.tradeList[0];
- this.$emit("input", this.newPriceObj);
- localStorage.setItem("price",this.newPriceObj.price)
- });
- },
+ // getMarketInfo() {
+ // let data = {
+ // symbol: this.symbol
+ // };
+ // Contract.getMarketInfo(data).then(res => {
+ // this.sellList = res.swapSellList;
+ // this.buyList = res.swapBuyList;
+ // this.tradeList = res.swapTradeList;
+ // this.newPriceObj = this.tradeList[0];
+ // this.$emit("input", this.newPriceObj);
+ // localStorage.setItem("price",this.newPriceObj.price)
+ // });
+ // },
+
+
+ // ✅ 修复:加入 .catch 捕获超时,并自动延迟重试
+ getMarketInfo() {
+ 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
@@ -226,74 +286,182 @@ 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) {
- 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) {
+ // 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 e183362..b5f22f3 100644
--- a/src/views/contract/index.vue
+++ b/src/views/contract/index.vue
@@ -223,6 +223,259 @@