超級簡單的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