js的websocket客户端开发的代码割裂情况的解决

什么是代码割裂的情况

用一个例子说明一切。
在login方法发送登录,在onmessage方法处理登录的返回结果。
这种情况有两个弊端:

  1. 处理逻辑割裂,一整个逻辑被分割到两个地方处理。
  2. 上下文割裂,比如登录失败,我需要在登录按钮的边上搞个提示语,因为处理逻辑被割裂到两个函数,导致通过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 协议》,转载必须注明作者和本文链接
梦想星辰大海
《L01 基础入门》
我们将带你从零开发一个项目并部署到线上,本课程教授 Web 开发中专业、实用的技能,如 Git 工作流、Laravel Mix 前端工作流等。
《L03 构架 API 服务器》
你将学到如 RESTFul 设计风格、PostMan 的使用、OAuth 流程,JWT 概念及使用 和 API 开发相关的进阶知识。
讨论数量: 0
(= ̄ω ̄=)··· 暂无内容!

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!