tldraw SDK 的 `@tldraw/sync` 公共 API 全解析:`useSync`、`useSyncDemo` 与多人协同集成实战
2026/9/11 16:43:47 网站建设 项目流程

tldraw SDK 的@tldraw/sync公共 API 全解析:useSyncuseSyncDemo与多人协同集成实战

【免费下载链接】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是 tldraw SDK 中为 React 应用提供实时多人协同能力的官方绑定层,本指南以该包的公共 API 报告(packages/sync/api-report.api.md)为骨架,结合 packages/sync/src/useSync.ts、packages/sync/src/useSyncDemo.ts 等源码与测试,系统讲解useSync/useSyncDemo两个 React Hook 的全部公开类型、配置项、底层连接机制与实战用法。读完本文,你将能在一小时内为自己的 React 应用接入 tldraw 多人白板:从连接 WebSocket、上传图片资源,到展示在线光标、处理断线重连与鉴权失败。

@tldraw/sync是什么

@tldraw/sync是 tldraw SDK 的多人同步 React 绑定包(packages/sync/package.json 中描述为 "tldraw infinite canvas SDK (multiplayer sync react bindings)"),它把底层基于 WebSocket 的同步引擎@tldraw/sync-core包装成两个声明式的 React Hook:

  • useSync:连接任意自建服务器,uriconnect二选一;
  • useSyncDemo:直接连接 tldraw 官方演示服务器,一行代码即可完成原型。

从源码看,包的入口 packages/sync/src/index.ts 做了两件事:一是export * from '@tldraw/sync-core'把底层同步协议全部透传出去(例如TLSyncClientTLPersistentClientSocketClientWebSocketAdapter等,详见 packages/sync-core/src/index.ts),二是显式导出本包新增的useSyncuseSyncDemo及其配套类型,并在模块加载时注册库版本信息。也就是说,@tldraw/sync本质上是一个"薄封装 + 完整透传"的包:公共 API 报告中的每一项导出,都在 React 与同步引擎之间扮演明确角色。

RemoteTLStoreWithStatus:远程同步 store 的状态机

类型定义

export type RemoteTLStoreWithStatus = | Exclude< TLStoreWithStatus, { status: 'synced-local' } | { status: 'not-synced' } | { status: 'synced-remote' } > | (Extract<TLStoreWithStatus, { status: 'synced-remote' }> & { readonly objectAccess: TLObjectStoreAccess })

RemoteTLStoreWithStatususeSyncuseSyncDemo的返回值类型。与基础类型TLStoreWithStatus不同,它显式排除了not-synced(本地未同步)与synced-local(仅本地)两种状态——因为远程协同 store 的生命周期只有三种可能:正在连接、已同步到服务器、出错。源码 packages/sync/src/useSync.ts 中的注释明确写道:远程 store 始终处于 loading、connected 或 error 之一。

三种状态与 objectAccess

synced-remote分支上,类型额外挂载了一个readonly objectAccess: TLObjectStoreAccess字段。它表示服务器为本次会话授予的对象存储通道(object-store lane,例如评论记录)的写权限,与画布只读状态相互独立——也就是说,一个会话可以被允许评论但不被允许编辑画布。该字段默认值为'write'(见 packages/sync/src/useSync.ts)。

从 packages/sync/src/useSync.ts 的返回值推导逻辑可以看出完整状态机:

内部状态返回状态说明
尚未初始化 / 无 readyClient{ status: 'loading' }正在建立连接并同步初始状态
出现 error{ status: 'error', error }连接失败或同步错误(TLRemoteSyncError
连接成功{ status: 'synced-remote', connectionStatus, store, objectAccess }store可直接传给<Tldraw />

其中connectionStatus取值来自TLPersistentClientSocketStatus,为'online' | 'offline' | 'error'(见 packages/sync-core/src/lib/TLSyncClient.ts),且当 socket 处于 error 时会归一化为'offline'

典型消费模式

function MyCollaborativeApp() { const store: RemoteTLStoreWithStatus = useSync({ uri: 'wss://myserver.com/sync/room-123', assets: myAssetStore }) if (store.status === 'loading') { return <div>Connecting to multiplayer session...</div> } if (store.status === 'error') { return <div>Connection failed: {store.error.message}</div> } // store.status === 'synced-remote' return <Tldraw store={store.store} /> }

<Tldraw store={store} />会自行处理三种状态(loading/error/synced-remote)并渲染对应的界面;而显式分支写法则适合做自定义加载页或错误提示。值得注意的是 packages/sync/src/useSync.test.tsx 中专门有一条测试验证:当房间从旧 URI(例如NOT_FOUND)切换到新房间时,旧客户端的 error 会被清除,新房间正常进入synced-remote——这是onLoad回调中刻意保留objectAccess、丢弃旧 error 的防护逻辑(packages/sync/src/useSync.ts)。

useSync:连接自建服务器的核心 Hook

签名与选项类型

export function useSync(opts: UseSyncOptions & TLStoreSchemaOptions): RemoteTLStoreWithStatus

UseSyncOptions是联合类型,由两个互斥分支构成:

export type UseSyncOptions = UseSyncOptionsWithUri | UseSyncOptionsWithConnectFn export interface UseSyncOptionsWithUri extends UseSyncOptionsBase { uri: string | (() => string | Promise<string>) connect?: never } export interface UseSyncOptionsWithConnectFn extends UseSyncOptionsBase { connect: UseSyncConnectFn uri?: never }
  • uri模式:WebSocket 地址,可静态字符串,也可异步函数(每次连接尝试都会重新调用,便于携带会过期的鉴权 token 或做动态房间路由)。HTTP/HTTPS 地址会被自动升级为 WebSocket 连接。
  • connect模式:自定义传输层工厂函数,返回TLPersistentClientSocket,适合不想用默认 WebSocket 传输的场景(如自定义长连接协议)。

源码 packages/sync/src/useSync.ts 对两种模式做了强制校验:同时传uriconnect会抛出'uri and connect cannot be used together';两者都不传则抛出'uri or connect must be provided'

uri模式的保留查询参数

uri模式下,useSync内部通过ClientWebSocketAdapter建立连接(packages/sync-core/src/lib/ClientWebSocketAdapter.ts),并自动在 URI 上追加两个查询参数:

  • sessionId:当前浏览器标签页的会话标识(TAB_ID),同一用户的多标签页会被识别为不同会话;
  • storeId:本次 hook 实例生成的唯一 store 标识(uniqueId())。

sessionIdstoreId保留参数名——如果用户自己的 URI 中已经包含它们,会抛出明确错误(packages/sync/src/useSync.ts):

if (withParams.searchParams.has('sessionId')) { throw new Error('useSync. "sessionId" is a reserved query param name. Please use a different name') }

服务端正是依据这两个参数区分会话、识别同一房间内的多个连接。你自己的鉴权参数(如?token=...)需使用其他名字。

选项总览:UseSyncOptionsBase

选项类型必填说明
assetsTLAssetStore图片/视频等二进制资源的上传与解析实现。生产环境必须提供,否则大文件会以 base64 内联存储,导致序列化性能问题
uristring \| (() => string \| Promise<string>)二选一WebSocket 服务器地址,可动态返回(如携带刷新后的 token)
connectUseSyncConnectFn二选一自定义传输层工厂,返回TLPersistentClientSocket
usersTLUserStore用户身份、在线状态与操作归属(attribution)数据源
getUserPresence(store, user) => TLPresenceStateInfo \| null自定义广播给其他客户端的 presence 数据(光标、选区等)
onCustomMessageReceived(data: any) => void接收其他客户端经TLSyncClient.sendMessage发送的自定义消息
themesPartial<TLThemes>自定义主题色名,会在 store 构建前自动注册,保证持久化数据通过校验
onMount(editor: Editor) => void否(@internal)编辑器挂载回调
roomIdstring否(@internal)房间标识,仅用于埋点
trackAnalyticsEvent(name, data) => void否(@internal)分析事件上报

选项详解

assets: TLAssetStore(必填)——资源存储抽象,必须实现upload(上传新文件并返回 URL)与resolve(展示时按上下文解析 URL)。原型阶段可用inlineBase64AssetStore,但生产环境必须替换为外部对象存储。源码注释特别强调:不提供 assets 时,大图片与视频会被内联为 base64,序列化性能显著下降(packages/sync/src/useSync.ts)。

const myAssetStore: TLAssetStore = { upload: async (asset, file) => { const url = await uploadToCloudStorage(file) return { src: url } }, resolve: (asset, context) => { return getOptimizedUrl(asset.src, context) } }

users?: TLUserStore——用户 store,currentUser提供当前用户身份(用于 presence 广播与形状归属),可选resolve(userId)按 ID 解析其他用户。两者都返回响应式Signal。若不提供,useSync会回退到基于 localStorage 默认偏好实现的defaultUserStore,并用createCachedUserResolve包装:先尝试从 store 中的instance_presence记录反查用户信息(packages/sync/src/useSync.ts)。匿名会话时currentUser会退化为基于getUserPreferences()创建的匿名用户记录,但归属(attribution)仍正确返回 null(见 packages/sync/src/useSync.ts 的注释说明)。

getUserPresence?——响应式函数,store 状态变化时被调用,决定广播给其他客户端的 presence 对象。默认实现getDefaultUserPresence包含标准光标位置与选区状态;自定义实现可附加当前工具、视图状态等数据,返回null则隐藏 presence(packages/sync/src/useSync.ts):

getUserPresence: (store, user) => ({ userId: user.id, userName: user.name, cursor: { x: 100, y: 200 }, currentTool: 'select', isActive: true })

onCustomMessageReceived?——自定义消息通道。配合TLSyncClient.sendMessage使用,可在形状与 presence 同步之外实现任意客户端间通信,例如聊天消息:

onCustomMessageReceived: (data) => { if (data.type === 'chat') { displayChatMessage(data.message, data.userId) } }

themes?: Partial<TLThemes>——自定义主题。传入后useSync会在构建 store 前调用resolveThemesregisterColorsFromThemesregisterFontsFromThemes完成注册,使带自定义颜色的持久化数据在加载时能通过校验(packages/sync/src/useSync.ts)。

动态鉴权示例

function AuthenticatedApp() { const store = useSync({ // 每次连接尝试都会重新执行,token 过期后可以刷新 uri: async () => { const token = await getAuthToken() return `wss://myserver.com/sync/room-123?token=${token}` }, assets: authenticatedAssetStore, users: myUserStore, getUserPresence: (store, user) => ({ userId: user.id, userName: user.name, cursor: getCurrentCursor(store) }) }) return <Tldraw store={store} /> }

底层生命周期:TLSyncClient与连接状态

useSync内部把配置编译为一个TLSyncClient<TLRecord, TLStore>(packages/sync/src/useSync.ts),并围绕它做了四件关键工作:

  1. 连接状态信号化:把 socket 的connectionStatus包装成atom('collaboration status', ...)onStatusChange时更新,并以collaboration.status注入 store(packages/sync/src/useSync.ts)。
  2. 只读模式切换onAfterConnect时根据服务端下发的isReadonly在事务内设置syncModereadonlyreadwrite,同时调用store.ensureStoreIsUsable()做崩溃恢复兜底(packages/sync/src/useSync.ts)。
  3. presence 派生:通过createPresenceStateDerivation(currentUser, { getUserPresence })(store)构建响应式 presence;还用一个computed根据instance_presence记录数量判断当前房间是否只有自己一个会话(otherSessions.size === 0),决定 presence 同步频率走solo还是full模式(packages/sync/src/useSync.ts)。
  4. 错误分类上报onSyncError根据TLSyncErrorCloseEventReason区分NOT_FOUND(房间不存在)、FORBIDDEN(无权限)、NOT_AUTHENTICATED(未认证)、RATE_LIMITED(限流)等场景,记录埋点后以TLRemoteSyncError设置错误状态并关闭 socket(packages/sync/src/useSync.ts)。

组件卸载时(effect cleanup)会依次执行client.close()socket.close(),并通过didCancel标记防止异步回调在卸载后继续写入状态(packages/sync/src/useSync.ts)。

useSyncDemo:一行代码接入官方演示服务器

签名与选项

export function useSyncDemo(options: UseSyncDemoOptions & TLStoreSchemaOptions): RemoteTLStoreWithStatus export interface UseSyncDemoOptions { roomId: string // 房间 ID,命名空间与所有使用 demo 服务器的人共享 users?: TLUserStore // 可选,默认基于 localStorage host?: string // @internal,演示服务器地址 getUserPresence?(store: TLStore, user: TLUser): TLPresenceStateInfo | null }

使用示例

function MyApp() { const store = useSyncDemo({ roomId: 'my-app-test-room' }) return <Tldraw store={store} /> }

这是接入多人白板的最小可用代码:useSyncDemo内部自动完成三件事(packages/sync/src/useSyncDemo.ts):

  1. 构造连接地址`${host}/connect/${encodeURIComponent(roomId)}`,默认 host 为https://demo.tldraw.xyz(可通过TLDRAW_BEMO_URL环境变量覆盖);
  2. createDemoAssetStore(host)生成一套开箱即用的资源存取实现(上传到 demo 服务器、经TLDRAW_IMAGE_URL指向的图片优化服务做按需缩放);
  3. onMount中注册 URL 外部资源处理器,通过{host}/bookmarks/unfurl服务抓取链接元数据生成 bookmark 卡片。

此外它还会把自定义的shapeUtils/bindingUtils与默认实现合并(packages/sync/src/useSyncDemo.ts),测试 packages/sync/src/useSyncDemo.test.ts 对这一行为有明确断言。

官方演示服务器的限制

请务必注意 demo 服务器的数据语义(源码 JSDoc 明确声明,见 packages/sync/src/useSyncDemo.ts):

  • 数据大约一天后删除,只适合原型与演示;
  • 数据公开:任何知道 roomId 的人都能访问。建议用公司/项目名做前缀避免碰撞,追求隐私请直接生成 UUID 作为房间名;
  • 图片上传被禁用:当 host 属于tldraw.com/tldraw.xyz域(含子域)时,upload会弹出提示并抛错,防止滥用 demo 基础设施(packages/sync/src/useSyncDemo.ts,测试见 packages/sync/src/useSyncDemo.test.ts)。

Demo 资源存储的智能图片优化

createDemoAssetStoreresolve实现展示了 tldraw 的资源优化思路(packages/sync/src/useSyncDemo.ts):

  • 视频、data: URL、动图(isAnimatedImageType)、矢量图(isVectorImageType)不做变换,直接返回原 URL;
  • 只有托管在 tldraw 自有域名上的图片才走优化服务,外部域名原样返回;
  • 对 ≥ 1.5 MB 的图片按steppedScreenScaledpr与网络类型(非 4g 网络补偿系数 0.5)计算目标宽度,追加w参数交给图片 worker 缩放。

这些逻辑与上传文件名校验(非字母数字字符替换为-)一起,由 packages/sync/src/useSyncDemo.test.ts 中的多组用例覆盖验证。

从 API 到实战:仓库中的完整接入范例

仓库的 templates/sync-cloudflare 模板展示了useSync在真实前后端架构中的完整用法。其客户端房间页 templates/sync-cloudflare/client/pages/Room.tsx 直接从 URL 参数取房间号并建立连接:

export function Room() { const { roomId } = useParams<{ roomId: string }>() // Create a store connected to multiplayer. const store = useSync({ // We need to know the websockets URI... uri: `${window.location.origin}/api/connect/${roomId}`, // ...and how to handle static assets like images & videos assets: multiplayerAssetStore, }) return ( <RoomWrapper roomId={roomId}> <Tldraw store={store} options={{ deepLinks: true }} onMount={(editor) => { editor.registerExternalAssetHandler('url', getBookmarkPreview) }} /> </RoomWrapper> ) }

这个例子浓缩了接入useSync的全部要素:URI 拼接/api/connect/${roomId}由同源的 Cloudflare Worker 代理 WebSocket)、资源存储multiplayerAssetStore处理图片/视频)、store 透传<Tldraw store={store} />自动处理 loading 与光标/在线用户等多人 UX)、书签解包registerExternalAssetHandler('url', ...))。类似的用法还出现在 templates/simple-server-example/src/client/App.tsx 与 templates/socketio-server-example/src/client/App.tsx 中。

环境要求与安装

  • 安装yarn add @tldraw/sync(或npm install @tldraw/sync)。
  • React 版本:peer 依赖要求react/react-dom^18.2.0 || ^19.2.1(见 packages/sync/package.json)。
  • Node 版本>= 22.12.0
  • 许可@tldraw/sync是 tldraw SDK 的一部分,遵循 packages/sync/LICENSE.md 中的 SDK 许可协议;免费使用需保留画布上的 "Made with tldraw" 水印,移除水印需购买商业许可(详见 packages/sync/README.md)。
  • 版本注册:包入口会调用registerTldrawLibraryVersion注册库名、版本与模块信息(packages/sync/src/index.ts),便于 SDK 内部识别与调试。

常见问题速查

Q1:useSync报错 "uri or connect must be provided"?两者必须二选一提供;同时提供则报 "uri and connect cannot be used together"(packages/sync/src/useSync.ts)。

Q2:URI 里带了sessionId/storeId参数?这两个是保留参数名,会被 hook 自动注入,用户自定义同名参数会触发显式报错。请改用其他名字携带鉴权信息。

Q3:为什么连上了却看不到其他用户的光标?检查usersgetUserPresence:presence 数据由getUserPresence响应式生成并广播;默认实现依赖instance_presence记录,多标签页/多设备属于不同会话,需要各自连接同一房间才会互相可见。

Q4:生产环境能否用useSyncDemo不能。demo 数据约一天后删除、完全公开,且 tldraw 官方域名禁止图片上传。生产场景应自建服务器并使用useSync+ 自定义TLAssetStore

Q5:服务端返回不同错误时如何区分处理?store.status === 'error'store.errorTLRemoteSyncError,其 reason 可对应NOT_FOUNDFORBIDDENNOT_AUTHENTICATEDRATE_LIMITED等场景(见 packages/sync-core/src/lib/TLSyncClient.ts 中的TLSyncErrorCloseEventReason),据此可定制房间不存在、未登录、被限流等不同 UI。

【免费下载链接】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),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询