海狸IM 2.0 双端客户端技术实践:Electron 与 Flutter 如何对齐 IM 协议
接《海狸IM 2.0 服务端技术解析》,46 号文讲了 SendMsg → RocketMQ → WebSocket 和 seq + datasync 离线补消息。客户端这边的问题是:Electron 和 Flutter 两套 UI,怎么保证同一条消息在 PC 和手机上一致?
这篇把双端客户端拆成几个技术点分析:分层架构、乐观发送、增量 sync、WS 落库、本地 DB 设计、2.0 功能对齐。IM 客户端同样能写成一个系列------本篇是总篇,后面可以单拆 Drift 同步、Electron 多窗口、LiveKit 来电等。
1. 双端技术栈:不是两个 IM,是一套协议
| 维度 | PC(beaver-desktop 2.0.0) | 移动(beaver-flutter 2.0.1) |
|---|---|---|
| UI | Vue 3 + TypeScript + Vite | Flutter 3.x |
| 壳 | Electron 31 | iOS / Android |
| 本地 DB | Drizzle ORM + better-sqlite3 | Drift(SQLite) |
| 状态 | Pinia + 主进程 Store | BLoC + MessageStore |
| 网络 | REST + WebSocket | REST + WebSocket |
| 同步 | datasync 摘要 + chatSync 区间拉 | 同左,代码几乎镜像 |
2.0 起 Flutter 是移动端唯一主线,Uniapp 已停更。双端共用:
- 同一套 网关 REST
- 同一套 WS command / type
- 同一套 seq 语义
- 同一套 fileUrl 媒体字段
协议一改,双端必须一起改------这是维护双端 IM 的铁律。


2. 客户端四层架构(二次开发先记这个)
无论 Electron 还是 Flutter,海狸客户端都可以抽象成四层:
┌─────────────────────────────────────────┐
│ UI 层 │
│ 会话列表 / ChatWindow / ReplyBar / 设置 │
├─────────────────────────────────────────┤
│ Store 层 │
│ MessageStore、ConversationStore、User... │
├─────────────────────────────────────────┤
│ Business 层 │
│ 发消息、handleNewWSMessage、同步调度 │
├─────────────────────────────────────────┤
│ Data 层 │
│ SQLite 表 + API Client + WebSocketClient │
└─────────────────────────────────────────┘
│ REST │ WebSocket
└──────── beaver-server ────────┘
| 你想改... | 动哪层 |
|---|---|
| 气泡样式、Markdown 渲染 | UI |
| 未读数、列表排序 | Store + Business |
| 发送失败重试策略 | Business |
| 离线拉取频率、批量大小 | Data + datasync |
| 表结构、索引 | Data(DB schema) |
反模式 :在 Vue 组件里直接 axios.post 发消息------短期快,长期和 WS / 本地库状态打架。
3. Electron 双进程:为什么 PC 端多一层
beaver-desktop 是典型 Electron 主进程 + 渲染进程:
| 进程 | 职责 |
|---|---|
| 主进程 | SQLite、WS 连接、datasync 调度、系统托盘、窗口管理 |
| 渲染进程 | Vue UI,通过 IPC 读 Store / 发命令 |
消息同步器 MessageSync 跑在 主进程 main/datasync/chat/chat-message.ts,和数据库同进程,避免渲染进程卡 UI 时拖慢 sync。
渲染进程通过 IPC 收到 NotificationChatCommand 一类事件刷新界面------数据和 UI 解耦,这是 Electron IM 的常见打法(Slack 桌面版同类思路)。
4. 发消息:乐观 UI + REST 确认
用户点「发送」,双端流程高度一致:
4.1 六步流程
1. 生成本地临时 messageId(可选)
2. 插入本地 DB,status = sending(乐观 UI 立刻显示)
3. POST chat send API
4. 成功:写入服务端 seq,status = sent
5. 失败:status = failed,UI 显示重试
6. 其它在线成员:WS 推送;离线成员:下次 sync
为什么乐观 UI? REST 往返 50~300ms,不等服务器就画气泡,体验才跟得上微信。失败再标红重试,比「转圈半秒再出现」好得多。
4.2 2.0 扩展操作
| 操作 | 路径 |
|---|---|
| 回复 | 带 replyTo messageId,ReplyBar 组件 |
| 编辑 | edit API,双端 EditBar |
| 撤回 | recall API,本地改状态或插通知消息 |
| 转发 | 选目标会话,再走 send |
这些在 2.0 都是 REST 写 + WS/sync 传播,没有单独搞一套「编辑专用 WS」。
5. 增量同步:双端镜像实现(重点)
离线同步是 IM 客户端 最难写对 的部分。海狸 PC 和 Flutter 用了 同一算法,下面用 PC 源码讲(Flutter 几乎一一对应)。
5.1 核心表:chat_sync_status
PC 端 Drizzle 定义:
typescript
export const chatSyncStatus = sqliteTable('chat_sync_status', {
conversationId: text('conversation_id').notNull(),
module: text('module').notNull(), // 'message' | 'conversation' | ...
seq: integer('seq').default(0), // 消息同步进度
version: integer('version').default(0),
updatedAt: integer('updated_at'),
})
每个会话一条 module=message 的记录,表示「这个会话我同步到第几条 seq 了」。
5.2 三步同步算法
Step 1 --- 拉摘要
typescript
const serverResponse = await datasyncGetSyncChatMessagesApi({
since: lastSyncTime, // 本地游标:上次同步时间戳
})
// 返回:各 conversationId 的 serverMaxSeq
Step 2 --- 对比 seq
typescript
for (const [conversationId, serverSeq] of serverConversationMap) {
const localSeq = localVersionMap.get(conversationId) || 0
if (serverSeq > localSeq) {
needSyncConversations.push({ conversationId, serverSeq })
}
}
Step 3 --- 区间拉消息
typescript
await chatSyncApi({
conversationId,
fromSeq: localSeq + 1,
toSeq: serverSeq,
limit: 100,
})
// batchUpsert 到本地 messages 表
// 更新 chat_sync_status.seq = serverSeq
Flutter 端 lib/core/datasync/chat/message.dart 逻辑相同,只是语言换成 Dart。

5.3 为什么用「时间戳摘要 + seq 拉取」两层?
| 只用 seq | 只用时间戳 |
|---|---|
| 不知道哪个会话变了,要扫全库 | 时钟漂移会漏消息 |
| 摘要告诉你会话列表 | seq 保证会话内顺序和增量 |
两层合用:摘要 O(变更会话数) ,拉取 O(缺口消息数),千群用户也不会每次全量拉。
5.4 和 46 号文服务端如何配合
服务端 sendmsg 后更新 datasync 摘要;客户端 since 游标推进。闭环。
6. WebSocket:在线快车道
6.1 下行包处理(Flutter 示例)
MessageBusiness.handleNewWSMessage 大致步骤:
- 解析 WS:
messageId、conversationId、seq、msgJSON - Upsert 本地 Drift(防重复:messageId 唯一)
MessageStore.addMessage若当前正在该会话,立刻渲染ConversationBusiness.notifyConversationUpdate更新列表预览、未读
PC 主进程收到 MQ 转 WS 后,同样落 SQLite,再 IPC 通知 Vue。
6.2 WS 与 sync 会不会重复?
会收到重复包,靠 messageId / seq upsert 去重。UI 层只读 DB,不维护「WS 专用列表」和「HTTP 专用列表」两套状态------这是稳定性关键。
6.3 tableUpdates 结构
46 号文提到:服务端 WS 包可能带 tableUpdates(messages / conversations / user_conversations)。客户端解析后 按表批量 upsert,一次推送更新消息 + 会话未读 + 列表排序,减少 UI 抖动。
7. 本地数据库设计要点
7.1 PC:Drizzle + WAL
typescript
sqlite.pragma('journal_mode = WAL')
sqlite.pragma('synchronous = NORMAL')
WAL 模式读写并发更好,聊天窗口滑动时 sync 写库不容易卡 UI。
7.2 Flutter:Drift + 批量写入
同步拉到的消息 batchCreate / upsert,单会话上百条一次事务提交。Flutter 端列表用 ListView.builder,长历史不会一次 build 万条。
7.3 消息表核心字段(概念)
| 字段 | 用途 |
|---|---|
| messageId | 全局唯一,去重 |
| conversationId | 所属会话 |
| seq | 排序、sync |
| msgType | 文本/图/语音... |
| msg | JSON 消息体 |
| sendStatus | 本地 sending/sent/failed |
8. 2.0 功能点技术拆解
8.1 Markdown 文本
文本消息服务端存 HTML 或 Markdown(以实现为准),双端渲染组件支持代码块、链接。PC 大窗阅读体验更好。
8.2 语音消息与媒体态
- 录制 → file 上传 → 消息体带
fileUrl+duration - 「未听」不是改 msg JSON,而是 markMessageMedia API
- 另一端 datasync 拉媒体变更,更新图标
2.0.1 修过跨端不同步,说明 媒体态必须走独立同步 ,别在文本字段里塞 listened: true。
8.3 回复 / 编辑 UI 组件
| 组件 | 端 |
|---|---|
| ReplyBar | 双端 |
| EditBar | 双端 |
引用消息存 replyToMessageId,编辑走 edit API。UI 层要处理「编辑中禁用发送」「编辑后替换原气泡」。

8.4 PC 独立设置中心
2.0 Electron 把设置从聊天窗拆出来:
| 功能 | 技术 |
|---|---|
| 改密 | user API |
| 设备列表 / 踢下线 | user 设备 RPC |
| 快捷键 | user Settings API,JSON 存服务端 |
| 关于 / 更新 | Electron updater |


换电脑登录,快捷键跟着账号走------这是 成品 IM 和 demo 的差别。
8.5 Flutter 2.0 模块地图
| 模块 | 路径/技术 |
|---|---|
| 增量 sync | lib/core/datasync/chat/ |
| 发消息/WS | lib/core/business/chat/message.dart |
| 状态 | lib/store/message/ |
| 动态 Moment | moment 相关 store + API |
| 表情 | 系统/收藏/商店 |
| 音视频 | LiveKit + IncomingCallBanner |


8.6 群助手 Webhook(PC 配置)
群详情 → 群助手 → 通知机器人 → 复制 Webhook URL。外部 POST,服务端写群消息,双端 sync/WS 正常收。

配置在 PC,手机只 收消息------45 号文有产品说明。
8.7 音视频 LiveKit
移动端 IncomingCallBanner 横幅;PC 有通话 UI。信令走 call 模块,不和 chat MQ 混。二次开发通话别改 sendmsg。
9. PC vs Flutter:场景分工
| 场景 | 推荐端 | 原因 |
|---|---|---|
| 办公大窗、群管理 | PC | 多窗口、群助手 |
| 移动、弱网 | Flutter | Drift 离线 |
| 音视频来电 | Flutter 体验完整 | 横幅 + 后台 |
| 群 Webhook 配置 | PC | 2.0 入口在群助手 |
| OAuth 扫码 | PC 展示 + 手机扫 | 标准流程 |
同一账号、同一 seq、同一 fileUrl------不是两个半成品拼盘。
10. 双端对齐检查清单(发版前自用)
| 检查项 | PC | Flutter |
|---|---|---|
| 新发文本 seq 正确 | ☐ | ☐ |
| 离线 8h 后 sync 无漏 | ☐ | ☐ |
| 图片 fileUrl 双端可开 | ☐ | ☐ |
| 语音未听态 cross-device | ☐ | ☐ |
| 撤回后双端一致 | ☐ | ☐ |
| 编辑后双端一致 | ☐ | ☐ |
| 群 @ 全体成员(若有) | ☐ | ☐ |
| WS 断线重连 + sync | ☐ | ☐ |
IM 回归就测这些,比「点一遍所有菜单」有效。
11. 性能:IM 客户端容易踩的坑
11.1 列表
- 必须 虚拟列表(ListView.builder / 等价)
- 图片 缓存(cached_network_image 等)
- 会话列表 不要 每次 WS 全量 sort 全库
11.2 数据库
- 批量 upsert 用事务
- conversationId + seq 索引
- 历史消息分页加载,进聊天窗再拉旧 seq
11.3 WebSocket
- 心跳 + 指数退避重连
- 重连成功后 trigger checkAndSync(),补断线期间缺口
11.4 Electron 特有
- 大文件下载放主进程或 worker
- 多窗口 共享同一份 DB 连接(海狸在主进程)
12. 和 Uniapp 旧版对比(为何 2.0 换 Flutter)
| 维度 | Uniapp 旧版 | Flutter 2.0 |
|---|---|---|
| 渲染 | WebView 为主 | 原生渲染 |
| 长列表 | 易卡 | 60fps 可预期 |
| 本地库 | 弱 | Drift 完整 SQL |
| 音视频 | 受限 | LiveKit 原生集成 |
| 维护 | 停更 | 主线 |
43~41 号文章讲过 Flutter 重构故事;这篇从 协议对齐 角度补充:换框架不换协议,服务端 seq/datasync 不用推倒重来。
13. 二次开发:改哪里最快出效果
想改 UI 主题
→ desktop src/render / flutter lib/ui
想加消息类型
→ 服务端 proto(46 号文)+ 双端渲染组件 + send 逻辑
想改离线策略
→ MessageSync.checkAndSync 双端各一份,保持行为一致
想接推送
→ notification 模块 + FCM/APNs(2.1 在补 Flutter Token 注册)
14. 常见问题
Q:PC 有消息,手机没有?
看手机 sync 游标和 seq;是否 WS 在线只影响实时,最终应 sync 到。
Q:重复消息两条?
查 upsert 是否按 messageId 唯一;WS 和 sync 是否都去重。
Q:发送一直 sending?
查 REST 是否 401、网关是否通、本地是否没处理失败态。
Q:能不能只做 Web 不做 Electron?
协议支持,但 2.0 官方成品是 Electron + Flutter;Web 需自研或后续版本。
15. 后续系列:客户端也能拆很多篇
| 计划篇目 | 内容 |
|---|---|
| Drift 消息表设计与索引 | Flutter 专项 |
| Electron 主进程 DB 与 IPC | desktop 专项 |
| ReplyBar / EditBar 实现细节 | 双端 UI |
| LiveKit 来电与通话状态机 | call + Flutter Banner |
| 表情商店与收藏同步 | emoji 模块 |
| 动态 Moment 社交链 | moment |
| 客户端全局搜索实现 | desktop 强于 flutter |
16. 小结
海狸 IM 2.0 客户端的技术含金量:
- Electron + Flutter 双端,共用 REST + WS + seq
- datasync 摘要 + chatSync 区间拉 ,PC/Flutter 镜像实现
- 乐观发送 + WS/sync 去重,生产级体验
- fileUrl 统一媒体,语音媒体态独立同步
- 2.0 消息能力全:回复、编辑、撤回、Markdown、语音、动态
做 IM 客户端的同学,建议 双仓库对照读 MessageSync 和 sendmsglogic------服务端怎么写 seq,客户端就怎么拉 seq,对上号就通了。
相关链接
项目源码:
- 后端源码:https://github.com/wsrh8888/beaver-server
- 移动端Flutter源码:https://github.com/wsrh8888/beaver-flutter
- PC端源码:https://github.com/wsrh8888/beaver-desktop.git
- 后台管理系统源码:https://github.com/wsrh8888/beaver-manager
- 开放平台门户源码:https://github.com/wsrh8888/beaver-open
- OAuth 授权登录页源码:https://github.com/wsrh8888/beaver-oauth
学习资源:
核心教学视频:
- 本地搭建教程合集:https://space.bilibili.com/269553626/lists/6075764?type=season
- 服务器部署教程合集:https://space.bilibili.com/269553626/lists/6075828?type=season
上一篇:海狸IM 2.0 服务端技术解析:go-zero 微服务与消息链路(46.海狸IM 2.0 服务端技术解析:go-zero 微服务与消息链路.md)
系列回顾:
- 海狸IM 2.0 发版记录:六端工程拆分与主要变更说明(44.海狸IM 2.0 正式发布:全端能力大升级.md)
- 海狸IM 2.0 开放能力说明:OAuth2 接入与群推送机器人(45.海狸IM 2.0:开放平台、OAuth与Bot,让IM不止于聊天.md)