從零打造前沿Web聊天室:消息系統

消息存儲系統

聊天室設計,消息存儲系統非常關鍵,因為一開始設計時使用MongoDB,所以后續使用schemma方式存儲。

后端架構:express + MongoDB

消息插入策略

MongoDB 中設計聊天消息存儲時,插入策略的選擇會影響性能、擴展性和查詢效率。一般消息插入會按照單條消息插入群組插入,這里我選擇使用單條消息插入,理由在下方的比較當中。

對插入方式進行比較:

1. 單條消息獨立插入

每條消息作為獨立文檔插入 messages 集合

優點:

  • 查詢靈活:可以輕松實現各種查詢需求(按群組、按用戶、按時間范圍等)
  • 擴展性強:天然支持分片(按 group_id 分片效果最佳)
  • 寫入性能高:MongoDB 對單文檔插入做了高度優化
  • 維護簡單:消息更新/刪除只需操作單條文檔
  • 存儲效率:未讀消息標記等狀態管理更方便

缺點:

  • 大量消息時集合文檔數增長快(但MongoDB 單集合支持千萬級文檔無壓力)
  • 獲取群組完整聊天記錄需要查詢多個文檔(可通過索引優化)

2. 按群組聚合插入

每個聊天室(群組)作為一個文檔,消息以子文檔數組形式存儲

優點:

  • 獲取某個群組所有消息只需讀取一個文檔
  • 理論上減少文檔總數

缺點:

  • 文檔大小限制:MongoDB 單文檔最大16MB,高頻聊天群容易超出限制
  • 寫入沖突:高并發時對同一群組文檔的更新會產生鎖競爭
  • 擴展困難:無法有效分片,性能隨群組活躍度下降明顯
  • 查詢局限:難以實現跨群組查詢、全文搜索等復雜需求
  • 更新低效:修改/刪除單條消息需要定位到數組元素

這兩種插入方式優缺點都很明顯,但是對于絕大多數生產級聊天應用,推薦使用單條消息獨立插入的方式,只有在非常特殊的小規模、低頻率場景下,才考慮使用按群組聚合的插入方式。雖然表面上看起來會產生更多文檔,但 MongoDB 對這種模式有最好的支持。

并且對于單條消息獨立插入也可以進行優化,使用索引分片策略數據分頁等方式優化性能和效率。

單條消息插入的優化策略

1. 批量插入代替單條插入

使用insertMany代替insertOne的方式,批量插入的消息效率比單條插入更高

const messages = [...];
db.messages.insertMany(messages, { ordered: false }); // ordered:false 忽略錯誤繼續插入
2. 索引優化技巧

使用覆蓋索引優化常見查詢,覆蓋索引查詢的關鍵在于使用投影條件,只返回查詢結果所需字段,從而避免對實際文檔的訪問。

  • 所有的查詢字段是索引的一部分
  • 所有的查詢返回字段在同一個索引中

MongoDB 有一個字段索引的特定應用程序,稱為覆蓋索引查詢(Covered Queries),其中查詢的所有列都被進行索引。
通過創建適當的索引,使查詢可以直接從索引中獲取所需的數據,而無需訪問實際的文檔數據,減少磁盤 I/O 和內存消耗,提高查詢性能

使用部分索引減少索引大小,僅索引集合中符合指定過濾器表達式的文檔

3. 分片策略

可以進行按聊天室(群組)分片,可以確保同一群組的消息物理上存儲在相同分片上,同時保持集群負載均衡。


消息接口

消息模型

接下來設計node + express的路由接口配置,先準備消息的模型messageSchema

/*** 消息模型* @typedef {Object} Message* @property {string} messageId - 消息ID 必填字符串類型字段,必須唯一* @property {string} roomId - 聊天室ID 必填字符串類型字段,必須唯一* @property {string} senderId - 消息發送者ID 必填字符串類型字段,必須唯一* @property {string} username - 用戶名 必填字符串類型字段,必須唯一* @property {Date} time - 消息發送時間* @property {string} content - 消息內容* @property {Array} mentions - 消息中被@的用戶信息數組* @property {boolean} deleted - 是否刪除* @property {string} deletedBy - 刪除者ID* @property {Date} deletedAt - 刪除時間* @property {string} messageType - 消息類型,默認為 'text'* @property { Array } attachments - 附件信息數組,如圖片、文件等* @property { string } attachments.type - 附件類型,如 'image', 'file'* @property { string } attachments.url - 附件URL* @property { string } attachments.name - 附件名稱*/
const messageSchema = new Schema({messageId: { type: String, required: true, unique: true },roomId: { type: String, required: true },senderId: { type: String, required: true },username: { type: String, required: true },time: { type: Date, default: Date.now },content: { type: String, required: true },mentions: [{username: String,  // 被@的用戶名position: Number  // 在內容中的位置}],deleted: { type: Boolean, default: false },  // 是否刪除deletedBy: { type: String, default: '' },  // 刪除者IDdeletedAt: { type: Date, default: '' },  // 刪除時間messageType: { type: String, enum: ['text', 'image', 'file'], default: 'text' },  // 消息類型attachments: [{  // 附件信息數組,如圖片、文件等type: String,  // 附件類型,如 'image', 'file'url: String,  // 附件URLname: String  // 附件名稱}],
});

路由接口

提前定義一些將需要用到的路由接口,創建在routes文件夾中,引入controller控制器和token檢測中間件,在所有路由處理前添加 authMiddleware 進行身份驗證,確保只有攜帶有效 token 的用戶才能訪問消息相關接口。

關于token檢測中間件相關可以看:JWTの求生記錄 🌟 JWT這玩意兒就像你對象的聊天記錄——看不懂但必須得會驗證,不然API分分鐘給你返回401,來! - 掘金

在這里插入圖片描述

然后在主路由文件中掛載消息路由處理

// 掛載chatRoom文件夾中的消息路由
router.use('/chatRoom', messageRouter);

創建對應的消息處理控制器messageController.js,將之前所需要的方法都進行創建,下面我將以發送消息sendMessage為例進行講解。

messageController控制器

controller創建sendMessage方法,從請求體獲取參數: senderId , username , content , roomId

調用 handleMessageContent 處理消息內容,返回 mentions , messageType , attachments,這里是為了將消息內容中的@用戶信息提取出來,存入mentions數組中,因為不額外處理圖片邏輯了,所以messageType , attachments都是默認值,使用正則表達式提取@用戶信息。

最后,用 messageService.createMessage 保存消息到數據庫,返回相應的處理結果給前端

sendMessage代碼:

/*** 發送消息* @param {Object} req - 請求對象* @param {Object} res - 響應對象* @returns {Promise<void>} - 返回一個Promise對象,解析為void*/
const sendMessage = async (req, res) => {try {// 獲取請求參數const { senderId, username, content, roomId } = req.body;// 處理content中的@用戶信息,提取出用戶名,存入mentions數組中let { mentions, messageType, attachments } = handleMessageContent(content);// 調用服務層方法,創建消息并保存到數據庫中,返回保存后的消息對象,包括result, message, data, code等屬性let getResult = await messageService.createMessage(senderId, username, content, mentions, messageType, attachments, roomId);return res.status(getResult.code).json(getResult);} catch (error) {return res.status(500).json({ status: 'failure', code: 500, message: '發送消息失敗' });}
}

handleMessageContent 代碼:

const handleMessageContent = (content) => {let { html } = content;let mentions = [], attachments = [], messageType = 'text'; // 用于存儲@用戶信息的數組// "<span class="insert-mention" contenteditable="false" style="color: rgb(0, 123, 255);">@李四</span>&nbsp;2324"// 正則表達式匹配@用戶信息的格式const mentionRegex = /<span class="insert-mention" contenteditable="false"[^>]*>@([^<]+)<\/span>/g;// 遍歷消息內容中的所有@用戶信息,提取出用戶名,存入mentions數組中const mentionMatches = html.match(mentionRegex);if (mentionMatches) {mentionMatches.forEach((mention) => {// 提取出用戶名,存入mentions數組中let username = mention.match(/@([^<]+)/)[1]; // 使用正則表達式提取出用戶名mentions.push({ username }); // 存入mentions數組中,同時記錄位置信息});}return { mentions, messageType, attachments };
}

在一開始的時候還是寫了圖片處理,但是為了加快速度省去了這部分內容

  • 檢測base64圖片數據
  • 解碼并保存圖片到本地
  • 生成圖片信息對象存入attachments數組
  • 設置messageType為’image’
if (images && images.length > 0) { // 如果有圖片,將圖片信息存入attachments數組中attachments = images.map((image) => { // 遍歷圖片數組,將圖片信息存入attachments數組中// 對base64圖片數據進行特殊處理try { if (image.data.startsWith('data:image/')) { // 如果是base64圖片數據,將其存入attachments數組中,同時記錄圖片類型和urllet base64Data = image.data.split(',')[1]; // 提取出base64圖片數據let imageType = image.data.split(';')[0].split('/')[1]; // 提取出圖片類型,如jpg、png等let imageName = `${Date.now()}.${imageType}`; // 生成圖片名稱,使用當前時間戳作為文件名,加上圖片類型作為后綴名let imagePath = `E:/工作文件04/New_Work/images/${imageName}`; // 生成圖片路徑,使用images文件夾作為存放圖片的目錄// 將圖片數據寫入到指定路徑中,使用fs模塊的writeFileSync方法,將base64圖片數據解碼后寫入到指定路徑中,使用base64解碼方法decodeBase64fs.writeFileSync(imagePath, decodeBase64(base64Data), 'base64'); // 將base64圖片數據解碼后寫入到指定路徑中,使用base64解碼方法decodeBase64return { type: 'image', url: imagePath, name: imageName }; // 存入圖片信息,包括類型、url和名稱}} catch (error) { // 如果處理過程中出現錯誤,打印錯誤信息,并返回nullconsole.error('處理圖片數據失敗:', error); // 打印錯誤信息return null; // 返回null,表示處理失敗}})messageType = 'image'; // 將消息類型設置為image
}
messageService服務層

創建一個messageService.js文件進行業務邏輯處理,創建消息方法createMessage,此方法作用是創建并保存消息到數據庫。

邏輯流程:

  1. 接收參數: senderId , username , content , mentions , messageType , attachments , roomId
  2. 生成消息ID和當前時間戳
  3. 創建新的Message實例
  4. 調用DBService.insertMany保存到數據庫
  5. 處理保存結果:
    • 成功:返回200狀態碼和消息數據
    • 失敗:返回400狀態碼和錯誤信息
const createMessage = async (senderId, username, content, mentions, messageType, attachments, roomId) => {try {let messageId = generateId(8);let time = new Date(); // 獲取當前時間作為消息創建時間const newMessage = new Message({ messageId, roomId, senderId, username, time, content, mentions, messageType, attachments }); // 保存到數據庫let result = await DBService.insertMany('Message', newMessage); // 保存到數據庫if (result.success) { // 如果保存成功,返回保存后的消息對象return { result: 'success', message: '保存消息成功', data: result.data, code: 200 }; // 返回保存后的消息對象} else { // 如果保存失敗,返回錯誤信息return { result:'failure', message: '保存消息失敗', data: null, code: 400 }; }} catch (error) {console.error('保存消息失敗:', error); // 打印錯誤信息}
}

這樣基本的數據邏輯操作就完成了,在前端頁面中發送聊天消息進行驗證吧

在這里插入圖片描述


這里再按之前的方式,編寫一個根據roomId列表獲取每個聊天室最新消息的get方法,從query中獲取roomIdList,檢查 roomIdList 是否為空,如果為空則返回400錯誤。設置查詢參數:

  • limit: 1 只獲取最后一條消息
  • sort: { time: -1 } 按時間降序排序

調用服務層的 getMessagesByRoomIdList 方法執行實際查詢,這里本來是將id數組一個一個去進行查詢,使用Promise.all獲取所有結果,但是此種方法需要訪問數據庫的次數會過多,對性能不太好。

代碼如下:

const latestMessagesPromises = roomIdList.map(roomId => model.find({ roomId }).sort({ time: -1 }).limit(1)
);
const latestMessages = await Promise.all(latestMessagesPromises);

或許我發現了聚合查詢,在mongose中可以使用aggregate執行復雜的數據轉換和分析,最終修改完只需要對數據庫執行一次查詢訪問即可。

const getMessagesByRoomIdList = async (roomIdList, limit, sort) => {try {if (typeof roomIdList == "string") { // 如果房間Id列表是字符串,說明只有一個房間Id,需要轉換為數組roomIdList = JSON.parse(roomIdList.replace(/'/g, '"'));; // 將字符串轉換為數組}// 為每個房間ID查詢最新一條消息,使用聚合查詢const aggregateQuery = [{$match: { roomId: { $in: roomIdList } } // 匹配房間ID在列表中的消息}, {$sort: sort // 按時間降序排序}, {$group: { // 按房間ID分組_id: '$roomId', // 分組字段為房間IDmessage: { $first: '$$ROOT' } // 獲取每個分組中的第一條消息}}, {$project: { // 投影字段,只返回消息內容和時間content: '$message.content', // 返回消息內容time: '$message.time', // 返回消息時間username: '$message.username', // 消息發送者用戶名messageType: '$message.messageType', // 消息類型}}]; let result = await DBService.aggregate('Message', aggregateQuery); // 查詢消息列表if (result.success) { // 如果查詢成功,返回消息列表數組return { result:'success', message: '獲取消息列表成功', data: result.data, code: 200 }; // 返回消息列表數組} else { // 如果查詢失敗,返回錯誤信息return { result:'failure', message: '獲取消息列表失敗', data: null, code: 400 }; // 返回錯誤信息} } catch (error) {console.error('獲取消息列表失敗:', error); // 打印錯誤信息}
}

這里其他的查詢方法也是類似按自己的需求寫完了,現在在前端登錄后界面上就能夠獲取到之前發送到數據庫中的消息列表了

在這里插入圖片描述


WebSocket消息通信

在構建實時通信功能時,我選擇了Socket.IO庫作為解決方案。這并非首次嘗試,此前我曾基于Socket.IO開發過一個微信風格的聊天界面:SocketIO の 聊天練習基于socketIO的雙向通信,準備制作一個聊天界面。聊天界面的大體樣式參考于微信界面,后 - 掘金

再次選擇Socket.IO的主要原因很實際:它提供了高層抽象,使開發者無需處理底層細節,能夠專注于業務邏輯實現 😋

安裝過程非常簡單:

  • 后端安裝命令:yarn add socket.io
  • 前端安裝命令:yarn add socket.io-client

實現效果:

在這里插入圖片描述

1. 基礎配置

后端采用Express框架,通過以下代碼初始化Socket.IO服務:

const http = require('http');
const { Server } = require('socket.io');
// 引入Socket.IO處理器
const socketHandler = require('./socketHandler');const server = express();
const httpServer = http.createServer(server);
const IO = new Server(httpServer, { cors: { origin: '*' } }); // 允許所有來源的跨域請求
// 初始化Socket.IO處理
socketHandler(IO);

這里同樣也可以寫發送消息事件,并且比之前通過HTTP路由的方式更優,獲取消息列表推薦使用HTTP路由

2. 通信協議對比

HTTP路由socketIO對比:

特性Socket.ioHTTP路由
延遲毫秒級(長連接即時傳輸)高(每次新建TCP連接)
效率無HTTP頭開銷每個請求攜帶完整HTTP頭
方向性雙向通信單向請求
實時反饋可立即收到已送達/已讀回執需額外輪詢或長連接
連接狀態持久連接感知在線狀態無狀態
適用場景高頻、實時、小數據包交互低頻、非實時、大數據傳輸

后端核心模塊設計

node后端創建一個socketHandler.js文件,是后端Socket.IO的核心控制器,負責處理WebSocket連接、消息收發、房間管理和用戶認證。

  1. 連接管理
    • 使用 socketAuthMiddleware 進行用戶認證,驗證 JWT token
    • 維護 onlineUsers Map 存儲用戶ID與socket ID的映射關系
    • 處理連接斷開事件,清理相關資源
  2. 房間管理
    • 通過join-room 事件處理用戶加入房間邏輯
    • 使用 updateRoomMembers 函數維護房間成員列表
    • 存儲在 chatRooms Map
  3. 消息處理
    • send-message 事件處理消息收發
    • 調用 saveMessage 函數將消息存入數據庫
    • 使用 IO.to(roomId).emit 向房間內所有用戶廣播消息
  4. 心跳檢測
    • 30秒檢測一次心跳 ( heartBeat_interval )
    • 通過 resetHeartbeat 函數重置心跳計時器
    • 超時未收到心跳則斷開連接
  5. 錯誤處理
    • 對關鍵操作進行 try-catch 錯誤捕獲
    • 向客戶端發送錯誤信息

前端實現方案

前端通信流程如下圖所示:

在這里插入圖片描述

Socket工具類

前端創建一個socketUtils.js,這是一個封裝了Socket.IO客戶端功能的工具類

  1. 連接管理
    • 使用 io() 建立WebSocket連接
    • 支持攜帶token認證
    • 配置了重連策略和超時設置
  2. 心跳機制
    • 每20秒發送一次心跳包( heartbeat 事件)
    • 監聽服務端響應( pong 事件)
    • 斷開連接時清除定時器
  3. 事件處理
    • 提供 emitEvent 方法發送事件
    • 提供 onEvent / offEvent 方法監聽/取消事件
    • 暴露 **disconnect **方法主動斷開連接
  4. 錯誤處理
    • 服務器主動斷開時清除本地token
    • 顯示斷開提示并跳轉登錄頁

該實現采用單例模式導出,確保全局只有一個Socket實例

此時,在前端文件當中,可以在main.js引入創建的socket實例

import socket from '@/utils/socketUtils';// 初始化socket
app.provide('socket', socket);

通過inject(‘socket’) 獲取Socket.IO實例,在業務場景當中主要使用聊天消息收發房間管理

// 發送消息
socket.emitEvent('send-message', body);
// 接收消息
socket.onEvent('room update message', updateMessage);
// 加入房間
socket.emitEvent('join-room', {roomId, userId});

關鍵實現細節

  1. 房間管理優化:采用動態房間加入機制,用戶只加入當前活躍的聊天房間,減少不必要的消息廣播。
  2. 消息持久化:在廣播消息前先存入數據庫,確保消息可靠性,即使連接中斷也不會丟失。
  3. 斷線重連:前端配置了5次重試機制,配合后端的在線狀態檢測,實現無縫重新連接。
  4. 資源清理:組件卸載時自動取消事件監聽,防止內存泄漏。

對于需要實時功能的現代Web應用,Socket.IO仍然是值得推薦的解決方案。后續可以考慮加入消息已讀狀態、輸入指示器等增強功能,進一步提升用戶體驗。


socketHandler.js實現代碼:

const { verifyToken } = require('../services/tokenService');
const messageService = require('../services/messageService'); // 引入服務層
const { handleMessageContent } = require('../utils/dataDeal.js'); // 引入工具函數const onlineUsers = new Map(); // 存儲用戶ID與socket ID的映射
// 存儲聊天群信息
const chatRooms = new Map();const heartBeat_interval = 30000; // 30秒檢測一次
let heartbeatTimer;
/*** socket連接*/
const socketHandler = (IO) => {// 連接處理IO.on('connection', (socket) => {// 判斷連接的token是否合法,不合法斷開連接socketAuthMiddleware(socket);// 監聽用戶加入聊天室事件socket.on('join-room', async (msg) => {try {console.log('用戶加入房間:', msg); // 打印用戶加入房間的信息socket.join(msg.roomId); // 加入房間let roomIds = onlineUsers.get(msg.userId)?.roomId; // 獲取用戶的房間ID列表if (!roomIds.includes(msg.roomId)) {onlineUsers.get(msg.userId)?.roomId.push(msg.roomId); // 將房間ID添加到用戶的房間ID列表中}updateRoomMembers(msg.roomId, socket); // 更新房間成員列表} catch (error) { // 捕獲異常console.error('加入房間失敗:', error); // 打印錯誤信息}})// 監聽客戶端消息socket.on('send-message', async (message) => {try {let userId = message.senderId, roomId = message.roomId; // 獲取用戶IDif (!userId) { // 如果沒有用戶ID,返回錯誤信息console.log('沒有用戶ID,無法發送消息'); // 打印錯誤信息return; // 返回錯誤信息}// 在這里處理消息,例如保存到數據庫或進行其他操作let saveResult = await saveMessage(message, socket);if (saveResult) { // 保存成功,向房間內的所有用戶發送消息// 向房間內的其他用戶發送消息IO.to(roomId).emit('room update message', {type: 'ohter_message', // 消息類型userId: userId, // 用戶IDmessage: saveResult // 消息內容}); }} catch (error) {console.error('處理消息時出錯:', error);let userId = message.senderId; // 獲取用戶IDconst toSocketId = onlineUsers.get(userId); // 獲取用戶的socket IDif (toSocketId) { // 如果有socket ID,向用戶發送錯誤信息socket.to(toSocketId).emit('message-error', { message: '處理消息時出錯', tempId: message.senderId }); // 向用戶發送錯誤信息}}});// 斷開連接處理socket.on('disconnect', () => {for (const [userId, socketId] of onlineUsers.entries()) {if (socketId === socket.id) {onlineUsers.delete(userId);console.log(`用戶斷開連接: ${userId}`);// 處理用戶離開房間的邏輯let roomIds = onlineUsers.get(userId)?.roomId; // 獲取用戶的房間ID列表if (roomIds) { // 如果有房間ID列表,遍歷房間ID列表,離開房間roomIds.forEach(roomId => { // 遍歷房間ID列表,離開房間socket.leave(roomId); // 離開房間});updateRoomMembers(roomId, socket); // 更新房間成員列表}break;}}clearInterval(heartbeatTimer); // 清除心跳檢測定時器});// 心跳檢測const resetHeartbeat = () => {clearTimeout(heartbeatTimer);heartbeatTimer = setTimeout(() => {console.log(`心跳超時,斷開連接: ${socket.id}`);socket.disconnect();}, heartBeat_interval);};socket.on('heartbeat', (token) => { // 監聽心跳事件socketAuthMiddleware(socket);resetHeartbeat(); // 重置心跳socket.emit('pong');});});
};/*** 更新聊天室內成員列表*/
const updateRoomMembers = (roomId, socket) => { // 傳入房間ID和成員列表if (!socket || !socket.adapter) {console.error('無效的socket對象或缺少adapter屬性');return;}const room = socket.adapter.rooms.get(roomId);if (room) { // 如果房間存在const members = Array.from(room);console.log(`房間 ${roomId} 成員列表: ${members}`);chatRooms.set(roomId, members); // 更新房間成員列表}
}/*** 驗證 token 的中間件* @param {Object} req - 請求對象* @param {Object} res - 響應對象* @param {Function} next - 下一步函數
*/
const socketAuthMiddleware = (socket) => {const token = socket.handshake.auth.token; // 從客戶端傳遞的token中獲取if (!token) { // 沒有token,不連接socket.disconnect(); // 斷開連接console.log('沒有token,不連接');return;}try {const user = verifyToken(token.replace('Bearer ', '')); // 驗證tokenif (!user) { // 驗證失敗,斷開連接socket.disconnect(); // 斷開連接console.log('token驗證失敗,不連接');return;}// 將用戶信息附加到socket對象socket.user = user;onlineUsers.set(user.userid, { socketId: socket.id, roomId: []}); // 存儲用戶ID與socket ID的映射} catch (error) { // 驗證失敗,斷開連接socket.disconnect(); // 斷開連接return;}
}/*** 存儲消息到數據庫中*/
const saveMessage = async (message) => {try {// 獲取請求參數const { senderId, username, content, roomId } = message;// 處理content中的@用戶信息,提取出用戶名,存入mentions數組中let { mentions, messageType, attachments } = handleMessageContent(content);// 調用服務層方法,創建消息并保存到數據庫中,返回保存后的消息對象,包括result, message, data, code等屬性let getResult = await messageService.createMessage(senderId, username, content, mentions, messageType, attachments, roomId);return getResult.data;} catch (error) {console.error('保存消息時出錯:', error); return null;}
}module.exports = socketHandler;

socketUtils.js實現代碼:

/*** socket連接*/
import { io } from 'socket.io-client';class SocketIOService {socket = null; // 存儲socket實例constructor() { }setupSocketConnection() {let token = localStorage.getItem('token')if (!token) { // 沒有token,不連接console.log('No token found, not connecting to socket')return;}this.socket = io('http://localhost:10086', {auth: {token: token,  // 攜帶認證token},transports: ['websocket'], // 指定傳輸方式reconnection: true, // 自動重連reconnectionAttempts: 5, // 重連嘗試次數reconnectionDelay: 1000, // 重連延遲timeout: 20000 // 連接超時時間})this.socket.on('connect', () => { // 連接成功console.log('Socket connected:', this.socket.id);// 每25秒發送一次心跳this.heartbeatInterval = setInterval(() => {// console.log('發送心跳'); // 打印日志,方便調試this.socket.emit('heartbeat', Date.now());}, 20000);});this.socket.on('pong', () => {console.log('收到服務端心跳響應');});this.socket.on('disconnect', (reason) => {console.log('Socket disconnected:', reason);// 清除心跳定時器clearInterval(this.heartbeatInterval);if (reason === 'io server disconnect') {// 清除無效tokenlocalStorage.removeItem('token');ElMessage.warning('與服務器心跳斷開,重新登錄');// 跳轉到登錄頁setTimeout(() => {window.location.href = '/';}, 1000);// 服務器主動斷開需要手動重連// this.socket.connect();}})}// 發送emitEvent(eventName, data) {if (!this.socket) return; // 如果socket未連接,不發送消息this.socket.emit(eventName, data);}// 監聽onEvent(eventName, callback) {if (!this.socket) return; // 如果socket未連接,不監聽消息this.socket.on(eventName, callback);}// 取消監聽offEvent(eventName, callback) {if (!this.socket) return; // 如果socket未連接,不取消監聽消息this.socket.off(eventName, callback);}// 斷開連接disconnect() {if (!this.socket) returnthis.socket.disconnect()}
}export default new SocketIOService();

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/bicheng/85102.shtml
繁體地址,請注明出處:http://hk.pswp.cn/bicheng/85102.shtml
英文地址,請注明出處:http://en.pswp.cn/bicheng/85102.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

[7-01-03].第03節:環境搭建 - 集群架構

RabbitMQ學習大綱 一、使用集群的原因 1.基于以下原因&#xff0c;需要搭建一個 RabbitMQ 集群來解決實際問題 單機版的&#xff0c;無法滿足目前真實應用的要求。如果 RabbitMQ 服務器遇到內存崩潰、機器掉電或者主板故障等情況&#xff0c;會導致rabbitMQ無法提供服務單臺 R…

【vivado】時序分析之Latch pins with no clock

問題&#xff1a; vivado打開時序報告&#xff0c;如下圖 表示存在鎖存器Latch 解決方法&#xff1a; 查看代碼中是否存在狀態機的狀態沒有寫全&#xff0c;或者default中直接寫了null。

如何將 MX Linux 的垂直任務欄面板移到底部

MX Linux 因其速度和較低的資源消耗&#xff0c;比同類其他 Linux 系統更快地獲得了人氣。它默認帶有 Xfce 桌面環境&#xff0c;但任務欄在左側且是垂直的&#xff0c;這對一部分人來說真的非常不舒服且令人煩惱。如果你也有同感&#xff0c;并且也想將 MX Linux 的任務欄自定…

python debug 監控雙下劃線的變量顯示沒有此變量

名稱改寫&#xff08;Name Mangling&#xff09; 在Python中&#xff0c;如果你在類中定義一個屬性或方法時以雙下劃線開頭&#xff08;例如__attribute&#xff09;&#xff0c;Python會自動對其進行名稱改寫。名稱改寫實際上是在屬性或方法名前加上類名&#xff0c;以避免子…

list使用及模擬

01. list介紹 list是支持常數時間內任意位置插入刪除的序列容器,具備雙向迭代能力。其底層為雙向鏈表結構,各元素存于獨立節點,通過指針指向前后元素。與forward_list的主要區別:后者是單鏈表,僅支持單向迭代,結構更簡單高效。相比array、vector、deque等序列容器,list在…

NLP基礎與詞嵌入:讓AI理解文字(superior哥深度學習系列第13期)

13_NLP基礎與詞嵌入&#xff1a;讓AI理解文字 superior哥深度學習系列第十三篇 從像素到文字&#xff0c;從視覺到語言——讓AI跨越認知的橋梁 &#x1f3af; 前言&#xff1a;當AI學會"讀懂"文字 各位小伙伴們&#xff0c;歡迎來到superior哥深度學習系列的第十三篇…

【時時三省】(C語言基礎)關于變量的聲明和定義

山不在高&#xff0c;有仙則名。水不在深&#xff0c;有龍則靈。 ----CSDN 時時三省 可能有些人弄不清楚定義與聲明有什么區別&#xff0c;它們是否是一回事。有人認為聲明就是定義&#xff0c;有人認為只有賦了值的才是定義。在C語言的學習中&#xff0c;關于定義與聲明這兩個…

Java 時間處理指南:從“踩坑”到“填坑”實戰

&#x1f525;「炎碼工坊」技術彈藥已裝填&#xff01; 點擊關注 → 解鎖工業級干貨【工具實測|項目避坑|源碼燃燒指南】 場景問題&#xff1a;訂單處理系統的時間計算 假設你正在開發一個電商訂單系統&#xff0c;需要解決以下問題&#xff1a; 用戶下單后&#xff0c;需在…

基于Java的Excel列數據提取工具實現

摘要&#xff1a;本文介紹了一個使用Java語言開發的Excel列數據提取工具&#xff0c;該工具借助Apache POI庫實現對Excel文件的讀取與特定列數據提取功能。通過用戶輸入文件路徑與列名&#xff0c;程序可從指定Excel文件中提取相應列的數據并展示&#xff0c;同時詳細闡述了關鍵…

關于人工智能未來的趨勢

學而不思則罔 翻譯&#xff1a;使用深度學習、強化學習卻不用專家系統&#xff0c;就會產生幻覺。 思而不學則殆 翻譯&#xff1a;只有專家系統邏輯推理&#xff0c;但是不用大模型更新知識&#xff0c;就無法發展下去了。 因此&#xff0c;未來智能的范式應該是&#xff1a; …

Java八股文——MySQL「性能調優篇」

MySQL的EXPLAIN有什么作用&#xff1f; 面試官您好&#xff0c;EXPLAIN命令是我在進行SQL性能優化時&#xff0c;使用最頻繁、也最重要的一個工具。 它的核心作用可以一句話概括&#xff1a;模擬MySQL的查詢優化器來執行一條SQL語句&#xff0c;并向我們展示出它最終決定采用…

win打印機共享處理

win打印機共享處理 軟件鏈接 無法啟動Print Spooler服務錯誤193:0xc1的解決方案主要涉及修復服務依賴關系、清理打印緩存及修復系統文件?。該錯誤通常由系統文件損壞、注冊表配置異常或依賴服務未啟動導致&#xff0c;可通過以下步驟系統化解決。?? 解決方法&#xff1a;替換…

C++ map代碼練習 1、2、priority_queue基礎概念、對象創建、數據插入、獲取堆頂、出隊操作、大小操作,自定義結構、代碼練習 1 2

map代碼練習1&#xff0c;對應力扣 兩個數據的交集&#xff0c;代碼見下 class Solution { public:vector<int> intersect(vector<int>& nums1, vector<int>& nums2) {map<int, int> cnt;vector<int> ans;for(int i0; i<nums1.size(…

三天沖刺《編譯原理》——筆記(一)

點關注不迷路喲。你的點贊、收藏&#xff0c;一鍵三連&#xff0c;是我持續更新的動力喲&#xff01;&#xff01;&#xff01; 持續關注我~~~主頁&#xff0c;查看更多內容喲&#xff08;希望你能在這里有所收獲&#x1f92d;&#xff09;。點關注&#xff0c;不迷路&#xf…

代理模式Proxy Pattern

模式定義 給某一個對象提供一個代理&#xff0c;并由代理對象控制對原對象的引用 對象結構型模式 模式結構 Subject&#xff1a;抽象主題角色Proxy&#xff1a;代理主題角色RealSubject&#xff1a;真實主題角色 代理類實現代碼 public class Proxy implements Subject {p…

基于YOLOv11與單目測距的實戰教程:從目標檢測到距離估算

引言 在計算機視覺領域&#xff0c;目標檢測與距離估算的結合是自動駕駛、機器人導航等場景的關鍵技術。本文將以YOLOv8模型為核心&#xff0c;結合單目相機的幾何模型&#xff0c;實現對視頻中目標的實時檢測與距離估算。代碼參考自單目測距原理博客&#xff0c;并通過實踐驗…

代碼生成器使用原理以及使用方法

代碼生成器使用原理以及使用方法 版本號&#xff1a;1.0 二Ο二五年二月 目錄 文檔介紹 1.1編寫目的 1.2文檔范圍 1.3讀者對象 系統設計 2.1設計目標 2.2設計思路 2.3代碼實現原理 使用方法 3.1如何使用 3.2如何修改&#xff1f; 對原程序的bug修改及簡…

STM32標準庫-I2C通信

文章目錄 一、I2C通信1.1 I2C1.2硬件電路1.3I2C時序基本單元1.4I2C時序 二、MPU60502.1簡介2.2MPU6050參數2.3硬件電路2.4MPU6050框圖 三、I2C外設(硬件)3.1簡介3.2I2C框圖3.3I2C基本結構3.4主機發送3.5主機接收3.6軟件/硬件波形對比1. 時序精度2. 信號穩定性3. 速率與效率4. 波…

使用 Azure LLM Functions 與 Elasticsearch 構建更智能的查詢體驗

作者&#xff1a;來自 Elastic Jonathan Simon 及 James Williams 試用這個示例房地產搜索應用&#xff0c;它結合了 Azure Gen AI LLM Functions 與 Elasticsearch&#xff0c;提供靈活的混合搜索結果。在 GitHub Codespaces 中查看逐步配置和運行該示例應用的方法。 更多閱讀…

模糊查詢 的深度技術解析

以下是 模糊查詢 的深度技術解析&#xff0c;涵蓋核心語法、通配符策略、性能優化及實戰陷阱&#xff1a; &#x1f50d; 一、核心運算符&#xff1a;LIKE SELECT * FROM 表名 WHERE 列名 LIKE 模式字符串;&#x1f3af; 二、通配符詳解 通配符作用示例匹配案例%任意長度字符…