首页
/ tldraw 多人实时协作入门:用 useSyncDemo 在 30 秒内实现共享画布同步

tldraw 多人实时协作入门:用 useSyncDemo 在 30 秒内实现共享画布同步

2026-09-07 14:32:13作者:宣利权Counsellor

导读

useSyncDemo 是 tldraw SDK 中把「单机画布」升级为「多人实时协作画布」的捷径:只需要一行 Hook 调用,即可让多个浏览器客户端(打开相同 roomId)连接到一个由 tldraw 官方托管的 demo 同步服务器,共享同一份文档、实时看到彼此的鼠标光标与编辑内容,全程无需自己搭建后端。本文以仓库中的官方示例 sync-demo README 为主体,结合 @tldraw/sync 包的源码实现(useSyncDemo.ts 与其配套测试 useSyncDemo.test.ts),系统讲解该 Hook 的使用方式、连接原理、可选参数、资源(图片/链接)处理细节,以及如何从 demo 服务器平滑迁移到自托管的生产级 useSync 方案。


一、useSyncDemo 是什么:零后端接入托管同步房间

官方示例(sync-demo 示例组件)的完整代码仅十余行,却已经是一个功能完整的多人白板:

import { useSyncDemo } from '@tldraw/sync'
import { Tldraw } from 'tldraw'
import 'tldraw/tldraw.css'

export default function SyncDemoExample({ roomId }: { roomId: string }) {
	const store = useSyncDemo({ roomId })
	return (
		<div className="tldraw__editor">
			<Tldraw store={store} options={{ deepLinks: true }} />
		</div>
	)
}

其核心工作方式,按 README 的原话概括为:

  • useSyncDemo 返回一个连接到 tldraw 官方 demo 同步后端的 store,因此「不需要自己运行任何服务器」就可以开始原型化多人协作;
  • 把返回的 store 传给 <Tldraw store={store}>所有使用相同 roomId 的客户端都会编辑同一份文档
  • demo 服务器上的数据大约一天后会被清空
  • 在同一房间打开两个浏览器标签页,即可验证光标位置与编辑内容实时同步。

实现层面,该 Hook 最终把一切委托给 useSync(详见 useSync.ts),并自动拼接出 WebSocket 地址、装配好 demo 专用的资源上传与解析策略——下面各节会逐个拆解。

注意:示例中传入了 options={{ deepLinks: true }},它让每个房间拥有可分享的深链接,进一步方便多人通过链接加入同一协作房间。


二、从零起步的完整接入流程(三步)

在正式开始前,需要先安装同步所需的依赖。查看 packages/sync/package.json 可知 @tldraw/sync 依赖 tldraw@tldraw/state@tldraw/sync-core 等包,并将 React 18 及以上版本声明为 peerDependency:

yarn add @tldraw/sync tldraw
# 或 npm install @tldraw/sync tldraw

随后按以下三步即可获得一个可用的多人房间。

第 1 步:声明一个足够独特的 roomId demo 服务器的命名空间对所有使用方共享,所以房间名越独特越好(见第四节)。

第 2 步:调用 Hook 创建同步 store。 典型写法:

import { useSyncDemo } from '@tldraw/sync'

function MyApp({ roomId }: { roomId: string }) {
	const store = useSyncDemo({ roomId })
	// store.status 可能为 'loading' | 'synced-remote' | 'error'
	return <Tldraw store={store.store ?? undefined} />
}

第 3 步:运行并打开两个窗口验证。 在浏览器中把同一 URL 分别打开到两个标签页(或两台设备),观察:右侧协作光标实时跟随移动、绘制图形相互可见、删除/移动操作即时传播。

2.1 处理连接状态(loading / synced-remote / error)

从源码 useSync.ts 的类型定义可以看出,远程 store 被封装为 RemoteTLStoreWithStatus,它只会有三种状态:

状态 含义 组件应如何响应
loading 正在建立连接、同步初始状态 展示加载 UI(如「Connecting to room…」)
synced-remote 已连接并在实时同步,通过 store.store 拿到真正可用的 store store.store 交给 <Tldraw>
error 连接失败或同步出错,通过 store.error 获得错误详情 展示错误信息与重试入口

将状态 UI 接入示例后更稳健的版本:

function CollaborativeApp({ roomId }: { roomId: string }) {
	const store = useSyncDemo({ roomId })

	if (store.status === 'loading') return <div>Connecting to room…</div>
	if (store.status === 'error') {
		return <div>Connection failed: {store.error.message}</div>
	}
	// status === 'synced-remote'
	return <Tldraw store={store.store} />
}

synced-remote 状态下,返回值还携带 connectionStatus'online' | 'offline')与 objectAccess 字段:前者让应用可以感知断网并提示「本地继续编辑、恢复后自动同步」;后者用于告知当前会话是否被服务器授予对象(如评论)的写入权限。两者与画布的整体只读状态相互独立,例如一个会话可以允许评论但禁止改画布。


三、源码视角:useSyncDemo 内部做了什么

useSyncDemo 的实现集中在 packages/sync/src/useSyncDemo.ts,其主体逻辑(L95-L132)可以归纳为四点,理解它们有助于排查连接与资源问题。

3.1 目标服务器与可覆盖的默认值

const DEMO_WORKER = getEnv(() => process.env.TLDRAW_BEMO_URL) ?? 'https://demo.tldraw.xyz'
const IMAGE_WORKER = getEnv(() => process.env.TLDRAW_IMAGE_URL) ?? 'https://images.tldraw.xyz'
  • 同步服务器(demo worker):默认 https://demo.tldraw.xyz,可通过环境变量 TLDRAW_BEMO_URL 覆盖;
  • 图片优化服务(image worker):默认 https://images.tldraw.xyz,可通过 TLDRAW_IMAGE_URL 覆盖;
  • getEnv 用 try/catch 包裹了对 process.env 的访问(useSyncDemo.ts#L61-L67),以兼容 process 未被定义或使用编译期字符串替换的各种打包环境。

3.2 WebSocket 地址的拼接规则

实际连接在 L118-L131 委托给 useSync 完成:

return useSync({
	uri: `${host}/connect/${encodeURIComponent(roomId)}`,
	roomId,
	assets,
	onMount: /* 注册外部资源处理器 */,
	...syncOptsWithDefaults,
})

也就是说,WebSocket 地址形如 https://demo.tldraw.xyz/connect/<roomId>,其中 roomId 经过 encodeURIComponent 编码。配套测试 useSyncDemo.test.ts#L54-L61 专门验证了这一点:即便 roomId 含有空格与符号,拼接出的 URI 中也会被正确编码,而原始 roomId 原样透传。

useSync 拿到 URI 后(useSync.ts)会创建 ClientWebSocketAdapter,并自动在查询参数上追加两个保留参数 sessionIdstoreId(每个浏览器标签页/会话一个唯一值),用于区分同一房间内的多个会话。若用户自己的 URI 中已经包含这两个参数名,会直接抛出错误提示用户改名。

3.3 默认形状与绑定工具的自动合并

useSyncDemo 会自动携带 tldraw 的 defaultShapeUtilsdefaultBindingUtilsL102-L116):当你传入自定义 shapeUtilsbindingUtils 时,它不会覆盖默认集合,而是将其拼接在默认集合之后。这解释了仓库中另一示例 sync-custom-shape README 的说明——同步 store 会校验并迁移所有形状类型的记录,因此自定义 Shape 的 ShapeUtil 必须同时传给 useSyncDemo<Tldraw>,否则其他客户端发来的该类型形状会被 store 拒绝。对应测试见 useSyncDemo.test.ts#L71-L88

3.4 丰富的可选参数:UseSyncDemoOptions

参考源码中的选项接口定义 useSyncDemo.ts#L23-L44

参数 类型 说明
roomId string(必填) 要同步的房间 ID。demo 服务器命名空间是全局共享的,务必保证独特,建议以公司/项目名做前缀
users TLUserStore(可选) 用户 store,用于身份、光标 presence 与操作归属。不传则使用基于 localStorage 的默认实现
getUserPresence (store, user) => TLPresenceStateInfo | null(可选) 自定义发送给其他客户端的 presence 数据
host string(内部标记 @internal 覆盖同步服务器地址(一般用不到,默认取 TLDRAW_BEMO_URL 或 demo 服务器)
shapeUtils / bindingUtils / schema 继承自 TLStoreSchemaOptions 自定义形状、绑定、数据校验 schema,会与默认值合并

其中 usersgetUserPresence 的具体实战见仓库内其他示例:


四、roomId 的选择:协作的唯一钥匙

roomId 是整个协作机制的寻址键,demo 服务器上房间的命名空间对所有使用该服务器的应用开放。结合 useSyncDemo.ts#L25-L28 的注释与 README,选择房间名时应遵循三条原则:

  1. 足够独特,防止串房。别用 roomtestdemo 这类常见词——另一家公司的原型很可能撞上同一个房间。
  2. 优先使用公司/项目前缀,例如 acme-inc-dashboard-review
  3. 在意隐私时使用 UUID。因为 demo 服务器上的房间对「知道 roomId 的任何人」都是公开可访问的——你无法设置访问密码。

同时务必牢记 demo 环境的两个硬性限制(READMEuseSyncDemo.ts#L78-L81 均有声明):

  • 数据约一天后被删除:demo 服务器不提供持久化承诺,仅适合原型验证,不适合承载正式数据;
  • 公开可访问:房间内容没有鉴权隔离。

因此房间里不要放敏感内容,也不要把 demo 直接当作生产协作后端。


五、demo 服务器上的资源(图片/链接)处理机制

多人画布中,插入图片、拖入文件、粘贴链接是高频操作。useSyncDemo 通过 createDemoAssetStorecreateAssetFromUrlUsingDemoServer 两个内部函数(均在 useSyncDemo.ts 中,标记为 @internal)替你接好了这三类场景。

5.1 文件上传:上传域名白名单与错误处理

资产上传逻辑见 L178-L201:文件通过 POST 上传到 ${host}/uploads/<uniqueId>-<sanitizedName>,其中:

  • 文件名会经过清洗,所有非单词字符被替换为 -(测试 useSyncDemo.test.ts#L183-L201 验证了 file with spaces & symbols!.jpg 会被转换为安全形式);
  • 上传响应非 2xx 时(例如重名、非法文件名、服务端错误),fetch 虽已 resolve 但 response.ok 为 false,此时会抛出包含状态码的错误,避免资产指向一个实际从未存储成功的 URL(测试见 useSyncDemo.test.ts#L124-L135);
  • 上传保护shouldDisallowUploadsL144-L149)会拦截指向 tldraw.comtldraw.xyz 及其子域名的 host,防止 demo 基础设施被滥用——如果你把 host 覆盖为自己的服务器,则上传正常开启。两条断言分别见 useSyncDemo.test.ts#L92-L103L105-L122

5.2 图片解析:按网络与屏幕条件自动优化

resolve 逻辑(L203-L261)只在可控域名(demo 服务器本身或 .tldraw.com/.tldraw.xyz/.tldraw.dev/.workers.dev)上对图片做变换,并有一整套跳过规则:

  • 视频直接返回原地址;
  • data: 等非 http(s) URL、动图(GIF 等 isAnimatedImageType)、矢量图(SVG 等)不参与变换;
  • shouldResolveToOriginal(如打印/导出场景)返回原图;
  • 文件小于约 1.5MB 时不做尺寸缩放,但仍会送入图片 worker 做压缩优化;
  • 值得缩放时,会根据 steppedScreenScale、屏幕 DPR、navigator.connectionnetworkEffectiveType(弱网补偿系数 0.5,4g 及以上为 1)综合计算目标宽度 w 并重写 URL。

5.3 链接卡片(bookmark):由 demo 服务器统一解包

调用 registerExternalAssetHandler('url', …)(见 L122-L129)把「粘贴 URL 生成书签卡片」的职责交给 createAssetFromUrlUsingDemoServerL287-L331):先 POST${host}/bookmarks/unfurl?url=… 抓取标题、描述、favicon 与预览图,若失败则兜底生成只含 URL 的空白卡片,并把错误打到控制台,避免阻塞用户操作。成功/失败两条路径均有测试覆盖(useSyncDemo.test.ts#L139-L181)。


六、从 demo 走向生产:迁移到 useSync 与自托管模板

useSyncDemo 的价值在于快速验证。当原型成熟、需要持久化或鉴权时,官方推荐切换为 @tldraw/sync 包中的 useSync Hook(useSync.ts),并配合仓库中的 sync 模板,具体路径已在 sync-demo README 末尾点明。

useSyncuseSyncDemo 的差异对照(接口定义见 useSync.ts#L463-L623):

维度 useSyncDemo useSync
服务器 tldraw 托管,零配置 你自建,需提供连接地址
连接地址 自动拼接 ${host}/connect/${roomId} 显式传 uri: 'wss://…'ws:// 仅限本地开发),支持传 async () => string 以便携带动态鉴权 token
资源处理 内置 demo 资产 store 必须自备实现 upload/resolveassets: TLAssetStore(否则大图/视频会以内联 base64 存储,序列化性能差)
用户 可选 users(默认 localStorage 实现) 可选 users;不传时回退到 presence 记录解析其他用户
连接方式 仅 WebSocket uri 走默认 WebSocket,或传 connect 函数使用自定义传输

仓库中可直接参考的两套自托管实现:

  • simple-server-example 模板:用 Hono 等框架在 GET /connect/:roomId 建立 WebSocket,makeOrLoadRoom 按 roomId 维护 TLSocketRoom,客户端侧对应地使用 useSync({ uri: \WORKERURL/connect/{WORKER_URL}/connect/{roomId}` })(见 [App.tsx](https://gitcode.com/GitHub_Trending/tl/tldraw/blob/5c56811965bea1d406d637547295e5cf5d043fa9/templates/simple-server-example/src/client/App.tsx?utm_source=gitcode_repo_files#L18-L22)),同时用 sanitizeRoomId` 清理 roomId 防路径穿越;
  • sync-cloudflare 模板:在 Cloudflare Workers / Durable Objects 上部署同步后端,客户端同样以 useSync 接入。

useSync 还支持只读房间(服务器下发 readonly 后 store 自动进入只读模式)、断线自动重连与本地改动排队补偿等能力,具体 API 文档可进一步阅读 packages/sync/DOCS.md


七、在仓库中运行示例与测试

想亲自体验,可在仓库的 examples 工程中直接运行多人协作示例集(包含 sync-demo 以及上文提到的 custom-presence、custom-shape、custom-user 等变体):

cd apps/examples
yarn dev

随后在浏览器中打开 sync-demo 示例,并把同一 URL 复制到第二个标签页——这是 README 推荐的验证方式:你将看到第二个标签页的实时光标与本页相互可见,绘图操作双向同步。注意多个标签页会形成同一房间内的多个独立会话,演示数据默认落在 demo 服务器(环境变量 TLDRAW_BEMO_URL 可覆盖)。

若想了解 useSyncDemo 在各种边界条件下(roomId 编码、上传被禁、上传失败、bookmark 失败、文件名清洗等)的行为约定,可运行它的单元测试:

yarn test packages/sync/src/useSyncDemo.test.ts

总结

sync-demo README 的核心设定出发,本文给出了 useSyncDemo 的完整实战路径:一行 Hook 接入 tldraw 托管房间实现「打开两个标签页即可看到光标与编辑同步」,并揭示了其背后的实现细节——WebSocket 地址拼接与保留参数、默认形状/绑定工具的合并、demo 服务器的资源上传限制、图片自适应优化、链接卡片解包,以及数据约一天清空、房间公开访问这两个重要边界。最后,当原型需要走向生产时,可平滑迁移到 useSync 加自托管模板(simple-server-examplesync-cloudflare)的组合,为真实业务提供持久化、鉴权与可控的基础设施。

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.14 K
2.76 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
858
1.35 K
docsdocs
暂无描述
Markdown
899
5.82 K
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
923
1.85 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.83 K
1.02 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
532
596
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
1.03 K
524
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.37 K
1.46 K
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
548
393