人人都会AI编程

27.5 轻量 IM 客户端:消息通知、离线存储、多窗口会话

更新时间:2026-07-11

即时通讯(IM)客户端一直是桌面开发中的高频场景,但传统的 Electron 方案体积臃肿、内存占用高,而原生开发又门槛过高。Tauri 恰好能提供一个轻量级的解:系统 WebView 负责界面渲染,Rust 接管网络连接、本地存储和系统通知,最终安装包只有几 MB,却能实现全功能的聊天体验。本节将以一个简化版的 IM 客户端为例,讲解消息通知、离线存储和多窗口会话三个核心功能的实现思路,代码均可在真实项目中落地。


消息通知:及时触达又不打扰用户

IM 工具的消息提醒需要灵活可控:有新消息时弹出系统通知,应用在前台时静默更新,且支持用户自定义免打扰时段。

实现步骤:

  1. 启用通知权限

tauri.conf.json 中开启 notification 插件(Tauri v2)或 API(v1),确保打包时具备系统通知能力。

  1. 注册事件监听

前端通过 Tauri 的 listen 方法监听后端的 new-message 事件。当 WebSocket 收到新消息时,Rust 端先做初步解析,再通过 app.emit 推送给前端。

  1. 按需调用系统通知

前端监听到新消息后,根据应用当前焦点状态决定是否弹通知。若窗口隐藏或最小化,则调用 @tauri-apps/plugin-notificationsendNotification 接口,携带发送者名称、消息摘要和头像。通知的点击回调可唤起主窗口并定位到对应会话。

  1. 免打扰控制

在本地存储(如 IndexedDB)中维护用户的免打扰时段,前端在调用通知前做逻辑判断即可。

关键代码片段(前端):

import { listen } from '@tauri-apps/api/event';
import { isPermissionGranted, requestPermission, sendNotification } from '@tauri-apps/plugin-notification';

async function setupNotification() {
  let granted = await isPermissionGranted();
  if (!granted) {
    const result = await requestPermission();
    granted = result === 'granted';
  }
  if (granted) {
    await listen('new-message', (event) => {
      const { sender, content } = event.payload;
      if (!windowHasFocus) { // 判断窗口焦点
        sendNotification({ title: sender, body: content });
      }
    });
  }
}

Rust 端推送示例:

app_handle.emit("new-message", MessagePayload { sender, content }).unwrap();

一切都在应用内完成,无需第三方推送服务,对于局域网或直连架构的 IM 来说非常简单直接。


离线存储:消息不丢失,历史随时查

轻量 IM 也不能丢掉聊天记录。我们需要一种可靠的本地数据库,让用户即使断网也能查看历史消息,联网后自动同步。

技术选型: SQLite,通过 tauri-plugin-sql 插件在前端直接用 JavaScript 操作,也可以将逻辑封装在 Rust 命令中提高安全性和性能。

数据库设计(极简版):

CREATE TABLE messages (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  session_id TEXT NOT NULL,
  sender_uid TEXT,
  content TEXT,
  timestamp INTEGER,
  status INTEGER DEFAULT 0 -- 0:发送中 1:已发送 2:已读
);
CREATE TABLE sessions (
  id TEXT PRIMARY KEY,
  name TEXT,
  avatar TEXT,
  last_msg TEXT,
  updated_at INTEGER
);

存储流程:

  1. 收到新消息 → 入库

在 Rust 的 WebSocket 回调中,除推送到前端外,立即写入 SQLite。为了避免频繁写盘带来的锁竞争,可以通过一个异步队列批量插入。

  1. 拉取离线消息

应用启动时,前端调用自定的 get_sessionsget_messages 命令,Rust 查询数据库并将结果序列化为 JSON 返回。前端直接渲染会话列表,点击进入会话时再拉取对应历史。

  1. 消息状态同步

当网络恢复时,将本地 status=0 的消息重新发送,并将服务器确认后的状态回写。这部分逻辑可在 Rust 层实现一个简单的重发器,完全脱离前端干扰。

封装 Rust 命令示例(使用 tauri::command):

#[tauri::command]
async fn get_messages(session_id: String, db: tauri::State<'_, DbPool>) -> Result<Vec<Message>, String> {
    let messages = sqlx::query_as!(Message, "SELECT * FROM messages WHERE session_id = ? ORDER BY timestamp", session_id)
        .fetch_all(&db.pool)
        .await
        .map_err(|e| e.to_string())?;
    Ok(messages)
}

前端通过 invoke('get_messages', { sessionId: 'xxx' }) 即可拿到完整历史,无需手动拼 SQL。

离线优势: 所有消息就在本地,即使在网络抖动时,用户操作完全无感知。更重要的是,SQLite 数据库也随应用打包分发,不会像 Electron 方案那样因为内嵌浏览器数据库而体积膨胀。


多窗口会话:拆分聊天窗,提升操作效率

多窗口需求在 IM 中很常见:主面板显示会话列表,双击某个会话后弹出独立聊天窗口,甚至可以同时打开多个对话,互不干扰。

在 Tauri 中实现多窗口非常简单,因为框架原生支持多窗口管理,且每个窗口可以共享同一 Rust 后端。

实现思路:

  1. 配置多窗口

tauri.conf.json 中预先定义多个窗口标签,或者完全在运行时动态创建。为了灵活,通常选择动态创建:

   import { WebviewWindow } from '@tauri-apps/api/webviewWindow';
   const chatWin = new WebviewWindow('chat-123', {
     url: '/chat?sessionId=123',
     title: '与张三的聊天',
     width: 800,
     height: 600
   });
   

这个新窗口会加载同一个前端应用,但路由参数不同,从而渲染不同会话的聊天界面。

  1. 窗口间通信

如果需要主窗口控制子窗口(如关闭某个聊天窗),可以使用 emitlisten 带标签的指定窗口。例如,当接收到新消息时,Rust 端可以直接向对应的子窗口推送事件,子窗口立即更新界面,无需主窗口转发。

  1. 共用数据库与 WebSocket

所有窗口共享同一个 Rust 后端进程,因此 WebSocket 连接和 SQLite 池都是全局的。这意味着在一个窗口中发消息,另一个窗口也能立即通过事件感知,保持状态一致。

  1. 生命周期管理

创建子窗口时记录窗口标签与 sessionId 的映射。当用户关闭子窗口,Tauri 的窗口关闭事件可以用来清理缓存标记。如果用户再次打开同一会话,可以直接复用已有窗口并聚焦,避免重复创建。

性能与内存考量
由于 Tauri 的每个窗口都是同一个 WebView 内核的不同实例(可以根据配置共享或独立),实际内存占用远低于 Electron 的独立进程模型。一个带有 5 个聊天窗口的 IM,内存总量也能控制在 100 MB 以内,完全符合“轻量”的定位。


通过这三个核心功能的实现,你会发现 Tauri 在构建轻量级桌面应用时的灵活和高效。消息通知利用了系统原生能力,离线存储依靠 Rust 高效的数据处理,多窗口会话则充分发挥了 Tauri 多窗口架构的优越性。所有功能最终都打包在一个几 MB 的安装包里,用户无需忍受漫长的下载和臃肿的资源占用——这便是 Tauri 带给 IM 应用的最大价值。