js的websocket客户端开发的代码割裂情况的解决
什么是代码割裂的情况
用一个例子说明一切。
在login方法发送登录,在onmessage方法处理登录的返回结果。
这种情况有两个弊端:
- 处理逻辑割裂,一整个逻辑被分割到两个地方处理。
- 上下文割裂,比如登录失败,我需要在登录按钮的边上搞个提示语,因为处理逻辑被割裂到两个函数,导致通过onclick传递的this,被丢失了。
示例代码如下:
<style>
div {
margin: 10px;
}
</style>
<div>
<a href="javascript:;" onclick="socket.connect()">打开连接</a>
<a href="javascript:;" onclick="socket.close()">关闭连接</a>
</div>
<div>
<label for="j-token">token</label><input type="text" id="j-token" value="WagCUcmxoUB3brbT">
<a href="javascript:;" onclick="login(this)" id="j-login">登录</a>
</div>
<script>
const socket = {
_ws: null,
_heartbeatIndex: -1,
_onopen: function () {
this._heartbeatIndex = setInterval(function () {
if (!socket._ws) {
if (socket._heartbeatIndex > 0) {
clearTimeout(socket._heartbeatIndex);
socket._heartbeatIndex = -1;
}
return;
}
socket._ws.send('~3yPvmnz~');
}, 45 * 1000);
},
_onmessage: function (event) {
if (event.data === '~u38NvZ~') {
return;
}
//接收服务端的消息,并进行逻辑处理
const router = JSON.parse(event.data);
//处理登录请求的返回
if (router.cmd === cmd.login) {
//因为发送登录的逻辑在login函数,所以拿不到触发登录的按钮,只能重新查找。
let a = document.querySelector('#j-login');
if (router.code === 0) {
a.innerText = '登录成功';
} else {
a.innerText = '登录失败' + router.data;
}
}
},
_onclose: function () {
console.log('WebSocket连接已关闭');
},
_onerror: function (error) {
console.log('WebSocket错误: ' + error);
},
/**
* 发送一条消息到服务端
* @param cmd int
* @param data object|string
*/
send: function (cmd, data) {
if (!this._ws) {
return;
}
if (data instanceof Object) {
data = JSON.stringify(data);
}
let router = {
cmd: cmd,
data: data
};
this._ws.send(JSON.stringify(router));
},
connect: function () {
this._ws = new WebSocket('ws://127.0.0.1:7272');
this._ws.onopen = this._onopen;
this._ws.onmessage = this._onmessage;
this._ws.onclose = this._onclose;
this._ws.onerror = this._onerror;
},
close: function () {
if (this._ws) {
this._ws.close(1000, '主动关闭连接');
this._ws = null;
}
}
};
const cmd = {
login: 3,
};
function login(aThis) {
//这个上下文在websocket对象返回数据时,是拿不到的。
console.log(aThis);
let data = {
token: document.querySelector("#j-token").value,
type: 2
};
socket.send(cmd.login, data);
}
</script>
解决方案
用messageChannel进行通信。
示例代码如下:
<style>
div {
margin: 10px;
}
</style>
<div>
<a href="javascript:;" onClick="wsSocketObj.connect()">打开连接</a>
<a href="javascript:;" onClick="wsSocketObj.close()">关闭连接</a>
</div>
<div>
<label for="j-token">token</label><input type="text" id="j-token" value="WagCUcmxoUB3brbT">
<a href="javascript:;" onClick="login(this)">登录</a>
</div>
<script>
const wsSocketObj = {
_ws: null,
_heartbeatIndex: -1,
_requestId: 0,
_channel: new Map(),
_onopen: function () {
wsSocketObj._heartbeatIndex = setInterval(function () {
if (!wsSocketObj._ws) {
if (wsSocketObj._heartbeatIndex > 0) {
clearTimeout(wsSocketObj._heartbeatIndex);
wsSocketObj._heartbeatIndex = -1;
}
return;
}
wsSocketObj._ws.send('~3yPvmnzu38NZv~');
}, 25 * 1000);
},
_onmessage: function (event) {
//这里有个要求,服务端的返回结构必须是:{cmd: int, code: int, data: mixed, requestId: string},其中requestId必须原样回传
const router = JSON.parse(event.data);
const channel = wsSocketObj._channel.get(router.requestId);
wsSocketObj._channel.delete(router.requestId);
if (!channel instanceof MessageChannel) {
console.log('客户端未知的消息:' + event.data);
return;
}
channel.port2.postMessage(router);
},
_onclose: function () {
wsSocketObj._ws = null;
setTimeout(function () {
wsSocketObj.connect();
}, 3000);
console.log('WebSocket连接已关闭');
},
_onerror: function (error) {
wsSocketObj._ws = null;
setTimeout(function () {
wsSocketObj.connect();
}, 3000);
console.log('WebSocket错误: ' + error);
},
/**
* 发送一条消息到服务端
* @param cmd int
* @param data object|string
* @param timeout int 服务端响应的超时时间,单位秒
* @returns {Promise<unknown>}
*/
send: async function (cmd, data, timeout = 60) {
if (!wsSocketObj._ws) {
return;
}
if (data instanceof Object) {
data = JSON.stringify(data);
}
let router = {
cmd: cmd,
data: data,
requestId: (new Date).getTime() + '-' + (++wsSocketObj._requestId)
};
//明确了,不需要服务器返回数据
if (timeout <= 0) {
wsSocketObj._ws.send(JSON.stringify(router));
return;
}
const channel = new MessageChannel();
wsSocketObj._channel.set(router.requestId, channel);
wsSocketObj._ws.send(JSON.stringify(router));
return new Promise(function (resolve) {
//需要服务器返回数据,但是服务器不一定会返回,有可能不返回,有可能超时,所以一定要有一个timeout
let setTimeoutIndex = setTimeout(function () {
wsSocketObj._channel.delete(router.requestId);
channel.port1.close();
channel.port2.close();
router.data = '服务端响应超时';
router.code = 500;
resolve(router);
}, 1000 * timeout);
channel.port1.onmessage = function (event) {
clearTimeout(setTimeoutIndex);
channel.port1.close();
channel.port2.close();
resolve(event.data);
};
channel.port1.onmessageerror = function (event) {
clearTimeout(setTimeoutIndex);
wsSocketObj._channel.delete(router.requestId);
channel.port1.close();
channel.port2.close();
router.data = '客户端socket接收逻辑错误:' + event.data;
router.code = 400;
resolve(router);
};
});
},
connect: function () {
if (wsSocketObj._ws) {
return;
}
wsSocketObj._ws = new WebSocket('ws://127.0.0.1:7272');
wsSocketObj._ws.onopen = wsSocketObj._onopen;
wsSocketObj._ws.onmessage = wsSocketObj._onmessage;
wsSocketObj._ws.onclose = wsSocketObj._onclose;
wsSocketObj._ws.onerror = wsSocketObj._onerror;
},
close: function () {
if (wsSocketObj._ws) {
wsSocketObj._ws.close(1000, '主动关闭连接');
wsSocketObj._ws = null;
}
}
};
const cmd = {
login: 3,
};
async function login(aThis) {
let data = {
token: document.querySelector("#j-token").value,
type: 2
};
//发送请求,并接收响应
let router = await wsSocketObj.send(cmd.login, data);
//处理响应
if (router.code === 0) {
aThis.innerText = '登录成功';
} else {
aThis.innerText = '登录失败' + router.data;
}
}
</script>
本作品采用《CC 协议》,转载必须注明作者和本文链接
推荐文章: