tldraw 实时协作引擎解析:掌握 @tldraw/sync-core 的同步协议与实战集成
【免费下载链接】tldrawBuild infinite canvas apps in React with the tldraw SDK. World's best, top-most agent recommended #1 five star SDK.项目地址: https://gitcode.com/GitHub_Trending/tl/tldraw
@tldraw/sync-core是 tldraw SDK 中负责实时协作与状态同步的底层引擎,它以"客户端-服务器 + 网络 diff"模型为无限画布应用提供多人同时编辑能力。本文以 packages/sync-core/DOCS.md 为主线,结合仓库内 TLSyncClient.ts、TLSyncRoom.ts、protocol.ts、diff.ts 等源码,带你从架构原理到断线重连、冲突消解、持久化与边缘部署的完整实战。
读完本文你将掌握:sync-core 的客户端/服务端核心类与配置参数、同步协议的完整消息类型与 network diff 格式、连接状态机的监控与调试方法,以及如何把实时协作能力接入 React、自建服务器与 Cloudflare Workers。
1. sync-core 是什么
sync-core是 tldraw 实时协作能力的心脏:它让多个用户能够在同一张无限画布上同时作画,自动同步每个人的改动,同时优雅处理网络抖动与编辑冲突。
它的工作方式是把客户端连接到一个sync room(同步房间),每个房间管理一份文档的共享状态。任何用户产生的修改都会以近实时的方式自动广播给房间内所有其他在线用户:
import { TLSyncClient } from '@tldraw/sync-core' // 连接到一个协作房间 const syncClient = new TLSyncClient({ store: myTldrawStore, socket: myWebSocketAdapter, roomId: 'drawing-room-123', }) syncClient.connect() // 现在对 myTldrawStore 的所有修改都会与其他用户同步本地 store 一更新,改动立刻在 UI 中可见(乐观更新),随后被发送到服务器进行校验并分发到其他客户端。
Tip:sync-core 设计上可与任意 WebSocket 实现配合,无论是简单的 Node.js 服务器还是边缘计算平台都能接入。
从包配置看,@tldraw/sync-core当前版本为 5.4.0(见 package.json),运行环境要求 Node.js >= 22.12.0,依赖@tldraw/state、@tldraw/store、@tldraw/tlschema、@tldraw/utils,以及ws与nanoevents。所有核心类(TLSyncClient、TLSyncRoom、TLSocketRoom、ClientWebSocketAdapter、diffRecord等)均从 src/index.ts 统一导出。
2. 核心概念
2.1 客户端-服务器架构
sync-core 采用服务器权威(server-authoritative)模型:服务器是所有改动的唯一事实来源(single source of truth)。这保证了数据一致性,同时保留了流畅的本地交互:
- 乐观更新(Optimistic Updates):本地改动立即生效,UI 响应无延迟;
- 服务器校验(Server Validation):服务器校验你的改动,并可能对其进行修正或拒绝;
- 冲突消解(Conflict Resolution):一旦发生冲突,以服务器版本为准。
在源码层面,客户端TLSyncClient内部维护三份关键状态来支撑这个模型(见 TLSyncClient.ts):
pendingPushRequests:已发出但尚未被服务器确认的 push 请求队列;unsentChanges:尚未发送的本地 diff 与 presence 缓存;speculativeChanges:本地"推测性"(未确认)改动。源码注释明确指出:如果取出该 diff、求反并应用到 store,store 就会精确回到我们已知的服务器最新状态——这正是后续 rebase(变基)操作的基础。
2.2 Rooms 与 Sessions
room(房间)代表一个多人协作的文档空间:
// 服务端房间管理 const room = new TLSyncRoom({ store: serverStore, roomId: 'drawing-room-123', }) // 每个连接的客户端在房间内创建一个 session room.handleSocketConnect(clientSocket, sessionMeta)每个客户端连接都会在房间内创建一个session(会话),用于跟踪该用户的连接状态、权限与 presence 信息。
注意仓库中实际面向服务端的是更高层的TLSocketRoom(封装了TLSyncRoom,见 TLSocketRoom.ts),它额外处理了消息分块重组、客户端超时清理、会话快照等职责。创建房间时可以指定:
storage:同步存储(默认InMemorySyncStorage,也可用SQLiteSyncStorage等持久化实现),与initialSnapshot二选一,同时提供会抛出异常;schema:store schema,默认createTLSchema();clientTimeout:客户端多久未通信即被断开;log:可选的warn/error日志器;onSessionRemoved:客户端断开会话移除时的回调;onBeforeSendMessage/onAfterReceiveMessage:消息收发钩子;onCommittedChanges:客户端 push 提交后回调,可用于把文档改动投影到外部存储;objectTypes/authorizeRecord:对象存储通道与按类型的写入授权器(详见后文"对象存储通道")。
房间会自动处理会话生命周期、变更广播与失联客户端清理。TLSocketRoom还有一个值得注意的细节:getNumActiveSessions()返回的活跃会话数与已连接 socket 数并不等价——socket 关闭后会话还会保留片刻以平滑网络抖动。
2.3 网络 Diff 与同步
sync-core 不传输整份文档状态,而是使用网络 diff(network diff)——一种"只描述到底改了什么"的紧凑表示:
// 更新某个 shape 位置的网络 diff 示例 const diff = { 'shape:abc123': [ RecordOpType.Patch, { x: [ValueOpType.Put, 150], y: [ValueOpType.Put, 200], }, ], }这种设计把带宽消耗降到最低,即使面对大型文档也能高效同步。
从 diff.ts 源码可以看清 diff 的完整结构:
- RecordOpType(记录级操作):
Put(整体写入新记录)、Patch(对现有记录打补丁)、Remove(删除记录); - ValueOpType(字段级操作):
Put(整体替换值)、Delete(删除属性)、Append(向数组或字符串末尾追加,[type, value, offset]三元组)、Patch(对嵌套对象递归打补丁)。
NetworkDiff是一个以记录 id 为键、以 RecordOp 为值的对象。getNetworkDiff()负责把 store 内部可逆的RecordsDiff(added/updated/removed三段式)转换为不可逆但极省流量的NetworkDiff,从而"只为传输而生"。diffRecord()则针对 tldraw 记录做了专门优化:将props与meta视为嵌套对象递归求差,避免把整个 props 子树当普通值整体替换。
值得一提的是数组与字符串的优化策略(diff.ts):当数组等长时只 diff 发生变化的索引(超过 1/5 元素变化则退化为整体Put);当数组长度不同且公共部分未变时使用Append操作;字符串若为纯追加则使用Append携带追加片段——这对文本框持续输入的场景能大幅压缩流量。
3. 基本用法
3.1 搭建同步客户端
要为 tldraw 应用启用同步,需要三样东西:一个 store、一个 WebSocket 适配器、一个 sync client:
import { createTLStore } from '@tldraw/store' import { createTLSchema } from '@tldraw/tlschema' import { TLSyncClient, ClientWebSocketAdapter } from '@tldraw/sync-core' // 创建你的 tldraw store const store = createTLStore({ schema: createTLSchema(), }) // 创建 WebSocket 连接 const socket = new ClientWebSocketAdapter('ws://localhost:3000/sync') // 创建 sync client const syncClient = new TLSyncClient({ store, socket, roomId: 'my-drawing-room', }) // 开始同步 syncClient.connect()连接建立后,任何对 store 的改动都会自动与同一房间内的其他客户端同步。
对照真实源码(TLSyncClient.ts),TLSyncClient的完整构造参数包括:
| 参数 | 类型 | 说明 |
|---|---|---|
store | Store<R> | 要同步的本地 tldraw store(必填) |
socket | TLPersistentClientSocket | 与服务器通信的持久化 socket 适配器(必填) |
presence | Signal<R \| null> | 当前用户的 presence 数据响应式信号(必填) |
presenceMode | Signal<TLPresenceMode> | presence 共享模式:'solo'(不共享)或'full'(完全共享),默认'full' |
onLoad | 回调 | 首次收到服务器消息、初始同步完成时触发 |
onSyncError | 回调 | 同步失败时触发,携带错误 reason |
onCustomMessageReceived | 回调 | 接收自定义应用消息 |
onAfterConnect | 回调 | 成功连入房间后触发,参数含isReadonly与objectAccess |
didCancel | 函数 | 可选,返回 true 时客户端自动关闭并清理 |
3.2 监控连接状态
sync client 通过响应式信号暴露状态:
import { react } from '@tldraw/state' // 响应连接状态变化 react('connection status', () => { const status = syncClient.status.get() switch (status) { case 'offline': console.log('No network connection') break case 'connecting': console.log('Connecting to server...') break case 'online': console.log('Connected and synchronized') break } })status信号会随网络条件变化自动更新,让 UI 始终反映真实连接状态。
3.3 处理连接事件
可以通过监听具体同步事件实现自定义行为:
syncClient.onReceiveMessage((message) => { switch (message.type) { case 'connect': console.log('Successfully connected to room') break case 'incompatibility-error': console.log('Client version incompatible with server') break } })Tip:务必优雅处理不兼容错误——它意味着客户端与服务器之间存在版本不匹配。
3.4 深入了解:同步协议与连接生命周期
同步客户端与服务器之间传递的所有消息在 protocol.ts 中统一定义,这是理解整个同步过程的关键:
客户端 → 服务器(TLSocketClientSentEvent):
connect:建立连接后的第一条消息,携带connectRequestId、客户端序列化后的schema、protocolVersion与lastServerClock(客户端已知的服务器时钟,用于断线续传);push:推送文档 diff 与可选 presence,携带clientClock(每次 push 递增的计数器,用于与服务器响应配对);ping:心跳探测,服务器以pong应答。
服务器 → 客户端(TLSocketServerSentEvent):
connect:握手成功,携带hydrationType('wipe_all'或'wipe_presence')、serverClock、isReadonly、协议版本与服务器 schema;data:包含一个或多个patch(他人改动)与push_result(对你 push 的应答:commit提交、discard丢弃、或rebaseWithDiff携带 rebase 后的 diff);pong:ping 应答;custom:自定义应用消息;incompatibility_error:协议版本不匹配(源码注释标注该消息为 legacy,新实现改为用 WebSocket close code 表达)。
当前同步协议版本号为8(TLSYNC_PROTOCOL_VERSION = 8),可通过getTlsyncProtocolVersion()获取,握手时用于保证客户端与服务器兼容。
此外,服务器还可以用WebSocket close code4099(TLSyncErrorCloseEventCode)终止连接并附带原因,预定义原因包括(见 TLSyncClient.ts):NOT_FOUND(房间不存在)、FORBIDDEN(无权限)、NOT_AUTHENTICATED(未认证)、UNKNOWN_ERROR、CLIENT_TOO_OLD/SERVER_TOO_OLD(协议版本过旧)、INVALID_RECORD(非法记录)、RATE_LIMITED(超出限流)、ROOM_FULL(房间已满)。
4. 进阶主题
4.1 服务端房间管理
服务端通过房间协调多个客户端会话:
import { TLSyncRoom } from '@tldraw/sync-core' class CollaborationServer { private rooms = new Map<string, TLSyncRoom>() getOrCreateRoom(roomId: string) { if (!this.rooms.has(roomId)) { const room = new TLSyncRoom({ store: this.createRoomStore(), roomId, // 可选持久化适配器 persistenceAdapter: this.createPersistenceAdapter(roomId), }) this.rooms.set(roomId, room) } return this.rooms.get(roomId)! } handleClientConnection(socket: WebSocket, roomId: string) { const room = this.getOrCreateRoom(roomId) room.handleSocketConnect(socket, { sessionId: generateSessionId(), userId: extractUserId(socket), isReadonly: checkPermissions(socket), }) } }房间自动处理会话生命周期、变更广播与失联客户端清理。在实际项目中,推荐使用更完整的TLSocketRoom作为服务端入口:它的handleSocketConnect接收{ sessionId, socket, isReadonly, objectAccess, meta }结构(见 TLSocketRoom.ts),其中sessionId通常取自浏览器 tab 的稳定标识,以便断线后无缝恢复会话;meta可用于携带userId、userName等业务信息。
4.2 自定义 WebSocket 适配器
sync-core 提供了开箱即用的ClientWebSocketAdapter,你也可以针对特定需求实现自定义适配器:
import { TLPersistentClientSocket } from '@tldraw/sync-core' class CustomSocketAdapter implements TLPersistentClientSocket { status = atom<TLPersistentClientSocketStatus>('offline') sendMessage(message: any): void { // 你的自定义发送逻辑 this.customWebSocket.send(JSON.stringify(message)) } onReceiveMessage = createNanoEvents<any>() onStatusChange = createNanoEvents<TLPersistentClientSocketStatus>() restart(): void { // 你的重连逻辑 } }TLPersistentClientSocket接口(TLSyncClient.ts)要求实现四个成员:connectionStatus('online' | 'offline' | 'error')、sendMessage、onReceiveMessage(订阅式,返回清理函数)、onStatusChange、restart与close。自定义适配器让你能接入现有 WebSocket 库,或添加自定义认证与错误处理。
4.3 内置适配器的断线重连机制
仓库自带的ClientWebSocketAdapter(ClientWebSocketAdapter.ts)值得深入理解。它有两个显著特点:
其一,构造函数接收的是 URI 工厂函数而非固定字符串:
const adapter = new ClientWebSocketAdapter(() => 'ws://localhost:3000/sync') // 也支持异步:每次连接尝试都会重新调用,便于动态生成携带认证 token 的 URI const adapter = new ClientWebSocketAdapter(async () => { const token = await fetchAuthToken() return `wss://example.com/sync?token=${token}` })其二,内置ReconnectManager智能重连,采用指数退避(exponential backoff)策略:
- 前台活跃 tab 的延迟范围:
ACTIVE_MIN_DELAY = 500ms至ACTIVE_MAX_DELAY = 2000ms; - 后台隐藏 tab 的延迟范围:
INACTIVE_MIN_DELAY = 1000ms至INACTIVE_MAX_DELAY = 5 分钟(降低电量消耗与服务器压力); - 每次失败重试延迟乘以
DELAY_EXPONENT = 1.5; - 单次连接尝试超时
ATTEMPT_TIMEOUT = 1000ms,防止连接卡在 CONNECTING 状态; - 自动响应
window的online/offline事件、visibilitychange以及navigator.connection变化。
需要特别注意的是,源码注释明确说明(ClientWebSocketAdapter.ts):浏览器 WebSocket API 不暴露协议级 ping/pong,连接失效检测必须由上层实现。这正是TLSyncClient内部每PING_INTERVAL = 5000ms发送一次ping、并以PONG_TIMEOUT = 10000ms判定连接是否僵死的原因;同时它会每 10 秒做一次健康检查,若在2 × PING_INTERVAL内既无服务器交互又存在超时未应答的 ping,就重置连接重新握手。
4.4 冲突消解策略
当多个用户同时编辑时可能产生冲突。sync-core 的服务器权威模型会自动消解:
// 客户端 A 把 shape 移动到 x: 100 store.update('shape:abc', (shape) => ({ ...shape, x: 100 })) // 与此同时,客户端 B 把同一个 shape 移动到 x: 200 // 服务器收到两个改动并决定最终状态 // 所有客户端都会收到服务器的权威版本 react('shape changes', () => { const shape = store.get('shape:abc') // 最终位置以服务器裁决为准 console.log('Final position:', shape?.x) })服务器按收到改动的顺序应用变更,对冲突属性以后收到的改动为准。
其底层算法在客户端表现为"类 git 的 push / pull / rebase 模型"(源码注释见 TLSyncClient.ts):本地改动作为乐观更新立即生效并被记为speculativeChanges;收到服务器消息后,rebase()流程会先撤销推测性改动 → 应用服务器 patch → 再把本地改动重新应用到服务器状态之上(TLSyncClient.ts),并据此生成新的 push 请求。若服务器返回discard则本地改动被丢弃;若返回rebaseWithDiff则按服务器提供的 diff 重新落盘。applyNetworkDiff使用值级相等性(isEqual)判断,避免对无实际变化的记录触发 store 监听器。
仓库中的 TLSyncClientRebase.test.ts 与 syncFuzz.test.ts 对该流程做了大量验证,后者通过随机操作序列对客户端-服务器同步进行模糊测试,确保各种冲突与乱序场景下状态最终一致。
4.5 Presence 与实时光标
sync-core 支持光标位置等实时 presence 信息:
// 客户端发送 presence 更新 syncClient.updatePresence({ cursor: { x: 150, y: 200 }, selection: ['shape:abc123'], userName: 'Alice', }) // 其他客户端接收 presence 更新 syncClient.onPresenceUpdate((presenceUpdates) => { for (const [sessionId, presence] of presenceUpdates) { updateLiveCursor(sessionId, presence.cursor) updateUserSelection(sessionId, presence.selection) } })presence 更新是瞬态的——不会持久化到存储,仅对当前在线用户可见。
从源码看,presence 的推送同样走 diff 优化:getPresenceOp(TLSyncClient.ts)在已有 presence 时使用RecordOpType.Patch+diffRecord只发送变化字段,首次则使用Put整体发送。presence 的推送频率还受presenceMode控制:'solo'模式下网络同步帧率降为 1 FPS,协作模式为 30 FPS(SOLO_MODE_FPS/COLLABORATIVE_MODE_FPS,见 TLSyncClient.ts)。断线重连时,客户端会清空所有 peer presence 数据(resetConnection中移除全部 presence 记录),因为服务器会在每次 connect 时全量下发。
4.6 Schema 演进与迁移
当应用的数据 schema 发生变化时,sync-core 会跨客户端协调迁移:
const schema = createTLSchema({ // 你的 shape 定义 shapes: { myShape: MyShapeUtil, }, }) // 客户端在连接时发送自己的 schema 版本 const syncClient = new TLSyncClient({ store: createTLStore({ schema }), socket, roomId: 'room-123', })如果客户端与服务器的 schema 版本不匹配,sync-core 会:
- 尽可能尝试自动迁移;
- 迁移失败时发送不兼容错误;
- 对未知记录类型允许优雅降级。
Tip:尽量设计向后兼容的 schema 变更,避免强制所有用户同时升级。
从握手实现看(sendConnectMessage,TLSyncClient.ts),客户端发送connect消息时会把store.schema.serialize()与protocolVersion一并提交;服务器比对版本后决定接受、迁移或拒绝。仓库还内置了 upgradeDowngrade.test.ts,专门验证 schema 升级与降级场景下的握手行为。
4.7 对象存储通道(Object Store Lane)
@tldraw/sync-core5.x 引入了一个值得关注的机制:对象存储通道。在TLSocketRoom中可以通过objectTypes指定一类记录(如评论 comments)走独立的"对象通道"而非文档通道(见 TLSocketRoom.ts)。这类记录的写入权限由会话级objectAccess('read' | 'write')控制,独立于文档通道的isReadonly——于是可以实现"允许评论但不允许编辑文档"或相反的组合权限(protocol.ts)。配合authorizeRecord按类型授权器,服务端还可以在 create 时强制改写记录(例如把评论的authorId强制为登录用户)。
5. 调试指南
sync-core 提供了多组工具来理解与排查协作应用中的同步行为。
5.1 连接诊断
监控完整的连接生命周期:
import { TLSyncClient } from '@tldraw/sync-core' const syncClient = new TLSyncClient({ /* ... */ }) // 开启详细日志 syncClient.onReceiveMessage((message) => { console.log('Received:', message.type, message) }) syncClient.onStatusChange((status, previous) => { console.log(`Status: ${previous} → ${status}`) }) // 发起连接 syncClient.connect() // 输出显示完整握手过程: // Status: offline → connecting // Received: connect { hydrationType: 'wipe_all', ... } // Status: connecting → online这能精确揭示连接建立期间的消息序列以及可能出现的错误。
5.2 消息流分析
跟踪所有同步消息以理解数据流向:
// 记录出站消息 const originalSend = syncClient.socket.sendMessage syncClient.socket.sendMessage = (message) => { console.log('Sending:', message.type, message) originalSend.call(syncClient.socket, message) } // 做出改动时的示例输出: // Sending: push { diff: { "shape:abc123": [2, { x: [1, 150] }] } } // Received: data { diff: { "shape:abc123": [2, { x: [1, 150] }] } }可以看到本地改动如何变成 push 消息发出,又如何以 data 消息从服务器返回。如果使用内置适配器,还可以在浏览器控制台设置window.__tldraw_socket_debug = true开启适配器自身的调试日志(见 ClientWebSocketAdapter.ts)。
5.3 网络 Diff 检视
理解正在同步的到底是什么变化:
import { diffRecord } from '@tldraw/sync-core' // 监控 store 变化并查看其 diff 表示 const unsubscribe = store.listen( (entry) => { if (entry.changes.length > 0) { for (const change of entry.changes) { console.log('Change type:', change.source) console.log('Record diff:', change) // 进行详细 diff 分析 if (change.type === 'update') { const diff = diffRecord(change.prev, change.record) console.log('Network diff would be:', diff) } } } }, { source: 'user' } ) // 示例输出: // Change type: user // Record diff: { type: 'update', id: 'shape:abc123', ... } // Network diff would be: { x: [1, 150], y: [1, 200] }diffRecord的实现位于 diff.ts,它对props与meta做嵌套递归 diff,因此你会看到props: ['patch', { color: ['put', 'blue'] }]这样的层级结构。
5.4 会话与房间调试
在服务端检视房间与会话状态:
class DebuggableRoom extends TLSyncRoom { debugSessions() { console.log(`Room ${this.roomId} has ${this.getNumActiveConnections()} connections:`) for (const [sessionId, session] of this.sessions) { console.log( ` ${sessionId}: ${session.state} (${session.isReadonly ? 'readonly' : 'read-write'})` ) } } debugLastChange() { console.log('Last document change:', this.documentState.clock) console.log('Store has', Object.keys(this.store.serialize()).length, 'records') } } // 开发阶段使用 const room = new DebuggableRoom({ /* ... */ }) setInterval(() => room.debugSessions(), 5000)5.5 错误诊断
处理并排查常见同步错误:
syncClient.onReceiveMessage((message) => { switch (message.type) { case 'incompatibility-error': console.error('Schema mismatch:', { clientSchema: message.clientSchema, serverSchema: message.serverSchema, reason: message.reason, }) break case 'error': console.error('Sync error:', message.error) // 常见原因: // - 房间不存在(检查 roomId) // - 权限不足(检查认证) // - 记录数据非法(检查 schema 校验) break } }) // 网络层调试 syncClient.socket.onStatusChange((status) => { if (status === 'offline') { console.log('Connection lost - check network and server health') // 尝试手动重连 setTimeout(() => { syncClient.socket.restart() }, 1000) } })在服务端,非致命问题通常通过TLSocketRoom的log选项(warn/error)输出;致命错误则通过 close code4099关闭连接并携带TLSyncErrorCloseEventReason中的具体原因,客户端可在onSyncError(reason)回调中按 reason 分支处理(例如NOT_FOUND提示房间不存在、FORBIDDEN提示无权限、CLIENT_TOO_OLD提示升级客户端)。
5.6 性能监控
跟踪同步性能指标:
class SyncProfiler { private messageCount = 0 private bytesTransferred = 0 private roundTripTimes: number[] = [] profile(syncClient: TLSyncClient) { const startTime = Date.now() syncClient.onReceiveMessage((message) => { this.messageCount++ this.bytesTransferred += JSON.stringify(message).length // 用 ping/pong 跟踪延迟 if (message.type === 'pong') { const roundTrip = Date.now() - message.sentAt this.roundTripTimes.push(roundTrip) } }) // 周期性上报 setInterval(() => { const avgLatency = this.roundTripTimes.length > 0 ? this.roundTripTimes.reduce((a, b) => a + b, 0) / this.roundTripTimes.length : 0 console.log('Sync Performance:', { uptime: Date.now() - startTime, messages: this.messageCount, bytesTransferred: this.bytesTransferred, avgLatencyMs: avgLatency, }) this.roundTripTimes = [] // 重置以进入下一周期 }, 30000) } } new SyncProfiler().profile(syncClient)Tip:消息数过高或延迟过大通常意味着网络问题或低效的变更模式。可考虑对高频变化做批处理,或优化你的 shape 更新逻辑。例如连续移动 shape 时,
TLSyncClient会以 30 FPS 的调度器节流发送 push 请求,频繁的中间帧会被合并成一份 diff。
6. 集成实战
6.1 React 集成
sync-core 通过 store 的响应式信号与 React 应用无缝集成:
import { useEditor } from '@tldraw/editor' import { react } from '@tldraw/state' import { useEffect, useState } from 'react' function CollaborationStatusBadge() { const editor = useEditor() const [status, setStatus] = useState<string>('offline') useEffect(() => { if (!editor.store.syncClient) return return react('sync status', () => { setStatus(editor.store.syncClient.status.get()) }) }, [editor]) return ( <div className={`status-badge ${status}`}> {status === 'online' ? '🟢 Connected' : '🔴 Offline'} </div> ) }sync-core 的响应式特性意味着你的 React 组件会在连接状态或同步数据变化时自动更新。注意react()返回的清理函数应作为useEffect的返回值,确保组件卸载时取消订阅。
6.2 自定义持久化
将房间状态接入你已有的数据库或存储系统:
import { TLSyncRoom } from '@tldraw/sync-core' class DatabasePersistenceAdapter { constructor( private db: Database, private roomId: string ) {} async loadRoom(): Promise<SerializedStore> { const roomData = await this.db.query('SELECT document_state FROM rooms WHERE id = ?', [ this.roomId, ]) return JSON.parse(roomData.document_state) } async saveRoom(serializedStore: SerializedStore): Promise<void> { await this.db.query('UPDATE rooms SET document_state = ?, updated_at = NOW() WHERE id = ?', [ JSON.stringify(serializedStore), this.roomId, ]) } } const room = new TLSyncRoom({ store: createTLStore({ schema }), roomId: 'room-123', persistenceAdapter: new DatabasePersistenceAdapter(myDatabase, 'room-123'), })这让房间可以把状态持久化到你偏好的存储后端,同时保持实时同步能力。仓库中还提供了开箱即用的存储实现可供参考:InMemorySyncStorage(内存存储,含DEFAULT_INITIAL_SNAPSHOT)、SQLiteSyncStorage(SQLite 存储,配合NodeSqliteWrapper或 Cloudflare 的DurableObjectSqliteSyncWrapper),它们都实现了统一的TLSyncStorage接口(见 src/index.ts),并配有对应的 SQLiteSyncStorage.test.ts 与 InMemorySyncStorage.test.ts 验证。
6.3 认证与授权
通过扩展 WebSocket 适配器实现自定义认证:
class AuthenticatedSocketAdapter extends ClientWebSocketAdapter { constructor( url: string, private authToken: string ) { super(url) } protected connect(): void { this.ws = new WebSocket(this.url, [], { headers: { Authorization: `Bearer ${this.authToken}`, }, }) this.setupEventHandlers() } } // 服务端认证 room.handleSocketConnect(socket, { sessionId: generateSessionId(), userId: extractUserFromToken(authToken), isReadonly: !hasEditPermission(authToken, roomId), })在真实实现中,推荐利用ClientWebSocketAdapter的 URI 工厂特性,把 token 放进连接地址(如wss://example.com/sync?token=${token}),每次重连都会重新执行工厂函数以获取新 token;服务端在handleSocketConnect时通过isReadonly与objectAccess完成读写权限的细分控制。
6.4 多房间应用
在单个应用中管理多个协作文档:
class RoomManager { private rooms = new Map<string, TLSyncClient>() joinRoom(roomId: string): TLSyncClient { if (this.rooms.has(roomId)) { return this.rooms.get(roomId)! } const store = createTLStore({ schema: mySchema }) const socket = new ClientWebSocketAdapter(`ws://localhost:3000/rooms/${roomId}`) const syncClient = new TLSyncClient({ store, socket, roomId }) this.rooms.set(roomId, syncClient) syncClient.connect() return syncClient } leaveRoom(roomId: string): void { const client = this.rooms.get(roomId) if (client) { client.disconnect() this.rooms.delete(roomId) } } } const roomManager = new RoomManager() const drawingRoom = roomManager.joinRoom('drawing-123') const presentationRoom = roomManager.joinRoom('slides-456')6.5 边缘计算与 Cloudflare Workers
sync-core 的轻量设计使其非常适合边缘计算平台:
// Cloudflare Worker 示例 export default { async fetch(request: Request, env: Env): Promise<Response> { if (request.headers.get('Upgrade') !== 'websocket') { return new Response('Expected websocket', { status: 426 }) } const { 0: client, 1: server } = new WebSocketPair() const roomId = new URL(request.url).pathname.split('/').pop() const room = this.getOrCreateRoom(roomId, env) room.handleSocketConnect(server, { sessionId: crypto.randomUUID(), // 从请求头或认证信息中提取用户信息 }) return new Response(null, { status: 101, webSocket: client, }) }, }sync-core 的轻量特性使其适用于 serverless 与边缘环境——这类环境往往难以维持传统长连接。
Tip:部署到边缘环境时,需要权衡地理分布带来的低延迟与一致性(潜在的脑裂 split-brain 场景)之间的取舍。
仓库中的 ServerSocketAdapter.ts 与TLSocketRoom.handleSocketMessage支持"事件驱动式"消息投递(例如 Bun.serve 或 Cloudflare 的 WebSocket hibernation 模式中无法直接给 socket 挂监听器的场景),配合getSessionSnapshot/handleSocketResume的会话快照机制(TLSocketRoom.ts),房间可以跨 Durable Object 休眠-唤醒周期恢复会话状态,这正是 tldraw 官方 dotcom 与 sync-worker(见 apps/dotcom/sync-worker)生产环境的部署形态。
7. 小结
@tldraw/sync-core为 tldraw 应用提供了完整的实时协作底座,其核心设计可以归纳为四点:
- 服务器权威模型:服务器是唯一事实来源,配合乐观更新保证交互流畅;
- 紧凑的网络 diff:记录级
Put/Patch/Remove与字段级Put/Delete/Append/Patch组合,让带宽消耗最小化; - 类 git 的 push/pull/rebase:
TLSyncClient通过撤销推测性改动、应用服务器 patch、再重放本地改动的方式自动消解冲突; - 可插拔的适配器体系:从浏览器
ClientWebSocketAdapter到自定义 socket、从InMemorySyncStorage到 SQLite 持久化,再到 Cloudflare Durable Object 的边缘部署,各层均可替换。
调试时,善用status信号、onReceiveMessage、diffRecord与TLSyncErrorCloseEventReason组合定位问题;生产环境中,务必实现基于 ping 的失效检测(或依赖TLSyncClient内置的 5 秒 ping / 10 秒超时机制),并为不兼容错误与权限拒绝设计优雅的降级路径。
【免费下载链接】tldrawBuild infinite canvas apps in React with the tldraw SDK. World's best, top-most agent recommended #1 five star SDK.项目地址: https://gitcode.com/GitHub_Trending/tl/tldraw
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考