news 2026/9/13 14:27:18

使用 SpacetimeDB 构建 Discord 风格实时聊天应用:从表结构到完整前端实现

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
使用 SpacetimeDB 构建 Discord 风格实时聊天应用:从表结构到完整前端实现

使用 SpacetimeDB 构建 Discord 风格实时聊天应用:从表结构到完整前端实现

【免费下载链接】SpacetimeDBDevelopment at the speed of light项目地址: https://gitcode.com/GitHub_Trending/sp/SpacetimeDB

导读

本文以开源仓库中的完整示例应用Chat App(位于 tools/llm-oneshot/apps/chat-app/typescript/opus-4-5/spacetime/chat-app-20260102-171317)为对象,系统讲解如何在 SpacetimeDB 上仅用 TypeScript 编写服务端(表定义 + Reducer),并用 React 客户端订阅实时数据,搭建一个功能完整的 Discord 风格聊天应用。读完本文,你将掌握:SpacetimeDB 表与索引的声明方式、reducer与生命周期回调的编写、scheduled定时任务表驱动“定时消息/自动过期/自动 Away”的机制,以及 React 客户端通过SpacetimeDBProvider+useTable消费实时订阅的完整链路。

一、应用概览:一个“全功能”实时聊天示例

Chat App 是一个用 SpacetimeDB + React 构建的、功能近乎完整的 Discord 风格聊天应用。其关联 README(chat-app-20260102-171317/README.md)列出了 12 个核心功能模块,本文将其作为骨架逐一展开:

功能模块核心能力底层支撑表 / Reducer
基础聊天显示名、创建/加入/离开房间、实时消息、在线列表userroomroom_membermessage
输入指示“XX 正在输入…”、5 秒无操作自动过期typing_indicator+typing_cleanup_job
已读回执消息下方“已读:X、Y、Z”,实时更新read_receiptroom_member.lastRead*
未读计数房间列表角标,按用户/房间记录最后阅读位置room_member
定时消息未来时间发送、查看与取消待发送消息scheduled_message+scheduled_message_view
阅后即焚1 分钟/5 分钟/1 小时自动删除,带倒计时message.expiresAt+ephemeral_cleanup_job
表情回应👍❤️😂😮😢👎🎉🔥 切换与悬停查看message_reaction
编辑历史编辑自己的消息、(edited)标记、历史回看message_edit
实时权限房主即管理员、踢人/封禁/提升管理员、即时生效banned_userroom_member.isAdmin
丰富在线状态Online/Away/DND/Invisible、离线显示最后活跃时间、5 分钟自动 Awayuser.status+presence_away_job
消息线程回复指定消息、回复计数、线程面板message.parentMessageIdreplyCount
私密房间与 DM邀请制私密房、按用户名邀请、双人私聊、接受/拒绝邀请room_invitationroom.isPrivate/isDm

该示例还配套了一份评分记录(GRADING_RESULTS.md),前 12 个功能模块得分 34.5/36(95.8%),其中功能 1–8、10–12 均为满分,唯一被扣分的功能 9(实时权限)并非后端问题,而是前端在App.tsx中用selectedRoom.isPrivate条件把管理工具限制在了私密房间内——后端对公共房间的踢人/封禁逻辑是完整可用的。这为我们评估“前后端职责边界”提供了很好的参考。

二、项目结构:前后端如何组织

chat-app-20260102-171317/ ├── backend/ │ └── spacetimedb/ │ ├── src/ │ │ ├── schema.ts # 表定义(15 张表) │ │ └── index.ts # Reducer 与生命周期处理器(约 1305 行) │ ├── package.json # 依赖 spacetimedb ^1.11.0,仅 TypeScript │ └── tsconfig.json ├── client/ │ ├── src/ │ │ ├── main.tsx # 入口:SpacetimeDBProvider + 连接构建 │ │ ├── App.tsx # 主应用组件(约 1235 行) │ │ ├── styles.css # 暗色主题样式 │ │ └── module_bindings/ # 生成绑定(发布后需重新生成) │ ├── index.html │ ├── package.json # React 18 + Vite 5 + spacetimedb │ ├── tsconfig.json │ └── vite.config.ts └── README.md

值得强调的是:服务端完全使用 TypeScript(后端package.json中唯一的运行时依赖是spacetimedb: ^1.11.0),无需自建 HTTP/WebSocket 服务;表与业务逻辑即“模块(module)”,一次spacetime publish即部署到 SpacetimeDB 运行时。客户端则使用spacetimedb/react提供的 React 绑定,将服务端表实时同步为前端状态。

三、环境准备与启动步骤

3.1 前置条件

  • Node.js 18+(服务端与客户端均需要)
  • SpacetimeDB CLI(本文所依托仓库的 CLI 实现位于 crates/cli/src,含startpublishgeneratelogslist等子命令)

3.2 后端启动三步走

  1. 启动 SpacetimeDB 服务

    spacetime start

    默认情况下服务监听本地地址,供后续 publish 与客户端连接使用。

  2. 发布模块

    cd backend/spacetimedb spacetime publish chat-app --module-path .

    其中chat-app是模块名(README 中特别注明“Usechat-appas the module name”),--module-path .指向包含schema.ts/index.tspackage.json的模块目录。发布后,服务端会编译模块并建立全部 15 张表。

  3. 生成客户端绑定

    spacetime generate --lang typescript --out-dir ../../client/src/module_bindings --module-path .

    该命令会根据服务端 schema 生成 TypeScript 绑定(DbConnectiontablesreducers等)。README 明确提示:client/src/module_bindings下默认是占位文件,发布后端后必须重新生成,否则客户端无法与真实 schema 对齐。

3.3 客户端启动

cd client npm install npm run dev

然后浏览器打开 http://localhost:3000 即可。此时页面会通过 WebSocket 连接本地 SpacetimeDB(连接地址见 client/src/main.tsx 中的SPACETIMEDB_URI = 'ws://localhost:3000')。

四、服务端设计:schema.ts 的 15 张表

服务端表定义全部集中在 backend/spacetimedb/src/schema.ts,使用schema/table/t三个 API 组合声明。注意本示例中表名大小写不统一(user小写、Room等大写),但最终表名由name字段指定,导出时全部注册进spacetimedbschema。

4.1 用户与房间域

  • user:以identityt.identity())为主键,含name(可选显示名)、onlinestatus(取值online | away | dnd | invisible)、lastActiveAtconnectionId;建立了名为by_name的 btree 索引用于按用户名检索(start_dminvite_to_room等 Reducer 依赖它)。
  • roomidt.u64().primaryKey().autoInc()(自增主键),含nameownerIdisPrivateisDmcreatedAtby_owner索引。
  • room_member:房间成员表,by_room/by_user双索引;字段isAdmin标记管理员,lastReadMessageId/lastReadAt记录“每个用户在每个房间的最后阅读位置”——这正是未读计数与已读回执的数据来源。
  • banned_user:封禁记录,含bannedBybannedAt、可选reason
  • room_invitation:邀请记录,status取值pending | accepted | declinedby_room/by_invitee双索引。

4.2 消息域

  • messageby_room/by_sender/by_parent三个索引;parentMessageId(可选)与replyCount支撑线程;expiresAt(可选)支撑阅后即焚。
  • message_edit:编辑历史(messageIdpreviousContenteditedAt)。
  • message_reaction:表情回应(messageIduserIdemoji)。
  • read_receipt:按消息记录“谁看过”(messageIduserIdseenAt)。

4.3 定时任务表(scheduled 表)

这是 SpacetimeDB 最具特色的部分——用普通表声明定时任务,字段声明里通过scheduled: '<reducer 名>'指定到期时自动触发的 Reducer,再以scheduledId(自增主键)+scheduledAtt.scheduleAt())声明任务时间:

  • typing_cleanup_jobrun_typing_cleanup:输入指示 5 秒后自动清理;
  • scheduled_messagerun_scheduled_message:定时消息到期自动发送;
  • scheduled_message_view:非调度表,公开表,供用户查看/取消自己的待发送消息(只读视图 + 索引by_room/by_sender);
  • ephemeral_cleanup_jobrun_ephemeral_cleanup:阅后即焚消息到期删除;
  • presence_away_jobrun_presence_away:用户 5 分钟无操作自动置为 Away。
// 以定时消息表为例(schema.ts 中精简示意) export const ScheduledMessage = table( { name: 'scheduled_message', scheduled: 'run_scheduled_message', // 到期触发该 reducer }, { scheduledId: t.u64().primaryKey().autoInc(), scheduledAt: t.scheduleAt(), // 触发时间 roomId: t.u64(), senderId: t.identity(), content: t.string(), createdAt: t.timestamp(), } );

最终通过schema(User, Room, ... , PresenceAwayJob)一次性导出,服务端即完成全部建表。

五、服务端逻辑:index.ts 的 Reducer 设计

业务逻辑集中在 backend/spacetimedb/src/index.ts(约 1305 行),所有 Reducer 均通过spacetimedb.reducer('name', { 参数 }, (ctx, args) => {...})注册,并大量使用SenderError抛出对调用方可见的校验错误。

5.1 生命周期:clientConnected / clientDisconnected

连接建立时(index.ts#L33-L56):已存在的用户被置为onlineinvisible状态保持不变并保留隐身),更新lastActiveAtconnectionId;新用户自动插入一行(name为空,稍后通过set_name设置)。随后调用schedulePresenceAway安排自动 Away。

断开时(index.ts#L58-L73):置online: false、清空connectionId,并顺手清理该用户残留的 typing 指示。

spacetimedb.clientConnected(ctx => { const user = ctx.db.user.identity.find(ctx.sender); if (user) { ctx.db.user.identity.update({ ...user, online: true, status: user.status === 'invisible' ? 'invisible' : 'online', lastActiveAt: ctx.timestamp, connectionId: ctx.connectionId, }); } else { ctx.db.user.insert({ identity: ctx.sender, name: undefined, online: true, status: 'online', ... }); } schedulePresenceAway(ctx, ctx.sender); });

5.2 用户与房间

  • set_name/set_status:前者校验非空、长度 ≤ 50;后者校验状态必须是['online','away','dnd','invisible']之一。
  • update_activity:客户端每分钟心跳一次(见 App.tsx 中setInterval(() => conn.reducers.updateActivity({}), 60000)),将状态重置为online并重新调度 Away 任务。
  • create_room:房主即管理员(isAdmin: true),创建者自动入房;join_room会依次校验私密性、封禁、重复入房;leave_room删除对应room_member行。

5.3 私密房间与 DM

  • start_dm(index.ts#L247-L318):按用户名查找目标用户,禁止给自己发 DM,遍历现有 DM 去重,然后创建isDm: true的房间并为双方各插入一条isAdmin: true的成员记录。
  • invite_to_room:仅管理员可邀请、仅私密房可邀请;会检查被邀请者是否已是成员、是否已有 pending 邀请。
  • respond_to_invitation:仅受邀者本人可响应,pending状态不可重复响应;接受则更新状态并插入成员行,拒绝则仅更新状态。

5.4 权限管理

kick_user/ban_user/unban_user/promote_to_admin遵循一致的模式:先验证调用者是房间管理员,再按by_name索引查找目标用户,最后操作成员表或封禁表。例如ban_user会先删除目标用户的成员资格、再写入banned_user行;所有权限变更都发生在单次事务性 Reducer 内,因此权限更新即时生效——这正是“Real-Time Permissions”的底层保证。

5.5 消息、反应、已读、输入指示

  • send_message:校验非空、长度 ≤ 4000、成员资格;支持parentMessageId回复,并原子地replyCount + 1;发送成功后清理自己的 typing 指示。
  • send_ephemeral_messagedurationSeconds必须在 10 秒~1 小时之间,计算expiresAt = timestamp + duration,写入message.expiresAt并插入ephemeral_cleanup_job
  • edit_message:仅作者可编辑;写入message_edit历史行后更新正文并置isEdited: true
  • delete_message:作者或房间管理员可删;级联清理反应、已读回执、编辑历史,并维护父消息的replyCount
  • toggle_reaction:白名单校验 8 个 emoji,若已点过同一 emoji 则删除(实现“切换”语义)。
  • start_typing/stop_typing:写入typing_indicator并插入 5 秒清理任务(TYPING_TIMEOUT_SECONDS = 5n)。
  • mark_messages_read:更新room_memberlastReadMessageId/lastReadAt,并为指定消息幂等地插入read_receipt

5.6 定时任务 Reducer(被 scheduled 表驱动的处理器)

  • run_scheduled_message(index.ts#L1150-L1202):到期后若房间仍存在且发送者仍是成员,则将消息写入message表,并同步从scheduled_message_view移除记录。
  • run_ephemeral_cleanup:到期后级联删除反应与已读回执,再删除消息本身,实现“永久删除”。
  • run_typing_cleanup:仅当指示已过期(expiresAt <= now)才删除,避免误删刚刷新的指示。
  • run_presence_away:仅当用户仍在线且状态为online、且不活跃时长 ≥ 300 秒时才置为awayschedulePresenceAway辅助函数会先取消旧任务再插入新任务(index.ts#L1279-L1300)。

六、客户端设计:React 如何消费实时数据

6.1 连接构建与令牌持久化

入口文件 client/src/main.tsx 展示了标准连接流程:

const SPACETIMEDB_URI = 'ws://localhost:3000'; const MODULE_NAME = 'chat-app'; const connectionBuilder = useMemo(() => { const onConnect = (conn, identity, token) => { window.__db_conn = conn; window.__my_identity = identity; if (token) localStorage.setItem('auth_token', token); conn.subscriptionBuilder().subscribeToAllTables(); // 订阅全部表 }; const onConnectError = (_ctx, err) => { if (err.message?.includes('Unauthorized') || err.message?.includes('401')) { localStorage.removeItem('auth_token'); window.location.reload(); } }; return DbConnection.builder() .withUri(SPACETIMEDB_URI) .withModuleName(MODULE_NAME) .withToken(localStorage.getItem('auth_token') || undefined) .onConnect(onConnect) .onConnectError(onConnectError) .onDisconnect(onDisconnect); }, []); <SpacetimeDBProvider connectionBuilder={connectionBuilder}> <App /> </SpacetimeDBProvider>

要点:连接令牌持久化在localStorageauth_token)中实现会话延续;鉴权失败(401/Unauthorized)时自动清除令牌并刷新页面;连接成功后通过subscribeToAllTables()一次性订阅全部表(表量大时可按需订阅)。

6.2 useTable 实时订阅

App.tsx 用useTable(tables.xxx)批量订阅服务端表:

const [users] = useTable(tables.user); const [rooms] = useTable(tables.room); const [messages] = useTable(tables.message); const [typingIndicators] = useTable(tables.typingIndicator); const [messageReactions] = useTable(tables.messageReaction); // ...readReceipt / messageEdit / bannedUser / roomInvitation / scheduledMessageView

服务端任何表变更都会实时推送到这些响应式数组;客户端逻辑随后以useMemo派生出可见房间、我的成员关系、未读数、线程回复、编辑历史等视图状态。消息列表在messages变化时自动滚动到底部;updateActivity心跳每 60 秒执行一次,驱动服务端的“自动 Away”判定。

6.3 UI 与交互

README 中列出的 UI 特性与代码一一对应:Discord 风格暗色主题(styles.css)、房间/DM 双 Tab 侧边栏、未读角标、状态指示、悬停反应选择器、线程面板、调度弹窗、加载/空状态等。管理类操作(踢人/封禁/提升)通过conn.reducers.kickUser(...)等生成绑定方法触发;GRADING_RESULTS.md同时提示了一个可复用的经验:将管理工具限制在私密房间的前端条件(selectedRoom.isPrivate)导致公共房间缺少管理入口,后端逻辑本身并无问题。

七、运行与验证建议

  1. 按序执行spacetime startspacetime publish chat-app --module-path .spacetime generate --lang typescript --out-dir .../module_bindings --module-path .npm install && npm run dev
  2. 务必重新生成绑定:README“Notes”强调module_bindings内是占位文件,发布后需重新spacetime generate,否则DbConnection/tables与真实 schema 不一致。
  3. 移除 StrictMode:README“Notes”指出应去掉<React.StrictMode>,因为它会干扰 WebSocket 连接生命周期(React 18 开发模式下 StrictMode 的双调用会重复建立连接)。
  4. 多开验证实时性:开两个浏览器窗口分别设置不同显示名,即可直观验证实时消息、输入指示、已读回执、表情回应与在线状态的即时同步。
  5. 结合评分清单自查:可对照 GRADING_RESULTS.md 中的 15 个功能验收标准逐项验证,例如“踢人后立即失去访问权限”“定时消息准时出现”“阅后即焚到期彻底删除”。

八、小结:该示例带给 SpacetimeDB 开发者的启示

从本文示例可以提炼出几条可直接复用的 SpacetimeDB 开发范式:

  • 表即状态、Reducer 即业务:全部服务端逻辑(含权限校验)收敛在 TypeScript 模块内,客户端通过生成绑定调用,天然获得实时广播能力;
  • scheduled 表驱动一切“到时执行”:输入指示过期、定时消息、阅后即焚、自动 Away 全部用scheduled: 'reducer_name'+t.scheduleAt()声明式完成,无需外部任务队列;
  • 索引设计决定查询模式by_room/by_user/by_name等 btree 索引与 Reducer 中的.filter()一一对应,多对多关系(成员、封禁、邀请、反应、回执)都以此为基础;
  • 前后端契约由spacetime generate维系:修改 schema 后重新生成绑定,客户端类型与 Reducer 自动对齐,是避免运行时错位的关键步骤。

如果你希望在此基础上继续扩展,仓库中还提供了sdk-test-*系列模块(见 modules 目录)与完整的 TS SDK 源码(bindings-typescript/src),可作为学习useTable之外更多 API(如按条件订阅、事务、定时任务参数化)的补充资料。

【免费下载链接】SpacetimeDBDevelopment at the speed of light项目地址: https://gitcode.com/GitHub_Trending/sp/SpacetimeDB

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/13 14:26:54

Python图像信息隐藏实战:LSB+DCT混合嵌入与工程化部署

简介&#xff1a;本资源是一套完整的Python毕业设计项目&#xff0c;面向计算机专业本科生及信息安全初学者&#xff0c;聚焦图像信息隐藏&#xff08;隐写术&#xff09;技术的工程化实现&#xff0c;解决数字内容版权保护、敏感信息隐蔽传输等实际问题。压缩包共322个文件&am…

作者头像 李华
网站建设 2026/9/13 14:26:27

Zernike系数到PSF:光学仿真中zernike_psf原理与MATLAB实现

简介&#xff1a;这是一份面向光学工程与视觉科学研究者的波前光学与Zernike像差分析工具包&#xff0c;围绕点扩散函数&#xff08;PSF&#xff09;计算与成像质量评估展开&#xff0c;可帮助理解Zernike系数如何影响系统成像分辨率&#xff0c;在光学设计、视觉模型验证与成像…

作者头像 李华
网站建设 2026/9/13 14:23:03

GPU服务器远程开发实战:SSH免密、VSCode/PyCharm与端口转发

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/13 14:22:06

Nebius AI Builder免费计划深度实测:400美元GPU算力如何加速RAG开发

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华