超級簡單的WebSocket的聊天應用

超級簡單的WebSocket的聊天應用

1.定義消息類型

2.定義WebSocket的實例類型

3.定義聊天消息model

4.定義Socket連接、發送消息、發送心跳類

5.定義發佈訂閱類,用於新消息來了立即發佈接收到的消息到相關的頁面

6.實現網頁打開時,連接服務器;關閉頁面時,斷開socket連接

7.收到消息後,發送瀏覽器外的通知

效果預覽:https://web.yooul.com/chatbox



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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章