消息存儲系統
聊天室設計,消息存儲系統非常關鍵,因為一開始設計時使用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> 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,此方法作用是創建并保存消息到數據庫。
邏輯流程:
- 接收參數: senderId , username , content , mentions , messageType , attachments , roomId
- 生成消息ID和當前時間戳
- 創建新的Message實例
- 調用DBService.insertMany保存到數據庫
- 處理保存結果:
- 成功:返回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.io | HTTP路由 |
---|---|---|
延遲 | 毫秒級(長連接即時傳輸) | 高(每次新建TCP連接) |
效率 | 無HTTP頭開銷 | 每個請求攜帶完整HTTP頭 |
方向性 | 雙向通信 | 單向請求 |
實時反饋 | 可立即收到已送達/已讀回執 | 需額外輪詢或長連接 |
連接狀態 | 持久連接感知在線狀態 | 無狀態 |
適用場景 | 高頻、實時、小數據包交互 | 低頻、非實時、大數據傳輸 |
后端核心模塊設計
node后端創建一個socketHandler.js文件,是后端Socket.IO的核心控制器,負責處理WebSocket連接、消息收發、房間管理和用戶認證。
- 連接管理
- 使用 socketAuthMiddleware 進行用戶認證,驗證 JWT token
- 維護 onlineUsers Map 存儲用戶ID與socket ID的映射關系
- 處理連接斷開事件,清理相關資源
- 房間管理
- 通過join-room 事件處理用戶加入房間邏輯
- 使用 updateRoomMembers 函數維護房間成員列表
- 存儲在 chatRooms Map 中
- 消息處理
- send-message 事件處理消息收發
- 調用 saveMessage 函數將消息存入數據庫
- 使用 IO.to(roomId).emit 向房間內所有用戶廣播消息
- 心跳檢測
- 30秒檢測一次心跳 ( heartBeat_interval )
- 通過 resetHeartbeat 函數重置心跳計時器
- 超時未收到心跳則斷開連接
- 錯誤處理
- 對關鍵操作進行 try-catch 錯誤捕獲
- 向客戶端發送錯誤信息
前端實現方案
前端通信流程如下圖所示:
Socket工具類
前端創建一個socketUtils.js,這是一個封裝了Socket.IO客戶端功能的工具類
- 連接管理
- 使用 io() 建立WebSocket連接
- 支持攜帶token認證
- 配置了重連策略和超時設置
- 心跳機制
- 每20秒發送一次心跳包( heartbeat 事件)
- 監聽服務端響應( pong 事件)
- 斷開連接時清除定時器
- 事件處理
- 提供 emitEvent 方法發送事件
- 提供 onEvent / offEvent 方法監聽/取消事件
- 暴露 **disconnect **方法主動斷開連接
- 錯誤處理
- 服務器主動斷開時清除本地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});
關鍵實現細節
- 房間管理優化:采用動態房間加入機制,用戶只加入當前活躍的聊天房間,減少不必要的消息廣播。
- 消息持久化:在廣播消息前先存入數據庫,確保消息可靠性,即使連接中斷也不會丟失。
- 斷線重連:前端配置了5次重試機制,配合后端的在線狀態檢測,實現無縫重新連接。
- 資源清理:組件卸載時自動取消事件監聽,防止內存泄漏。
對于需要實時功能的現代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();