Electron + IM
模块一:Electron 主进程/渲染进程通信
一、核心原理
Electron 基于 Chromium + Node.js 架构,采用多进程模型,这是Electron开发的核心基石:
- 主进程(Main Process):整个应用的入口,全局唯一。负责管理所有窗口、生命周期、系统原生API调用、Node.js全能力访问,相当于应用的「大脑」。
- 渲染进程(Renderer Process):每个BrowserWindow对应一个独立渲染进程,负责渲染Web页面、运行前端代码,基于Chromium渲染引擎,默认无法直接访问Node.js API(安全隔离)。
- 预加载脚本(Preload Script):运行在渲染进程的隔离上下文里,是唯一能同时接触「Web API」和「Node.js API」的桥梁,通过
contextBridge把受控的API暴露给渲染进程,是Electron官方推荐的安全通信方案。
- IPC通信核心:基于
ipcMain(主进程监听)和ipcRenderer(渲染进程发送)实现进程间通信,核心分为3种模式:
- 单向通信:渲染→主(send/on)、主→渲染(send/on)
- 双向调用:渲染调用主进程方法并获取返回值(invoke/handle,最常用)
- 广播通信:主进程给所有渲染进程群发消息
二、完整可运行示例(符合Electron 25+ 安全规范)
1. 主进程入口 main.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63
| const { app, BrowserWindow, ipcMain, dialog } = require('electron') const path = require('path')
let mainWindow
function createWindow() { mainWindow = new BrowserWindow({ width: 1200, height: 800, webPreferences: { contextIsolation: true, nodeIntegration: false, preload: path.join(__dirname, 'preload.js') } })
mainWindow.loadFile('index.html') mainWindow.webContents.openDevTools() }
app.whenReady().then(() => { ipcMain.handle('notification:send', async (event, content) => { console.log('收到渲染进程的通知请求:', content) return { code: 0, msg: '通知发送成功', data: { timestamp: Date.now(), content } } })
ipcMain.on('log:report', (event, logInfo) => { console.log('收到用户操作日志:', logInfo) event.reply('log:report-reply', { status: 'success', time: Date.now() }) })
ipcMain.handle('file:select', async () => { const result = await dialog.showOpenDialog(mainWindow, { properties: ['openFile', 'multiSelections'], filters: [{ name: '图片', extensions: ['jpg', 'png', 'gif'] }] }) return result.filePaths })
createWindow() })
app.on('window-all-closed', () => { if (process.platform !== 'darwin') app.quit() })
|
2. 预加载脚本 preload.js(核心桥梁)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| const { contextBridge, ipcRenderer } = require('electron')
contextBridge.exposeInMainWorld('electronAPI', { sendNotification: (content) => ipcRenderer.invoke('notification:send', content), reportLog: (logInfo) => ipcRenderer.send('log:report', logInfo), onLogReply: (callback) => ipcRenderer.on('log:report-reply', (event, data) => callback(data)), selectFile: () => ipcRenderer.invoke('file:select'), removeLogListener: () => ipcRenderer.removeAllListeners('log:report-reply') })
|
3. 渲染进程(前端代码)renderer.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
| const { electronAPI } = window
const sendIMNotification = async () => { const res = await electronAPI.sendNotification('你有一条新的IM消息') console.log('主进程返回结果:', res) }
const reportUserAction = () => { electronAPI.reportLog({ action: 'send_message', sessionId: '123456', userId: 'user_001', timestamp: Date.now() }) }
electronAPI.onLogReply((data) => { console.log('日志上报结果:', data) })
const selectAndSendFile = async () => { const filePaths = await electronAPI.selectFile() console.log('选中的文件路径:', filePaths) }
window.addEventListener('beforeunload', () => { electronAPI.removeLogListener() })
window.sendIMNotification = sendIMNotification window.reportUserAction = reportUserAction window.selectAndSendFile = selectAndSendFile
|
4. 页面入口 index.html
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| <!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>Electron IM客户端</title> </head> <body> <h1>IM客户端功能测试</h1> <button onclick="sendIMNotification()">发送新消息通知</button> <button onclick="reportUserAction()">上报用户操作</button> <button onclick="selectAndSendFile()">选择文件发送</button> <script src="renderer.js"></script> </body> </html>
|
三、逐模块深度讲解
安全核心:为什么必须用preload+contextBridge?
直接在渲染进程开启nodeIntegration: true会导致严重的安全漏洞:一旦前端页面被XSS攻击,攻击者就能直接获取Node.js全能力,访问用户本地文件、执行系统命令。contextIsolation开启后,渲染进程的上下文和预加载脚本的上下文完全隔离,只能通过exposeInMainWorld暴露的受控API通信,从根源上杜绝安全风险。
通信模式的选型场景
invoke/handle:90%的业务场景首选,支持async/await,有明确的返回值,适合「渲染进程调用主进程能力并等待结果」,比如调用原生对话框、获取系统信息、执行文件操作。
send/on:适合单向通知,不需要等待返回值,比如上报日志、同步状态,主进程可以通过event.reply回发消息。
- 主进程主动推送给渲染进程:用
mainWindow.webContents.send('事件名', 数据)
性能与内存优化
所有通过ipcRenderer.on注册的监听器,必须在组件/页面卸载时通过removeAllListeners移除,否则会导致监听器重复注册、内存泄漏、回调多次触发,这是Electron开发最常见的性能坑。
四、进阶及要点
- 深入理解主进程/渲染进程的资源隔离:渲染进程的崩溃不会导致整个应用崩溃,主进程的崩溃会导致整个应用退出。
- 大文件传输优化:IPC通信不适合传输超大文件(超过100MB),会阻塞进程,应该用「共享内存」「文件流分片」「Blob URL」的方式实现。
- 原生模块集成:Node.js原生C++模块需要对应Electron的Node.js版本重新编译,常用工具
electron-rebuild,「原生模块集成」。
模块二:WebSocket IM 心跳保活+断线重连
一、核心原理
WebSocket是IM实时通信的核心协议,基于TCP实现全双工通信,通过HTTP升级握手建立持久连接,解决了HTTP轮询的低效问题。IM场景的核心痛点是网络波动、TCP假死、消息丢失,因此必须实现3个核心能力:
- 心跳保活原理
TCP协议自带的keepalive是系统层面的,默认超时时间长达2小时,完全不适合IM场景。应用层心跳的核心逻辑是:
- 客户端定时向服务端发送
ping心跳包,同时启动超时计时器
- 服务端收到
ping后,立即回复pong包
- 客户端在超时时间内收到
pong,说明连接正常,重置计时器
- 超时未收到
pong,判定连接已断开,主动关闭连接,触发重连
- 断线重连原理
监听WebSocket的onerror、onclose事件,触发重连逻辑,采用指数退避算法:重连失败后,等待时间按指数级增长(1s→2s→4s→8s→最大32s),避免频繁重连给服务端造成压力,同时设置最大重连次数,防止无限重连。
- 消息队列原理
发送消息时,如果连接处于非OPEN状态,将消息存入本地待发送队列,重连成功后批量发送,保证消息不丢失,同时通过消息ID实现幂等性,避免消息重复发送。
二、完整可运行示例(IM场景封装)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214
| class IMWebSocket {
constructor(options) { this.url = options.url this.heartbeatInterval = options.heartbeatInterval || 30 * 1000 this.heartbeatTimeout = options.heartbeatTimeout || 10 * 1000 this.maxReconnectCount = options.maxReconnectCount || 10 this.maxReconnectInterval = options.maxReconnectInterval || 32 * 1000
this.ws = null this.reconnectCount = 0 this.heartbeatTimer = null this.heartbeatTimeoutTimer = null this.reconnectTimer = null this.pendingMessageQueue = [] this.isManualClose = false }
connect() { if (this.ws && this.ws.readyState === WebSocket.OPEN) return
try { this.ws = new WebSocket(this.url) this.bindEvent() } catch (error) { console.error('WebSocket连接创建失败:', error) this.reconnect() } }
bindEvent() { this.ws.onopen = () => { console.log('WebSocket连接成功') this.reconnectCount = 0 this.startHeartbeat() this.flushPendingMessage() }
this.ws.onmessage = (event) => { const message = JSON.parse(event.data) if (message.type === 'pong') { this.resetHeartbeat() return } this.onMessage(message) }
this.ws.onclose = (event) => { console.log('WebSocket连接关闭:', event.code, event.reason) this.clearHeartbeat() if (!this.isManualClose) { this.reconnect() } }
this.ws.onerror = (error) => { console.error('WebSocket连接错误:', error) this.clearHeartbeat() } }
startHeartbeat() { this.clearHeartbeat() this.heartbeatTimer = setInterval(() => { if (this.ws.readyState === WebSocket.OPEN) { this.ws.send(JSON.stringify({ type: 'ping', timestamp: Date.now() })) this.heartbeatTimeoutTimer = setTimeout(() => { console.warn('心跳超时,主动关闭连接') this.ws.close() }, this.heartbeatTimeout) } }, this.heartbeatInterval) }
resetHeartbeat() { this.clearHeartbeat() this.startHeartbeat() }
clearHeartbeat() { clearInterval(this.heartbeatTimer) clearTimeout(this.heartbeatTimeoutTimer) this.heartbeatTimer = null this.heartbeatTimeoutTimer = null }
reconnect() { if (this.reconnectCount >= this.maxReconnectCount) { console.error('超过最大重连次数,停止重连') this.onReconnectFail() return }
const delay = Math.min(Math.pow(2, this.reconnectCount) * 1000, this.maxReconnectInterval) this.reconnectCount++ console.log(`第${this.reconnectCount}次重连,等待${delay}ms后执行`)
if (this.reconnectTimer) clearTimeout(this.reconnectTimer) this.reconnectTimer = setTimeout(() => { this.connect() }, delay) }
sendMessage(message) { const sendData = JSON.stringify({ ...message, msgId: `msg_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`, timestamp: Date.now() })
if (this.ws && this.ws.readyState === WebSocket.OPEN) { this.ws.send(sendData) } else { console.warn('连接未就绪,消息存入待发送队列') this.pendingMessageQueue.push(sendData) } }
flushPendingMessage() { if (this.pendingMessageQueue.length === 0) return console.log(`发送待发送队列消息,共${this.pendingMessageQueue.length}条`) this.pendingMessageQueue.forEach((data) => this.ws.send(data)) this.pendingMessageQueue = [] }
close() { this.isManualClose = true this.clearHeartbeat() if (this.reconnectTimer) clearTimeout(this.reconnectTimer) if (this.ws) this.ws.close() this.ws = null }
onMessage(message) { console.log('收到业务消息:', message) }
onReconnectFail() { console.error('重连失败,请检查网络或服务端状态') } }
const imClient = new IMWebSocket({ url: 'ws://127.0.0.1:8080/im', heartbeatInterval: 30 * 1000, heartbeatTimeout: 10 * 1000, })
imClient.onMessage = (message) => { console.log('收到新的IM消息:', message) }
imClient.onReconnectFail = () => { alert('网络连接异常,请检查网络后重试') }
imClient.connect()
const sendChatMessage = (content, sessionId) => { imClient.sendMessage({ type: 'chat_message', sessionId, content, fromUserId: 'user_001', toUserId: 'user_002' }) }
|
三、逐模块深度讲解
心跳保活的核心细节
心跳间隔和超时时间的选型是IM场景的关键:移动端建议心跳间隔30-60s,超时10s;PC端建议心跳间隔20-30s,超时5s。同时必须在收到服务端的任何消息时都重置心跳,不只是pong包,因为服务端有业务消息下发,本身就说明连接正常。
指数退避算法的意义
当服务端宕机时,大量客户端同时频繁重连会导致服务端雪崩,指数退避算法可以让重连请求分散开,降低服务端压力,同时设置最大重连间隔,避免等待时间过长影响用户体验。
待发送队列与幂等性
消息队列保证了用户在断网时发送的消息不会丢失,重连成功后会自动补发。每个消息都生成唯一的msgId,服务端通过msgId去重,避免网络波动导致的消息重复发送,这是IM系统的核心要求。
四、进阶及要点
- 消息乱序解决:TCP协议保证有序,但如果出现重发、分片,可能导致消息乱序,需要给消息添加递增的序列号,客户端按序列号排序渲染。
- 流量控制:高并发场景下,服务端推送大量消息时,客户端需要实现消息限流,避免主线程阻塞、页面卡顿。
- 传输安全:IM消息必须用
wss://(WebSocket over TLS)加密传输,避免消息被抓包窃取,对应「传输安全方案」。
模块三:IndexedDB 本地消息存储与增量同步
一、核心原理
IndexedDB是浏览器/渲染进程内置的事务型NoSQL数据库,是IM客户端本地存储的唯一合适方案,对比localStorage有压倒性优势:
- 异步API,不会阻塞主线程,适合大容量数据操作
- 支持事务,保证数据一致性,成功提交、失败回滚
- 支持索引,可实现高性能的条件查询、范围查询
- 无明确容量上限(一般超过500MB会提示用户授权),适合存储海量聊天消息
- 同源限制,数据安全隔离
IM场景的核心设计:
- 核心概念:数据库(DB)→ 对象仓库(Object Store,相当于数据表)→ 索引(Index,加速查询)→ 事务(Transaction)
- 表设计:核心表
messages,存储聊天消息,主键msgId,索引包括sessionId(会话ID,按会话查消息)、timestamp(发送时间,按时间排序)、sendStatus(发送状态,筛选发送失败的消息)
- 增量同步原理:本地存储最新一条消息的同步时间戳,上线后只拉取该时间戳之后的消息,避免全量同步,提升性能,同时处理数据冲突(服务端消息覆盖本地未同步的草稿消息)。
二、完整可运行示例(IM场景封装)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234
| class IMIndexedDB { constructor() { this.dbName = 'IM_DB' this.dbVersion = 1 this.storeName = 'messages' this.db = null }
openDB() { return new Promise((resolve, reject) => { const request = indexedDB.open(this.dbName, this.dbVersion)
request.onupgradeneeded = (event) => { this.db = event.target.result console.log('数据库版本升级,初始化表结构')
if (!this.db.objectStoreNames.contains(this.storeName)) { const store = this.db.createObjectStore(this.storeName, { keyPath: 'msgId' }) store.createIndex('sessionId', 'sessionId', { unique: false }) store.createIndex('timestamp', 'timestamp', { unique: false }) store.createIndex('sendStatus', 'sendStatus', { unique: false }) } }
request.onsuccess = (event) => { this.db = event.target.result console.log('数据库打开成功') resolve(this.db) }
request.onerror = (event) => { console.error('数据库打开失败:', event.target.error) reject(event.target.error) } }) }
addMessage(message) { return new Promise((resolve, reject) => { const transaction = this.db.transaction(this.storeName, 'readwrite') const store = transaction.objectStore(this.storeName) const request = store.put(message)
request.onsuccess = () => { resolve({ code: 0, msg: '消息保存成功' }) }
request.onerror = () => { reject(request.error) }
transaction.oncomplete = () => { console.log('消息保存事务完成') }
transaction.onerror = (event) => { console.error('事务执行失败:', event.target.error) } }) }
batchAddMessages(messages) { return new Promise((resolve, reject) => { const transaction = this.db.transaction(this.storeName, 'readwrite') const store = transaction.objectStore(this.storeName)
messages.forEach((message) => { store.put(message) })
transaction.oncomplete = () => { resolve({ code: 0, msg: `批量保存${messages.length}条消息成功` }) }
transaction.onerror = (event) => { reject(event.target.error) } }) }
getMessagesBySessionId(sessionId, pageSize = 20, offset = 0) { return new Promise((resolve, reject) => { const transaction = this.db.transaction(this.storeName, 'readonly') const store = transaction.objectStore(this.storeName) const index = store.index('sessionId') const request = index.openCursor(IDBKeyRange.only(sessionId), 'prev')
const messages = [] let count = 0 let skipCount = 0
request.onsuccess = (event) => { const cursor = event.target.result if (cursor) { if (skipCount < offset) { skipCount++ cursor.continue() } else { messages.push(cursor.value) count++ if (count < pageSize) { cursor.continue() } else { resolve(messages) } } } else { resolve(messages) } }
request.onerror = () => { reject(request.error) } }) }
getLatestMessageTimestamp(sessionId) { return new Promise((resolve, reject) => { const transaction = this.db.transaction(this.storeName, 'readonly') const store = transaction.objectStore(this.storeName) const index = store.index('sessionId') const request = index.openCursor(IDBKeyRange.only(sessionId), 'prev')
request.onsuccess = (event) => { const cursor = event.target.result if (cursor) { resolve(cursor.value.timestamp) } else { resolve(0) } }
request.onerror = () => { reject(request.error) } }) }
deleteMessage(msgId) { return new Promise((resolve, reject) => { const transaction = this.db.transaction(this.storeName, 'readwrite') const store = transaction.objectStore(this.storeName) const request = store.delete(msgId)
request.onsuccess = () => { resolve({ code: 0, msg: '消息删除成功' }) }
request.onerror = () => { reject(request.error) } }) }
closeDB() { if (this.db) { this.db.close() this.db = null console.log('数据库已关闭') } } }
const imDB = new IMIndexedDB()
const initIMDB = async () => { try { await imDB.openDB()
await imDB.addMessage({ msgId: 'msg_1710000000000_abc123', sessionId: 'session_001', content: '你好,这是一条测试消息', fromUserId: 'user_001', toUserId: 'user_002', timestamp: 1710000000000, sendStatus: 'success' })
const messages = await imDB.getMessagesBySessionId('session_001', 20, 0) console.log('会话消息列表:', messages)
const latestTimestamp = await imDB.getLatestMessageTimestamp('session_001') console.log('最新消息时间戳:', latestTimestamp)
} catch (error) { console.error('数据库操作失败:', error) } }
initIMDB()
|
三、逐模块深度讲解
数据库版本与表结构初始化
IndexedDB的表结构创建、索引修改只能在onupgradeneeded事件中执行,该事件只有在数据库首次打开、或者版本号升高时才会触发。如果需要新增表、修改索引,只需要升高版本号,在该事件中处理即可。
事务的核心作用
所有数据库操作都必须在事务中执行,事务有3种模式:
readonly:只读事务,多个只读事务可以并行执行,性能更高
readwrite:读写事务,同一时间只能有一个读写事务执行,保证数据一致性
versionchange:版本变更事务,只能在onupgradeneeded中使用
批量操作必须放在同一个事务中执行,要么全部成功,要么全部失败,避免出现数据不一致的情况,比如批量同步消息时,中途失败会自动回滚,不会出现部分消息存入、部分没存入的问题。
索引与游标分页
按会话ID查询消息是IM场景最高频的操作,给sessionId创建索引,能把查询性能从O(n)提升到O(log n)。游标cursor是IndexedDB遍历大量数据的最佳方式,相比getAll(),游标不会一次性把所有数据加载到内存中,分页查询时内存占用更低,性能更好。
四、进阶/要点
- 性能优化:批量插入大量消息时,用单个事务批量执行,而不是每个消息开一个事务,性能能提升10倍以上。
- 数据同步冲突解决:采用「服务端消息优先」策略,同一条消息(同msgId),服务端的消息覆盖本地的草稿消息,保证数据一致性。
- 本地存储优化:超过3个月的历史消息,自动归档到本地文件,只保留热数据在IndexedDB中,提升查询性能。
模块四:Vue3+Pinia 复杂SPA状态管理
一、核心原理
Pinia是Vue3官方推荐的状态管理库,完全替代Vuex,核心基于Vue3的响应式系统,天生适配TypeScript,是复杂SPA状态管理的首选。
- 核心设计原理
- 每个Store是一个独立的响应式对象,没有命名空间的概念,天然模块化
- 取消了Vuex的
mutations,只有state、getters、actions,简化了状态修改流程
- 完全支持TypeScript类型推导,不需要额外的类型封装
- 支持插件化、持久化、服务端渲染
- IM场景的状态设计
复杂IM单页应用的核心状态分为4个模块,全局共享,跨组件访问:
- 用户状态:当前登录用户信息、token、权限
- 会话状态:会话列表、当前选中的会话、未读消息计数
- 消息状态:当前会话的消息列表、发送中的消息、草稿消息
- 连接状态:WebSocket连接状态、网络状态、重连状态
二、完整可运行示例(IM场景Pinia Store)
1. 会话状态Store stores/imSession.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111
| import { defineStore } from 'pinia' import { ref, computed } from 'vue'
export const useIMSessionStore = defineStore('imSession', () => { const sessionList = ref([]) const activeSessionId = ref('') const totalUnreadCount = computed(() => { return sessionList.value.reduce((total, session) => total + session.unreadCount, 0) }) const activeSession = computed(() => { return sessionList.value.find(session => session.sessionId === activeSessionId.value) || null })
const initSessionList = (list) => { sessionList.value = list }
const setActiveSession = (sessionId) => { activeSessionId.value = sessionId clearSessionUnread(sessionId) }
const clearSessionUnread = (sessionId) => { const session = sessionList.value.find(item => item.sessionId === sessionId) if (session) { session.unreadCount = 0 } }
const updateSessionByNewMessage = (message) => { const { sessionId, content, timestamp, fromUserId } = message const sessionIndex = sessionList.value.findIndex(item => item.sessionId === sessionId)
if (sessionIndex !== -1) { const session = sessionList.value[sessionIndex] session.lastMessage = content session.lastMessageTime = timestamp if (sessionId !== activeSessionId.value) { session.unreadCount += 1 } sessionList.value.splice(sessionIndex, 1) sessionList.value.unshift(session) } else { const newSession = { sessionId, sessionName: fromUserId, lastMessage: content, lastMessageTime: timestamp, unreadCount: sessionId === activeSessionId.value ? 0 : 1, avatar: '' } sessionList.value.unshift(newSession) } }
const deleteSession = (sessionId) => { sessionList.value = sessionList.value.filter(item => item.sessionId !== sessionId) if (activeSessionId.value === sessionId) { activeSessionId.value = '' } }
return { sessionList, activeSessionId, totalUnreadCount, activeSession, initSessionList, setActiveSession, clearSessionUnread, updateSessionByNewMessage, deleteSession } })
|
2. 消息状态Store stores/imMessage.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122
| import { defineStore } from 'pinia' import { ref } from 'vue' import { useIMSessionStore } from './imSession' import { imDB } from '../utils/imIndexedDB' import { imClient } from '../utils/imWebSocket'
export const useIMMessageStore = defineStore('imMessage', () => { const messageList = ref([]) const sendingMsgSet = ref(new Set()) const draftMap = ref(new Map())
const initMessageList = (sessionId, messages) => { messageList.value = messages }
const sendMessage = async (content, sessionId) => { if (!content.trim() || !sessionId) return
const msgId = `msg_${Date.now()}_${Math.random().toString(36).substr(2, 9)}` const timestamp = Date.now()
const message = { msgId, sessionId, content, fromUserId: 'user_001', toUserId: useIMSessionStore().activeSession.toUserId, timestamp, sendStatus: 'pending' }
messageList.value.push(message) sendingMsgSet.value.add(msgId) await imDB.addMessage(message) useIMSessionStore().updateSessionByNewMessage(message) imClient.sendMessage(message)
return msgId }
const receiveMessage = async (message) => { const { sessionId } = message await imDB.addMessage(message) if (sessionId === useIMSessionStore().activeSessionId) { messageList.value.push(message) } useIMSessionStore().updateSessionByNewMessage(message) }
const updateMessageStatus = (msgId, status) => { const message = messageList.value.find(item => item.msgId === msgId) if (message) { message.sendStatus = status } sendingMsgSet.value.delete(msgId) }
const saveDraft = (sessionId, content) => { draftMap.value.set(sessionId, content) }
const getDraft = (sessionId) => { return draftMap.value.get(sessionId) || '' }
return { messageList, sendingMsgSet, draftMap, initMessageList, sendMessage, receiveMessage, updateMessageStatus, saveDraft, getDraft } })
|
3. Vue组件中使用示例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89
| <template> <div class="im-chat-page"> <!-- 会话列表 --> <div class="session-list"> <div v-for="session in sessionList" :key="session.sessionId" :class="['session-item', { active: session.sessionId === activeSessionId }]" @click="switchSession(session.sessionId)" > <div class="session-name">{{ session.sessionName }}</div> <div class="last-message">{{ session.lastMessage }}</div> <div class="unread-badge" v-if="session.unreadCount > 0"> {{ session.unreadCount }} </div> </div> </div>
<!-- 聊天区域 --> <div class="chat-area" v-if="activeSession"> <div class="message-list"> <div v-for="message in messageList" :key="message.msgId" :class="['message-item', message.fromUserId === currentUserId ? 'self' : 'other']" > <div class="message-content">{{ message.content }}</div> <div class="message-status"> {{ message.sendStatus === 'pending' ? '发送中' : message.sendStatus === 'failed' ? '发送失败' : '' }} </div> </div> </div>
<!-- 输入框 --> <div class="input-area"> <textarea v-model="inputContent" placeholder="请输入消息..." @keydown.enter.prevent="sendMessage" ></textarea> <button @click="sendMessage">发送</button> </div> </div> </div> </template>
<script setup> import { ref, computed, watch, onMounted, onBeforeUnmount } from 'vue' import { storeToRefs } from 'pinia' import { useIMSessionStore } from '@/stores/imSession' import { useIMMessageStore } from '@/stores/imMessage'
// 从Store中获取响应式状态,用storeToRefs保持响应式,避免解构丢失响应式 const imSessionStore = useIMSessionStore() const imMessageStore = useIMMessageStore() const { sessionList, activeSessionId, activeSession, totalUnreadCount } = storeToRefs(imSessionStore) const { messageList, sendingMsgSet } = storeToRefs(imMessageStore)
// 本地状态 const inputContent = ref('') const currentUserId = 'user_001'
// 切换会话 const switchSession = (sessionId) => { imSessionStore.setActiveSession(sessionId) // 恢复草稿 inputContent.value = imMessageStore.getDraft(sessionId) }
// 发送消息 const sendMessage = () => { imMessageStore.sendMessage(inputContent.value, activeSessionId.value) inputContent.value = '' }
// 监听输入框变化,保存草稿 watch(inputContent, (newVal) => { if (activeSessionId.value) { imMessageStore.saveDraft(activeSessionId.value, newVal) } })
// 页面卸载前保存草稿 onBeforeUnmount(() => { if (activeSessionId.value) { imMessageStore.saveDraft(activeSessionId.value, inputContent.value) } }) </script>
|
三、逐模块深度讲解
Store的两种写法
Pinia支持Option API和Composition API两种写法,上面示例用的是Composition API风格,和Vue3的setup语法完美契合,更灵活,适合复杂的业务逻辑,TypeScript类型推导也更友好。
storeToRefs的核心作用
直接解构Store中的state和getters会丢失响应式,storeToRefs会把state和getters转换成ref对象,保持响应式,同时不会把actions转换成ref,actions可以直接解构使用。
IM场景的状态设计最佳实践
- 状态分层:将会话、消息、用户、连接状态拆分到不同的Store中,避免单个Store过于臃肿,维护性更高
- 乐观更新:发送消息时,先把消息添加到页面,再等待服务端响应,用户无感知,体验更好
- 数据持久化:所有消息都先存入本地IndexedDB,再同步到服务端,保证离线也能查看消息
- 跨Store通信:在一个Store中可以直接导入另一个Store,调用其actions,访问其state,非常灵活
四、进阶/要点
- 性能优化:消息列表渲染优化,用虚拟列表解决万条消息渲染卡顿问题,「SPA性能优化」
- 状态持久化:用
pinia-plugin-persistedstate插件实现状态持久化,刷新页面后会话列表、草稿不丢失
- 模块化设计:大型项目中,按业务模块拆分Store,每个Store负责单一职责,符合单一原则,可维护性更高
整体串联
以上4个模块完全覆盖核心要求,组合起来就是一个完整的Electron IM客户端的核心架构:
- 前端层:Vue3+Pinia实现页面渲染和状态管理
- 通信层:WebSocket实现实时消息收发、心跳保活、断线重连
- 存储层:IndexedDB实现本地消息存储、增量同步
- 原生层:Electron实现跨平台桌面客户端、原生能力调用、进程通信
Electron + SQLite + ORM 完整操作指南(TypeORM + better-sqlite3)
在Electron中操作SQLite,TypeORM + better-sqlite3是目前最成熟、性能最好的方案:
- TypeORM:TypeScript/JavaScript生态最完善的ORM框架,支持装饰器、迁移、事务,与Electron兼容性极佳
- better-sqlite3:性能最强的SQLite Node.js驱动(同步API,无回调地狱),原生支持Electron
本指南完全贴合IM桌面客户端场景,从环境搭建→模型定义→CRUD操作→Electron安全集成,提供完整可运行的代码和深度原理讲解。
一、核心原理与架构设计
1. 为什么选这个组合?
| 对比项 |
TypeORM + better-sqlite3 |
Prisma + sqlite3 |
Sequelize + sqlite3 |
| 性能 |
极高(同步API,无异步开销) |
中等(异步API) |
中等 |
| TypeScript支持 |
完美(装饰器+类型推导) |
完美(Schema生成类型) |
良好 |
| Electron兼容性 |
极佳(原生模块易编译) |
一般(Prisma Client需特殊配置) |
良好 |
| 学习曲线 |
平缓(文档完善) |
中等(需学Schema语言) |
平缓 |
| IM场景适配 |
完美(支持事务、索引、复杂查询) |
良好 |
良好 |
2. Electron进程架构设计(安全红线)
SQLite是文件型数据库,不支持多进程并发写入,因此必须严格遵循:
- 数据库操作只在主进程执行:渲染进程通过IPC通信调用主进程的数据库方法
- 禁止在渲染进程直接操作数据库:违反Electron安全最佳实践(
nodeIntegration: false)
- 使用预加载脚本(preload)暴露受控API:通过
contextBridge暴露IPC调用方法,保证安全
二、环境搭建与依赖安装
1. 初始化Electron项目(以Vite+Electron为例)
1 2 3 4 5 6
| npm create vite@latest electron-im-sqlite -- --template vue-ts cd electron-im-sqlite
npm install -D electron electron-builder vite-plugin-electron vite-plugin-electron-renderer
|
2. 安装核心依赖
1 2 3 4 5
| npm install typeorm better-sqlite3 reflect-metadata
npm install -D @types/better-sqlite3 electron-rebuild
|
3. 配置原生模块编译(关键步骤)
better-sqlite3是原生C++模块,必须编译成对应Electron的Node.js版本,否则会报Module did not self-register错误。
在package.json中添加编译脚本:
1 2 3 4 5 6
| { "scripts": { "rebuild": "electron-rebuild -f -w better-sqlite3", "postinstall": "npm run rebuild" } }
|
执行编译:
三、TypeORM配置与模型定义
1. 项目目录结构
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| electron-im-sqlite/ ├── src/ │ ├── main/ # 主进程代码 │ │ ├── index.ts # 主进程入口 │ │ ├── database/ # 数据库相关 │ │ │ ├── data-source.ts # TypeORM数据源配置 │ │ │ └── entities/ # 实体(模型)定义 │ │ │ └── Message.ts # IM消息实体 │ │ └── ipc/ # IPC通信处理 │ │ └── db-ipc.ts │ ├── preload/ # 预加载脚本 │ │ └── index.ts │ └── renderer/ # 渲染进程代码(Vue) └── package.json
|
2. 定义IM消息实体(Model)
创建src/main/database/entities/Message.ts:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65
| import { Entity, PrimaryColumn, Column, Index } from 'typeorm'
@Entity('message') export class Message {
@PrimaryColumn({ type: 'varchar', length: 50 }) msgId: string
@Index() @Column({ type: 'varchar', length: 50 }) sessionId: string
@Column({ type: 'varchar', length: 50 }) fromUserId: string
@Column({ type: 'varchar', length: 50 }) toUserId: string
@Column({ type: 'text' }) content: string
@Column({ type: 'varchar', length: 20, default: 'text' }) msgType: string
@Column({ type: 'varchar', length: 20, default: 'pending' }) sendStatus: string
@Index() @Column({ type: 'bigint' }) timestamp: number
@Column({ type: 'tinyint', default: 0 }) isRead: number }
|
3. 配置TypeORM数据源
创建src/main/database/data-source.ts:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41
| import 'reflect-metadata' import { DataSource } from 'typeorm' import { app } from 'electron' import path from 'path' import { Message } from './entities/Message'
const getDBPath = (): string => { const userDataPath = app.getPath('userData') return path.join(userDataPath, 'im.db') }
export const AppDataSource = new DataSource({ type: 'better-sqlite3', database: getDBPath(), entities: [Message], synchronize: process.env.NODE_ENV === 'development', logging: process.env.NODE_ENV === 'development', driver: require('better-sqlite3'), extra: { journalMode: 'WAL', synchronous: 'NORMAL' } })
|
四、Electron主进程集成与IPC通信
1. 初始化数据库(主进程入口)
修改src/main/index.ts:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60
| import { app, BrowserWindow } from 'electron' import path from 'path' import { AppDataSource } from './database/data-source' import './ipc/db-ipc'
let mainWindow: BrowserWindow | null
app.whenReady().then(async () => { try { await AppDataSource.initialize() console.log('✅ 数据库初始化成功') } catch (error) { console.error('❌ 数据库初始化失败:', error) app.quit() return }
createWindow() })
const createWindow = () => { mainWindow = new BrowserWindow({ width: 1200, height: 800, webPreferences: { contextIsolation: true, nodeIntegration: false, preload: path.join(__dirname, '../preload/index.js') } })
if (process.env.NODE_ENV === 'development') { mainWindow.loadURL('http://localhost:5173') mainWindow.webContents.openDevTools() } else { mainWindow.loadFile(path.join(__dirname, '../renderer/index.html')) } }
app.on('before-quit', async () => { if (AppDataSource.isInitialized) { await AppDataSource.destroy() console.log('✅ 数据库连接已关闭') } })
app.on('window-all-closed', () => { if (process.platform !== 'darwin') app.quit() })
app.on('activate', () => { if (BrowserWindow.getAllWindows().length === 0) createWindow() })
|
2. 封装数据库操作与IPC通信
创建src/main/ipc/db-ipc.ts:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127
| import { ipcMain } from 'electron' import { AppDataSource } from '../database/data-source' import { Message } from '../database/entities/Message'
const getMessageRepository = () => { return AppDataSource.getRepository(Message) }
ipcMain.handle('db:saveMessage', async (event, message: Message) => { try { const repo = getMessageRepository() const savedMessage = await repo.save(message) return { code: 0, msg: '保存成功', data: savedMessage } } catch (error) { console.error('保存消息失败:', error) return { code: -1, msg: '保存失败', error: (error as Error).message } } })
ipcMain.handle('db:batchSaveMessages', async (event, messages: Message[]) => { try { const repo = getMessageRepository() const savedMessages = await AppDataSource.transaction(async (transactionalEntityManager) => { return await transactionalEntityManager.save(messages) }) return { code: 0, msg: `批量保存${savedMessages.length}条消息成功`, data: savedMessages } } catch (error) { console.error('批量保存消息失败:', error) return { code: -1, msg: '批量保存失败', error: (error as Error).message } } })
ipcMain.handle( 'db:getMessagesBySessionId', async (event, sessionId: string, pageSize: number = 20, offset: number = 0) => { try { const repo = getMessageRepository() const [messages, total] = await repo .createQueryBuilder('message') .where('message.sessionId = :sessionId', { sessionId }) .orderBy('message.timestamp', 'DESC') .skip(offset) .take(pageSize) .getManyAndCount()
return { code: 0, msg: '查询成功', data: { messages, total } } } catch (error) { console.error('查询消息失败:', error) return { code: -1, msg: '查询失败', error: (error as Error).message } } } )
ipcMain.handle('db:updateMessageStatus', async (event, msgId: string, status: string) => { try { const repo = getMessageRepository() const result = await repo.update({ msgId }, { sendStatus: status }) if (result.affected === 0) { return { code: -1, msg: '消息不存在' } } return { code: 0, msg: '更新成功' } } catch (error) { console.error('更新消息状态失败:', error) return { code: -1, msg: '更新失败', error: (error as Error).message } } })
ipcMain.handle('db:getLatestTimestamp', async (event, sessionId: string) => { try { const repo = getMessageRepository() const latestMessage = await repo .createQueryBuilder('message') .where('message.sessionId = :sessionId', { sessionId }) .orderBy('message.timestamp', 'DESC') .getOne()
return { code: 0, msg: '查询成功', data: latestMessage?.timestamp || 0 } } catch (error) { console.error('查询最新时间戳失败:', error) return { code: -1, msg: '查询失败', error: (error as Error).message } } })
ipcMain.handle('db:deleteMessage', async (event, msgId: string) => { try { const repo = getMessageRepository() const result = await repo.delete({ msgId }) if (result.affected === 0) { return { code: -1, msg: '消息不存在' } } return { code: 0, msg: '删除成功' } } catch (error) { console.error('删除消息失败:', error) return { code: -1, msg: '删除失败', error: (error as Error).message } } })
|
五、预加载脚本与渲染进程调用
1. 预加载脚本(preload)
创建src/preload/index.ts:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| import { contextBridge, ipcRenderer } from 'electron' import type { Message } from '../main/database/entities/Message'
export interface ElectronAPI { dbSaveMessage: (message: Message) => Promise<{ code: number; msg: string; data?: Message }> dbBatchSaveMessages: (messages: Message[]) => Promise<{ code: number; msg: string; data?: Message[] }> dbGetMessagesBySessionId: ( sessionId: string, pageSize?: number, offset?: number ) => Promise<{ code: number; msg: string; data?: { messages: Message[]; total: number } }> dbUpdateMessageStatus: (msgId: string, status: string) => Promise<{ code: number; msg: string }> dbGetLatestTimestamp: (sessionId: string) => Promise<{ code: number; msg: string; data: number }> dbDeleteMessage: (msgId: string) => Promise<{ code: number; msg: string }> }
contextBridge.exposeInMainWorld('electronAPI', { dbSaveMessage: (message) => ipcRenderer.invoke('db:saveMessage', message), dbBatchSaveMessages: (messages) => ipcRenderer.invoke('db:batchSaveMessages', messages), dbGetMessagesBySessionId: (sessionId, pageSize, offset) => ipcRenderer.invoke('db:getMessagesBySessionId', sessionId, pageSize, offset), dbUpdateMessageStatus: (msgId, status) => ipcRenderer.invoke('db:updateMessageStatus', msgId, status), dbGetLatestTimestamp: (sessionId) => ipcRenderer.invoke('db:getLatestTimestamp', sessionId), dbDeleteMessage: (msgId) => ipcRenderer.invoke('db:deleteMessage', msgId) } satisfies ElectronAPI)
|
2. 渲染进程(Vue组件)中调用
修改src/renderer/src/App.vue:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130
| <template> <div class="im-app"> <h1>Electron IM SQLite 示例</h1>
<!-- 操作按钮 --> <div class="actions"> <button @click="saveTestMessage">保存测试消息</button> <button @click="loadMessages">加载会话消息</button> <button @click="updateMessageStatus">更新消息状态</button> <button @click="deleteMessage">删除消息</button> </div>
<!-- 消息列表 --> <div class="message-list" v-if="messages.length > 0"> <h3>会话消息列表(共{{ total }}条)</h3> <div v-for="msg in messages" :key="msg.msgId" class="message-item"> <div class="msg-content">{{ msg.content }}</div> <div class="msg-meta"> 时间:{{ new Date(msg.timestamp).toLocaleString() }} | 状态:{{ msg.sendStatus }} | ID:{{ msg.msgId }} </div> </div> </div>
<!-- 结果提示 --> <div class="result" v-if="result">{{ result }}</div> </div> </template>
<script setup lang="ts"> import { ref } from 'vue'
// 获取TypeScript类型支持 const { electronAPI } = window as unknown as { electronAPI: import('../../preload').ElectronAPI }
// 本地状态 const messages = ref<import('../../main/database/entities/Message').Message[]>([]) const total = ref(0) const result = ref('') const testSessionId = 'session_001' let testMsgId = ''
// 1. 保存测试消息 const saveTestMessage = async () => { const msgId = `msg_${Date.now()}_${Math.random().toString(36).substr(2, 9)}` testMsgId = msgId
const message = { msgId, sessionId: testSessionId, fromUserId: 'user_001', toUserId: 'user_002', content: `测试消息 - ${new Date().toLocaleString()}`, msgType: 'text', sendStatus: 'pending', timestamp: Date.now(), isRead: 0 }
const res = await electronAPI.dbSaveMessage(message) result.value = res.code === 0 ? `✅ ${res.msg}` : `❌ ${res.msg}` }
// 2. 加载会话消息 const loadMessages = async () => { const res = await electronAPI.dbGetMessagesBySessionId(testSessionId, 20, 0) if (res.code === 0 && res.data) { messages.value = res.data.messages total.value = res.data.total result.value = `✅ 加载成功,共${res.data.total}条消息` } else { result.value = `❌ ${res.msg}` } }
// 3. 更新消息状态 const updateMessageStatus = async () => { if (!testMsgId) { result.value = '❌ 请先保存一条测试消息' return } const res = await electronAPI.dbUpdateMessageStatus(testMsgId, 'success') result.value = res.code === 0 ? `✅ ${res.msg}` : `❌ ${res.msg}` // 重新加载消息 await loadMessages() }
// 4. 删除消息 const deleteMessage = async () => { if (!testMsgId) { result.value = '❌ 请先保存一条测试消息' return } const res = await electronAPI.dbDeleteMessage(testMsgId) result.value = res.code === 0 ? `✅ ${res.msg}` : `❌ ${res.msg}` // 重新加载消息 await loadMessages() } </script>
<style scoped> .im-app { padding: 20px; } .actions { margin: 20px 0; gap: 10px; display: flex; } .message-item { border: 1px solid #eee; padding: 10px; margin: 10px 0; border-radius: 4px; } .msg-meta { font-size: 12px; color: #666; margin-top: 5px; } .result { margin-top: 20px; padding: 10px; background: #f5f5f5; border-radius: 4px; } </style>
|
六、核心进阶与最佳实践
1. WAL模式与性能优化
在TypeORM配置中开启的journalMode: 'WAL'是SQLite性能提升的关键:
- 原理:WAL(Write-Ahead Logging)将写入操作先写入WAL文件,后台异步合并到主数据库,读操作可以直接读主数据库+WAL文件,实现读并发
- 性能提升:写入性能提升10-100倍,读性能提升2-3倍,完全满足IM场景的高并发消息写入需求
- 注意事项:WAL模式会生成
-wal和-shm两个临时文件,不要手动删除
2. 事务处理(数据一致性保证)
批量操作必须使用事务,比如同步100条离线消息,要么全部成功,要么全部失败,避免出现部分消息存入的情况:
1 2 3 4 5 6
| await AppDataSource.transaction(async (transactionalEntityManager) => { await transactionalEntityManager.save(messages) await transactionalEntityManager.update(Message, { sessionId }, { isRead: 1 }) })
|
3. 索引优化(查询性能提升)
在IM场景中,sessionId和timestamp是最高频的查询条件,必须创建索引:
- @Index()装饰器:自动创建索引,查询性能从O(n)提升到O(log n)
- 复合索引:如果经常按
sessionId + timestamp查询,可以创建复合索引:1
| @Index(['sessionId', 'timestamp'])
|
4. 生产环境表结构管理(迁移)
开发环境用synchronize: true自动创建表,生产环境必须用迁移(Migration)管理表结构,避免数据丢失:
1 2 3 4 5
| npx typeorm-ts-node-commonjs migration:generate src/main/database/migrations/InitMessage -d src/main/database/data-source.ts
npx typeorm-ts-node-commonjs migration:run -d src/main/database/data-source.ts
|
5. 数据库加密(敏感数据保护)
如果IM消息包含敏感数据,可以用better-sqlite3的加密扩展sqlcipher,或者在应用层对敏感字段加密:
1 2 3 4 5 6 7 8 9 10
| import crypto from 'crypto'
const encrypt = (content: string, key: string) => { const cipher = crypto.createCipher('aes192', key) return cipher.update(content, 'utf8', 'hex') + cipher.final('hex') }
message.content = encrypt(message.content, 'your-secret-key')
|
七、常见问题排查
Module did not self-register错误:
- 原因:
better-sqlite3没有编译成对应Electron的Node.js版本
- 解决:执行
npm run rebuild重新编译
数据库文件锁定错误:
- 原因:SQLite不支持多进程并发写入,或者上一个进程没有正常关闭数据库连接
- 解决:确保所有数据库操作都在主进程执行,应用关闭时调用
AppDataSource.destroy()
查询性能慢:
- 原因:没有创建索引,或者查询条件没有用到索引
- 解决:给高频查询字段加
@Index(),用TypeORM的logging: true查看SQL语句,分析索引使用情况
TypeORM类型错误:
- 原因:实体定义的类型和数据库字段类型不匹配
- 解决:严格按照TypeORM文档定义实体类型,
bigint类型在JavaScript中用number处理
八、运行项目
1 2 3 4 5 6 7 8
| npm install
npm run rebuild
npm run dev
|
点击页面上的按钮,即可测试SQLite的CRUD操作,数据库文件会自动生成在Electron用户数据目录下。
IM SDK 多版本兼容与平滑升级完整方案
IM SDK 的多版本兼容和平滑升级是一个系统性工程,核心目标是保证新旧版本客户端互通、API 不崩溃、数据不丢失、升级无感知。本方案从协议层、API 层、数据层、发布层、服务端适配5个维度展开,提供可落地的架构设计和代码示例。
一、核心设计原则
- 语义化版本(SemVer):严格遵循
MAJOR.MINOR.PATCH 版本规则
MAJOR:大版本,允许破坏性变更(需提前6个月 deprecation 警告)
MINOR:小版本,新增功能,完全向后兼容
PATCH:补丁版本,修复 bug,完全向后兼容
- 双向兼容:不仅新版本兼容旧版本,旧版本也要能兼容新版本(至少能正常运行,不崩溃)
- 渐进式升级:给用户足够的过渡期,deprecation 的 API 至少保留 2 个大版本
- 可回滚:新版本发布后出现问题,能快速回滚到旧版本,服务端和 SDK 都要支持
二、协议层兼容(IM 互通的基础)
IM 的核心是网络通信,协议层兼容是新旧版本互通的前提,90%的 IM 兼容问题都出在协议层。
1. 可扩展的协议格式选型
| 协议格式 |
可扩展性 |
兼容性 |
性能 |
IM 场景适配 |
| Protocol Buffers (Protobuf) |
极高(支持字段新增/废弃) |
完美(旧版本能解析新版本的未知字段) |
极高 |
✅ 首选 |
| Thrift |
高 |
良好 |
极高 |
✅ 可选 |
| JSON |
中 |
中(字段缺失会报错) |
低 |
❌ 不推荐 |
Protobuf 兼容设计示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| syntax = "proto3";
message ChatMessage { string msg_id = 1; string session_id = 2; string from_user_id = 3; string content = 4; int64 timestamp = 5;
optional string msg_type = 6; optional bytes media_data = 7; reserved 8; }
|
核心规则:
- 永远不能修改已有字段的字段号和类型
- 新增字段必须用
optional(proto3 中默认 optional)
- 废弃字段必须用
reserved 标记,禁止复用字段号
- 旧版本解析新版本消息时,会自动忽略未知字段,不会崩溃
2. 协议版本协商机制
在 SDK 与服务端握手阶段,必须协商协议版本,确保双方使用都能支持的最高版本通信。
握手流程设计:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| 1. 客户端 → 服务端:HelloRequest { "sdk_version": "2.1.0", "protocol_version": "3.0", "supported_protocols": ["3.0", "2.5", "2.0"] }
2. 服务端 → 客户端:HelloResponse { "code": 0, "negotiated_protocol": "2.5", "server_version": "1.5.0", "upgrade_hint": "建议升级 SDK 到 2.1.0 以获得新功能" }
|
服务端兼容逻辑:
- 服务端同时维护多个版本的协议解析器
- 根据客户端的
supported_protocols,选择双方都支持的最高版本
- 如果客户端版本过低,返回
upgrade_required 错误,强制升级(仅在安全漏洞时使用)
三、API 层兼容(客户端开发体验的核心)
SDK 对外暴露的 API 是开发者最关心的部分,API 层兼容的目标是新版本 SDK 替换旧版本后,开发者的代码不用改就能编译运行。
1. API 设计的兼容原则
选项对象模式:避免使用多个参数,改用选项对象,方便新增参数
1 2 3 4 5 6 7 8 9 10 11
| function sendMessage(content: string, toUserId: string) {}
interface SendMessageOptions { content: string; toUserId: string; msgType?: string; priority?: number; } function sendMessage(options: SendMessageOptions) {}
|
方法重载:对于必须修改参数的方法,提供重载版本,保留旧方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
|
function sendMessage(content: string, toUserId: string): Promise<void>;
function sendMessage(options: SendMessageOptions): Promise<Message>;
function sendMessage(arg1: any, arg2?: any): Promise<any> { if (typeof arg1 === 'string' && typeof arg2 === 'string') { return sendMessage({ content: arg1, toUserId: arg2 }); } return doSendMessage(arg1); }
|
Deprecation 警告机制:对于即将废弃的 API,提前给出警告,给开发者过渡期
1 2 3 4 5 6 7 8 9 10
|
function login(username: string, password: string): Promise<void> { console.warn('[IM SDK] login() 已废弃,请使用 loginWithToken()'); return loginWithToken(convertToToken(username, password)); }
|
2. 接口隔离与模块化设计
将 SDK 拆分为核心模块和扩展模块,核心模块保持稳定,扩展模块可以独立升级:
1 2 3 4 5 6 7 8 9 10
| im-sdk/ ├── core/ # 核心模块(稳定,大版本才变更) │ ├── connection/ # 连接管理 │ ├── protocol/ # 协议解析 │ └── storage/ # 数据存储 ├── modules/ # 扩展模块(可独立升级) │ ├── chat/ # 聊天功能(v1.0) │ ├── group/ # 群组功能(v1.1 新增) │ └── rtc/ # 音视频功能(v2.0 新增) └── index.ts # 入口文件
|
好处:
- 开发者可以只引入需要的模块,减小包体积
- 扩展模块升级不影响核心模块,降低风险
- 可以按需发布模块更新,不用整个 SDK 都升级
四、数据层兼容(用户体验的核心)
IM SDK 会在本地存储大量消息、会话、用户数据,数据层兼容的目标是升级 SDK 后,旧数据不丢失、能正常读取、不用重新登录。
1. 数据库版本管理与迁移机制
使用版本化数据库,每次数据库结构变更时,编写迁移脚本,自动将旧数据转换为新格式。
SQLite 数据库迁移示例(TypeORM):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48
| @Entity('db_version') export class DBVersion { @PrimaryColumn() id: number = 1;
@Column() version: number; }
const migrations = [ { version: 1, up: migrateV1ToV2 }, { version: 2, up: migrateV2ToV3 }, { version: 3, up: migrateV3ToV4 }, ];
async function autoMigrate() { const versionRepo = dataSource.getRepository(DBVersion); let currentVersion = await versionRepo.findOne({ where: { id: 1 } }); if (!currentVersion) { await versionRepo.save({ id: 1, version: migrations.length }); return; }
for (let i = currentVersion.version; i < migrations.length; i++) { const migration = migrations[i]; console.log(`[IM SDK] 执行数据库迁移:v${i} → v${i + 1}`); await dataSource.transaction(async (tx) => { await migration.up(tx); await versionRepo.update({ id: 1 }, { version: i + 1 }); }); } }
async function migrateV1ToV2(tx: EntityManager) { await tx.query(`ALTER TABLE message ADD COLUMN msg_type TEXT DEFAULT 'text'`); await tx.query(`UPDATE message SET msg_type = 'text' WHERE msg_type IS NULL`); await tx.query(`CREATE INDEX IF NOT EXISTS idx_msg_type ON message(msg_type)`); }
|
核心规则:
- 每次数据库结构变更,必须编写迁移脚本
- 迁移必须在事务中执行,失败自动回滚
- 迁移前必须备份数据库(可选但推荐)
- 禁止删除字段,只能标记为废弃(避免旧版本 SDK 读取失败)
2. 认证信息兼容
升级 SDK 后,用户不应该需要重新登录,这就要求认证信息的存储格式兼容:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| interface OldAuthInfo { token: string; expireTime: number; }
interface NewAuthInfo { token: string; refreshToken: string; expireTime: number; }
async function getAuthInfo(): Promise<NewAuthInfo | null> { const stored = await localStorage.getItem('auth_info'); if (!stored) return null;
const data = JSON.parse(stored); if (!data.refreshToken) { const newData: NewAuthInfo = { ...data, refreshToken: await exchangeRefreshToken(data.token), }; await saveAuthInfo(newData); return newData; } return data; }
|
五、发布层:灰度发布与回滚机制
新版本 SDK 不能一下子全量发布,必须通过灰度发布逐步放量,观察指标,出现问题快速回滚。
1. 灰度发布策略
| 灰度阶段 |
放量比例 |
目标用户 |
观察指标 |
| 内部测试 |
0% |
内部员工、测试团队 |
崩溃率、错误率、功能完整性 |
| 小范围灰度 |
1% - 5% |
活跃用户、志愿者用户 |
崩溃率、错误率、用户反馈 |
| 中范围灰度 |
10% - 30% |
按地区、按设备类型放量 |
性能指标、兼容性问题 |
| 全量发布 |
100% |
所有用户 |
整体指标、监控告警 |
灰度发布工具:
- 自研灰度系统:根据用户 ID、设备 ID、地区等规则放量
- 第三方平台:Firebase Remote Config、阿里云移动热修复等
2. 回滚机制
新版本发布后,如果出现严重问题(崩溃率飙升、核心功能不可用),必须能快速回滚到旧版本:
- SDK 端回滚:
- 如果是动态更新的 SDK(如 Web SDK、React Native SDK),可以直接回滚 CDN 资源
- 如果是原生 SDK(iOS/Android/Windows),需要发布一个补丁版本,代码回退到旧版本,版本号递增(如
2.1.1 → 2.1.2,但代码是 2.1.0)
- 服务端回滚:
- 服务端同时部署多个版本,通过流量切换回滚到旧版本
- 数据库如果有变更,需要编写回滚脚本(迁移脚本要可逆)
3. 监控与告警
建立完善的监控体系,实时观察新版本的运行状态:
- 崩溃监控:Firebase Crashlytics、Sentry、Bugly
- 错误监控:自定义错误上报,监控协议错误、API 错误
- 性能监控:连接耗时、消息发送耗时、数据库操作耗时
- 业务监控:消息发送成功率、登录成功率、用户活跃度
告警规则:
- 崩溃率超过 0.1%,立即告警
- 错误率超过 1%,立即告警
- 核心功能成功率低于 99%,立即告警
六、服务端适配:多版本 SDK 同时支持
SDK 的兼容离不开服务端的配合,服务端必须同时支持多个版本的 SDK,不能因为服务端升级导致旧版本 SDK 不可用。
1. 服务端多版本架构
采用API 网关 + 多版本服务的架构:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| ┌─────────────┐ │ API 网关 │ │ (版本路由) │ └──────┬──────┘ │ ┌───────────────┼───────────────┐ │ │ │ ┌──────▼──────┐ ┌─────▼─────┐ ┌─────▼─────┐ │ IM Service │ │IM Service │ │IM Service │ │ v1.0 │ │ v2.0 │ │ v3.0 │ └─────────────┘ └───────────┘ └───────────┘ │ │ │ ┌──────▼───────────────▼───────────────▼──────┐ │ 共享数据库、消息队列 │ └───────────────────────────────────────────────┘
|
版本路由规则:
- API 网关根据 SDK 请求头中的
X-SDK-Version 路由到对应的服务版本
- 旧版本 SDK 请求路由到旧版本服务,新版本 SDK 请求路由到新版本服务
- 共享数据库和消息队列,保证新旧版本数据互通
2. 服务端兼容逻辑
- 协议兼容:服务端同时维护多个版本的协议解析器,能解析旧版本的协议
- 数据兼容:数据库结构变更时,保持旧字段不变,新增字段用默认值填充
- API 兼容:旧版本 API 继续保留,标记为 deprecated,至少保留 2 个大版本
- 消息互通:新旧版本 SDK 发送的消息,双方都能接收和解析(依赖协议层兼容)
七、完整落地流程
- 需求阶段:评估变更对兼容性的影响,如果是破坏性变更,提前规划 deprecation 周期
- 设计阶段:设计协议、API、数据库的兼容方案,编写迁移脚本
- 开发阶段:实现兼容逻辑,编写单元测试和兼容性测试
- 测试阶段:
- 兼容性测试:新旧版本 SDK 互通测试、数据迁移测试
- 压力测试:验证新版本性能
- 灰度测试:内部小范围测试
- 发布阶段:按灰度策略逐步放量,实时监控指标
- 运维阶段:观察监控数据,出现问题快速回滚,收集用户反馈
八、总结
IM SDK 的多版本兼容和平滑升级是一个长期工程,需要从协议、API、数据、发布、服务端5个维度全面考虑,核心是“向前兼容、向后兼容、可回滚”。通过严格遵循语义化版本、可扩展协议设计、数据库迁移机制、灰度发布策略,可以将升级风险降到最低,保证用户体验的连续性。
设计模式
IM SDK(即时通讯软件开发工具包)的核心场景是长连接管理、消息收发、状态同步、重连机制、事件通知,其架构设计强调高解耦、高可扩展、高稳定性,通常会组合使用多种设计模式来解决不同模块的问题。以下是IM SDK中最常用的设计模式,结合具体业务场景逐一说明:
一、核心高频设计模式
1. 单例模式(Singleton)【连接管理核心】
应用场景
- 全局唯一长连接:一个用户通常只需要一个WebSocket/TCP长连接,避免多个连接导致的资源浪费、消息重复、状态混乱。
- 全局消息队列:统一管理待发送消息、离线消息的队列,确保消息有序。
- 全局配置中心:统一管理SDK的初始化配置、用户信息、Token等。
代码示例(全局WebSocket连接管理)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37
| class IMClient { static #instance = null;
constructor(config) { if (IMClient.#instance) { return IMClient.#instance; } this.config = config; this.ws = null; this.isConnected = false; IMClient.#instance = this; }
static getInstance(config) { if (!IMClient.#instance) { IMClient.#instance = new IMClient(config); } return IMClient.#instance; }
connect() { if (this.isConnected) return; this.ws = new WebSocket(this.config.wsUrl); this.ws.onopen = () => { this.isConnected = true; console.log("IM连接成功"); }; } }
const client1 = IMClient.getInstance({ wsUrl: "wss://im.example.com" }); const client2 = IMClient.getInstance(); console.log(client1 === client2); client1.connect();
|
2. 发布-订阅模式(Publish-Subscribe)【事件通知核心】
应用场景
- SDK内部模块解耦:连接模块、消息模块、状态模块之间通过事件通信,互不感知。
- 向上层应用通知事件:收到新消息、连接状态变化、用户上线/下线、消息发送成功/失败等,通过事件通知上层UI更新。
- 跨页面/跨组件通信:在单页应用中,不同页面的IM状态同步。
代码示例(IM事件总线)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62
| class IMEventEmitter { constructor() { this.events = Object.create(null); }
on(eventName, callback) { if (!this.events[eventName]) this.events[eventName] = []; this.events[eventName].push(callback); }
emit(eventName, ...args) { this.events[eventName]?.forEach(cb => cb(...args)); }
off(eventName, callback) { if (!this.events[eventName]) return; this.events[eventName] = this.events[eventName].filter(cb => cb !== callback); } }
class IMClient { constructor() { this.eventBus = new IMEventEmitter(); this.ws = null; }
connect() { this.ws = new WebSocket("wss://im.example.com"); this.ws.onopen = () => { this.eventBus.emit("connected"); }; this.ws.onmessage = (event) => { const message = JSON.parse(event.data); this.eventBus.emit("message", message); }; this.ws.onclose = () => { this.eventBus.emit("disconnected"); }; } }
const imClient = new IMClient(); imClient.connect();
imClient.eventBus.on("connected", () => { console.log("UI更新:显示在线状态"); });
imClient.eventBus.on("message", (msg) => { console.log("UI更新:渲染新消息", msg); });
imClient.eventBus.on("disconnected", () => { console.log("UI更新:显示离线状态,提示重连"); });
|
3. 状态模式(State)【连接状态管理核心】
应用场景
- 长连接状态流转:连接中、已连接、断开、重连中、已失效等不同状态,对应不同的行为(比如已连接状态下直接发消息,断开状态下先缓存消息再触发重连)。
- 消息状态管理:待发送、发送中、发送成功、发送失败、已撤回等。
- 用户在线状态:在线、离线、忙碌、隐身等。
核心优势
消除大量的 if (state === 'connected') { ... } else if (state === 'disconnected') { ... } 状态判断代码,每个状态的行为独立封装,易扩展、易维护。
代码示例(连接状态管理)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94
| class ConnectedState { constructor(client) { this.client = client; } sendMessage(message) { console.log("直接发送消息:", message); this.client.ws.send(JSON.stringify(message)); } reconnect() { console.log("已连接,无需重连"); } }
class DisconnectedState { constructor(client) { this.client = client; } sendMessage(message) { console.log("缓存消息:", message); this.client.messageQueue.push(message); this.reconnect(); } reconnect() { console.log("开始重连..."); this.client.setState(this.client.reconnectingState); setTimeout(() => { this.client.setState(this.client.connectedState); console.log("重连成功,发送缓存消息"); this.client.flushMessageQueue(); }, 2000); } }
class ReconnectingState { constructor(client) { this.client = client; } sendMessage(message) { console.log("重连中,缓存消息:", message); this.client.messageQueue.push(message); } reconnect() { console.log("重连中,请勿重复操作"); } }
class IMClient { constructor() { this.connectedState = new ConnectedState(this); this.disconnectedState = new DisconnectedState(this); this.reconnectingState = new ReconnectingState(this); this.currentState = this.disconnectedState; this.messageQueue = []; this.ws = { send: () => {} }; }
setState(state) { this.currentState = state; }
sendMessage(message) { this.currentState.sendMessage(message); }
reconnect() { this.currentState.reconnect(); }
flushMessageQueue() { this.messageQueue.forEach(msg => this.sendMessage(msg)); this.messageQueue = []; } }
const imClient = new IMClient(); imClient.sendMessage({ type: "text", content: "你好" }); imClient.sendMessage({ type: "text", content: "在吗" });
|
4. 职责链模式(Chain of Responsibility)【消息处理核心】
应用场景
- 消息发送前处理:序列化 → 加密 → 添加时间戳/消息ID → 签名 → 缓存 → 发送,每个步骤是一个拦截器,组成一条链。
- 消息接收后处理:解密 → 反序列化 → 去重 → 存储 → 分发,依次处理。
- 重连拦截:网络检查 → Token刷新 → 重连策略选择 → 执行重连。
核心优势
每个处理逻辑独立封装,可灵活添加、删除、调整顺序,符合开闭原则。
代码示例(消息发送拦截器链)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73
|
class Interceptor { constructor() { this.next = null; }
setNext(interceptor) { this.next = interceptor; return interceptor; }
handle(message) { if (this.next) { return this.next.handle(message); } return message; } }
class MessageIdInterceptor extends Interceptor { handle(message) { message.id = Date.now().toString() + Math.random().toString(36).substr(2, 9); console.log("添加消息ID:", message.id); return super.handle(message); } }
class TimestampInterceptor extends Interceptor { handle(message) { message.timestamp = Date.now(); console.log("添加时间戳:", message.timestamp); return super.handle(message); } }
class SerializeInterceptor extends Interceptor { handle(message) { const serialized = JSON.stringify(message); console.log("序列化消息:", serialized); return super.handle(serialized); } }
class IMClient { constructor() { this.interceptorChain = new MessageIdInterceptor(); this.interceptorChain .setNext(new TimestampInterceptor()) .setNext(new SerializeInterceptor()); }
sendMessage(message) { const processedMessage = this.interceptorChain.handle(message); console.log("最终发送消息:", processedMessage); } }
const imClient = new IMClient(); imClient.sendMessage({ type: "text", content: "职责链测试" });
|
二、辅助常用设计模式
1. 策略模式(Strategy)
应用场景
- 重连策略:指数退避重连、固定间隔重连、根据网络质量动态调整重连间隔,封装成不同策略灵活切换。
- 消息加密策略:AES、RSA、国密SM2/SM4,根据安全需求选择。
- 存储策略:LocalStorage、IndexedDB、内存存储,根据消息量选择。
2. 代理模式(Proxy)
应用场景
- 消息发送代理:在连接不稳定时,代理先缓存消息,等连接恢复再发送,上层无需关心连接状态。
- 日志/监控代理:对SDK的所有接口做代理,自动添加日志、性能监控、异常上报。
- 权限代理:发送消息前检查用户权限、Token有效性。
3. 工厂模式(Factory)
应用场景
- 消息对象创建:根据消息类型(文本、图片、语音、视频、自定义),工厂动态创建对应的消息对象,上层无需关心创建细节。
- 连接对象创建:根据环境(浏览器WebSocket、Node.js net模块、小程序Socket),工厂创建对应的连接实例。
三、IM SDK设计模式选型总结
| 设计模式 |
核心解决问题 |
IM SDK典型落地 |
| 单例模式 |
全局唯一实例 |
全局长连接、消息队列、配置中心 |
| 发布-订阅模式 |
模块解耦、事件通知 |
连接状态变化、新消息通知、用户状态同步 |
| 状态模式 |
状态流转、消除if-else |
连接状态、消息状态、用户在线状态 |
| 职责链模式 |
消息处理流程 |
消息发送/接收拦截器链 |
| 策略模式 |
算法灵活切换 |
重连策略、加密策略、存储策略 |
| 代理模式 |
访问控制、功能增强 |
消息缓存代理、日志监控代理 |
| 工厂模式 |
对象创建封装 |
消息对象创建、连接对象创建 |
核心原则
IM SDK通常是多种设计模式组合使用,不要为了用模式而硬套,优先保证:
- 核心模块解耦:连接、消息、状态、存储模块独立,通过事件通信。
- 可扩展性:方便添加新的消息类型、新的重连策略、新的加密方式。
- 稳定性:异常处理、重连机制、消息缓存兜底,保证消息不丢失、不重复。
其它重点
20250428-ES5+语法重点.md
20250507-Typescript开发重点.md
20250829-前端常用的设计模式.md
20250103-前端常用的数据库.md