超级简单的WebSocket的聊天应用

1.定义消息类型

2.定义WebSocket的实例类型

3.定义聊天消息model

4.定义Socket连接、发送消息、发送心跳类

5.定义发布订阅类,用于新消息来了立即发布接收到的消息到相关的页面

6.实现网页打开时,连接服务器;关闭页面时,断开socket连接

7.收到消息后,发送浏览器外的通知




1.YLGroupChatProtocol 是定义的聊天消息类型,不同的ID对应不同的消息

// 通信协议定义
class YLGroupChatProtocol { 
    constructor () {
      this.create_room                = 1001;
      this.send_content               = 1002;
      this.send_qiniu_image           = 1003;
      this.send_file                  = 1004;
      this.newUserBroadcast           = 1005;  // 新用户连接时,广播消息给所有的用户
      this.heartbeat                  = 1006;  // 心跳
    }
}

2.这是 WebSocket 状态 ,按照官方描述的来定义的

WebSocket文档:https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/readyState

class YLWebSocketReadyState {
    constructor () {
        this.CONNECTING    = 0;	// Socket has been created. The connection is not yet open.
        this.OPEN          = 1; // The connection is open and ready to communicate.
        this.CLOSING       = 2;	// The connection is in the process of closing.
        this.CLOSED        = 3;	// The connection is closed or couldn't be opened.
    }
}

3.定义聊天消息消息实体model

class YLChatMessageModel {
    constructor () { 
        this.protocol = "";
        this.roomUUID = "";
        this.sender = "";
        this.content = "";
        this.imagekey = ""; // 图片的七牛云key
        this.uuid = ""; // 消息UUID
        this.users = null; // 用户列表
        this.client_ip = ""; // 用户真实IP
        this.rawProtocol = new YLGroupChatProtocol();
    }

    setData (data) {
        let jsonDict = JSON.parse(data);
        this.protocol = jsonDict["protocol"];
        if (this.protocol == this.rawProtocol.heartbeat) {
            
        } else if (this.protocol == this.rawProtocol.send_content) {
            this.roomUUID = jsonDict["roomUUID"];
            this.client_ip = jsonDict["client_ip"];
            this.sender = jsonDict["sender"];
            this.content = jsonDict["content"];
            this.uuid = jsonDict["uuid"];
        } else if (this.protocol == this.rawProtocol.send_qiniu_image) {
            this.roomUUID = jsonDict["roomUUID"];
            this.client_ip = jsonDict["client_ip"];
            this.sender = jsonDict["sender"];
            this.content = jsonDict["content"];
            this.imagekey = jsonDict["imagekey"];
            this.uuid = jsonDict["uuid"];
        } else if (this.protocol == this.rawProtocol.newUserBroadcast) {
            this.roomUUID = jsonDict["roomUUID"];
            this.client_ip = jsonDict["client_ip"];
            this.users = jsonDict["users"];
        }
    }

}

4.定义Socket连接、发送消息、发送心跳类

'use strict';
// 群组聊天
class YLGroupChatWebSocket {
    constructor () {
        this.current_user = null; // 当前登录用户实例对象
        this._group_ws = null;
        this.heartbeat_enabled = false; // 心跳包是否启用
        var ws_protocol = 'https:' == document.location.protocol ? "wss:": "ws:"; 
        this._roomUUID = "yooulchat2019"; 
        // 测试域名
        let test_domains = [ 
                            "localhost:8080", 
                            "localhost:9000",
                            ];
        let real_domain = "";
        if (test_domains.indexOf(window.location.host) != -1) {
            // 测试环境
            real_domain = "ws://test.yourdomain.com";
        } else {
            // 线上环境
            real_domain = "wss://yourdomain.com";
        } 
        this._group_ws_url = real_domain + "/stream/ws/gchat/" + this._roomUUID;

        // 临时的消息暂存
        this.tmp_message_protocol = null;
        this.tmp_message_content = null;
    }

    // 连接服务器 
    connect_to_server () {
        if (!this.hasLogin()) {
            console.log("请选登录!");
            return;
        }
        this._group_ws = new WebSocket(this._group_ws_url);
        this._group_ws.binaryType = "arraybuffer";
        var that = this;
        this._group_ws.onopen = function (event) {
            console.log("成功连接到服务器!");
            window.YLNotificationMessages.publish(window.YLNotificationMessageType.connected, event);
            // 重新连接上服务器后,把消息发出去 
            if (that.tmp_message_protocol != null && that.tmp_message_content != null) {
                that.send_content(that.tmp_message_protocol, that.tmp_message_content);
                that.tmp_message_protocol = null;
                that.tmp_message_content = null;
            }
            // 新加入连接的用户,发给服务器记录起来
            let rawProtocol = new YLGroupChatProtocol();
            that.send_content(rawProtocol.newUserBroadcast, "");
            // 定期发送心跳包
            that.heartbeat_enabled = true;
            that.send_heartbeat_periodically();
        }
        this._group_ws.onmessage = function (event) {
            // console.log("收到消息了", event)
            let msgModel = new YLChatMessageModel();
            msgModel.setData(event.data);
            if (msgModel.protocol == new YLGroupChatProtocol().heartbeat) {
                return;
            }
            // 发送消息到聊天的页面上显示
            window.YLNotificationMessages.publish(window.YLNotificationMessageType.receive_messages, msgModel);
        }
        this._group_ws.onclose = function (event) {
            console.log("与服务器已断开连接。", event);
            this.heartbeat_enabled = false;
            window.YLNotificationMessages.publish(window.YLNotificationMessageType.disconnected, event);
            if (that.hasLogin() && window.location.pathname.indexOf("/chatbox") > -1) {
                console.log("正在重新连接..."); 
                that.connect_to_server();
            }
        }
        this._group_ws.onerror = function (err) {
            console.log(err);
            this.heartbeat_enabled = false;
            window.YLNotificationMessages.publish(window.YLNotificationMessageType.socket_error, err);
        } 
    }

    // 发送数据到服务器
    send_content (protocolEnumVal, content, imagekey="") { 
        if (window.location.pathname.indexOf("/chatbox") == -1) {
            return;
        }
        if (!this.hasLogin()) {
            console.log("请选登录!");
            return;
        }
        if (this._group_ws == null) {
            this.connect_to_server();
            // 暂存发送的消息,待重新连接上服务器后,再把消息发出去
            this.tmp_message_protocol = protocolEnumVal;
            this.tmp_message_content = content; 
            return;
        }
        if ((this._group_ws.readyState == window.YLWebSocketReadyState.CLOSING || 
            this._group_ws.readyState == window.YLWebSocketReadyState.CLOSED)) {
            if (confirm("您与全球用户聊天已失去连接,是否自动连接?")) {
                this.connect_to_server();
                // 暂存发送的消息,待重新连接上服务器后,再把消息发出去
                this.tmp_message_protocol = protocolEnumVal;
                this.tmp_message_content = content; 
            }
            return;
        }  
        // 当前登录用户的User对象(游客)
        var token = window.localStorage.getItem("token");
        if (token == undefined || token == null) { 
            token = window.sessionStorage.getItem("token_guest");
        }
        // 当前登录用户的User对象
        var yooulUserStr = window.localStorage.getItem("$Yoouluser");
        if (yooulUserStr == undefined || yooulUserStr == null) {
            yooulUserStr = window.sessionStorage.getItem("$Yooulguest");
        }
        let data = {
            "protocol" : protocolEnumVal,
            "roomUUID" : this._roomUUID,
            "token": token,
            "content" : content,
            "sender": JSON.parse(yooulUserStr)
        }
        let rawProtocol = new YLGroupChatProtocol();
        if (protocolEnumVal == rawProtocol.send_qiniu_image && imagekey.length > 0) {
            data["imagekey"] = imagekey;
        }
        let jsonData = JSON.stringify(data)
        this._group_ws.send(jsonData);
    }

    // 关闭WebSocket链接
    close_connection () {
        if (this._group_ws != null) {
            this._group_ws.close();
            this._group_ws = null;
        }
    }

    // 间隔30秒发送心跳包
    send_heartbeat_periodically() {
        // 当前登录用户的User对象
        let self = this;
        var myHeartBeatInterval = window.setInterval(() => {
            if (!this.heartbeat_enabled) {
                // 关闭计时器
                window.clearInterval(myHeartBeatInterval);
                return;
            }
            let data = {"protocol" : new YLGroupChatProtocol().heartbeat} 
            let jsonData = JSON.stringify(data)
            self._group_ws.send(jsonData);
        }, 30 * 1000);
    }

    getUUID () {
        var d = new Date().getTime();
        if (window.performance && typeof window.performance.now === "function") {
            d += performance.now(); //use high-precision timer if available
        }
        var uuid = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
            var r = (d + Math.random() * 16) % 16 | 0;
            d = Math.floor(d / 16);
            return (c == 'x' ? r : (r & 0x3 | 0x8)).toString(16);
        });
        return uuid; 
    }

    hasLogin () {
        var token = "Your token";
        if (token != null) {
            return true;
        }
        return false;
    }
}


// 定义通知类型
class YLNotificationMessageType { 
    constructor () {
        this.socket_error            = -1; // socket发生错误
        this.connected               = 1;  // 已连接到服务器
        this.reconnect               = 2;  // 重新连接服务器
        this.receive_messages        = 3;  // 接收消息
        this.disconnected            = 4;  // 客户端已掉线
    }
}

5.定义发布订阅类,用于新消息来了立即发布接收到的消息到相关的页面

// 消息通知
// 发布/订阅模式
class YLNotificationMessages {
    constructor () {
        // 事件对象:存放事件的订阅名字和回调
        this.events = {};
    }
    // 订阅事件
    subscribe (eventName, callback) { 
        if (!this.events[eventName]) {
            // 一个名称可以有多个订阅回调事件,所以使用数组存储回调
            this.events[eventName] = [callback];
        } else {
            // 如果该事件名称存在,则继续往该名称添加回调事件
            this.events[eventName].push(callback);
        }
    }
    // 发布事件
    publish (eventName, ...args) {
        this.events[eventName] && this.events[eventName].forEach(cb => cb(...args));
    }
    // 取消订阅事件
    unsubscribe (eventName, callback) { 
        if (this.events[eventName]) {
            // 找到该回调,并移除它  
            // this.events[eventName].filter(cb => cb != callback); // 不管用
            var _events = [];
            for (var i = 0; i < this.events[eventName].length; i++) {
                if (this.events[eventName][i].toString()!=callback.toString()) {
                    _events.push(this.events[eventName][i]);
                }
            }
            this.events[eventName] = _events;
        }
    }
    // 取消订阅所有事件
    unsubscribeAll(eventName) {
      if (this.events[eventName]) {
        this.events[eventName] = [];
      }
    }
    
}

// 注册到Window对象中
window["YLNotificationMessageType"] = new YLNotificationMessageType();
window["YLNotificationMessages"] = new YLNotificationMessages();
window["YLGroupChatProtocol"] = new YLGroupChatProtocol();
window["YLWebSocketReadyState"] = new YLWebSocketReadyState();
window["YLGroupChatWebSocket"] = new YLGroupChatWebSocket();

6.实现网页打开时,连接服务器;关闭页面时,断开socket连接

// 当页面关闭或强制刷新时,主动去关闭websocket连接,防止连接还没断开就关闭窗口,server端会抛异常
window.onbeforeunload = function() {
    console.log("客户端连接已关闭"); 
    if (isUserLoginWhenConnectingToWSServer()) {
        window.YLGroupChatWebSocket.close_connection();

        // 取消订阅事件,取消接收聊天消息
        window.YLNotificationMessages.unsubscribe(window.YLNotificationMessageType.receive_messages, chatReceiveMessagesCallback);
    }
} 

window.onload = function () { 
    grantNotification();

    // 仅打开聊天界面时才开启WebSocket聊天
    // 等待页面加载完后再去连接WebSocket服务 
    if (isUserLoginWhenConnectingToWSServer()) {
        window.YLGroupChatWebSocket.connect_to_server()

        // 订阅事件,接收聊天消息
        window.YLNotificationMessages.subscribe(window.YLNotificationMessageType.receive_messages, chatReceiveMessagesCallback);
    }
}

// 连接WebSocket服务器时判断一下登录状态
function isUserLoginWhenConnectingToWSServer() {
    var token = "You token";
    if (token != null) {
        return true;
    }
    return false;
}

// 订阅事件,接收聊天消息回调
function chatReceiveMessagesCallback(data) {
    let rawProtocol = new YLGroupChatProtocol();
    var yooulUserStr = window.localStorage.getItem("$Yoouluser");
    let yooulUser = JSON.parse(yooulUserStr);
    if (yooulUser.user_name == data.sender.user_name && yooulUser.user_id == data.sender.user_id) { 
        // 如果是自己的消息,则不通知
    } else if (data.protocol == rawProtocol.send_content || data.protocol == rawProtocol.send_qiniu_image) {
        if (window.location.pathname == "/chatbox") {
            // 当用户在聊天页面时,不显示通知
            return;
        }
        // 只通知收到别人发的消息
        showBrowserNotification(data.sender.user_avatar, data.content);
    }
}

7.收到消息后,发送浏览器外的通知

// 显示浏览器通知
function showBrowserNotification(msgAvatar, msgContent) {
    console.log("用户设定的通知状态是:", Notification.permission);
    if (Notification.permission !== 'granted') {
        Notification.requestPermission();
    } else {
        var notification = new Notification('有人给你发送了消息', {
            icon: msgAvatar,
            body: msgContent,
        });
        notification.onclick = function() {
            window.open('/chatbox');
        };
    }
}

// 授权显示通知
function grantNotification() {
    if (Notification.permission !== 'granted') {
        Notification.requestPermission();
    }
}

完整代码:
https://gist.github.com/VictorZhang2014/3811b38aea41390039cbbe79d9dad0a3