首页
/ tldraw 多人在线协作:为 `useSyncDemo` 房间接入自定义用户身份与偏好(TLUserStore 实战指南)

tldraw 多人在线协作:为 `useSyncDemo` 房间接入自定义用户身份与偏好(TLUserStore 实战指南)

2026-09-08 17:20:52作者:伍霜盼Ellen

将应用自己的用户体系接入 tldraw 多人同步会话,是构建协作白板类产品时的常见需求。本文基于 apps/examples/src/examples/users/sync-custom-user 官方示例展开,讲解如何让 tldraw 的多人在线编辑真正"认识"本地用户:既能在协作者列表中正确展示名字与颜色,又能让编辑器内置的偏好面板将用户设置写回你自己的存储。读完本文,你将掌握 TLUserStoreuseSyncDemouseTldrawCurrentUser 三个 API 的配合方式,并理解 useSync(自建服务器)下的同一套接入模型。

一、问题背景:多人在线会话为什么需要"用户身份"

一个多人房间的同步链路需要知道两件事:

  1. 展示身份——本地用户的名称与颜色要同步给其他协作者,用于光标、选框、涂鸦等"presence"(在场信息)展示,以及形状上的归属信息(attribution);
  2. 编辑身份——编辑器内置的名字、颜色、偏好设置控件,需要有一个读写本地用户偏好的统一入口,修改后能立即反映到编辑器界面并持久化到你自己的后端。

官方示例给出的结论是:把用户身份放在 React state(真实应用中是你的认证系统),用一层"桥"把它转成 useSyncDemo 能识别的 TLUserStore(其 currentUser 是一个响应式 signal),同时把 useTldrawCurrentUser 生成的 TLCurrentUser 传给 <Tldraw> 组件,让内置偏好控件能够回写。参考实现见 SyncCustomUser.tsx

二、完整示例:一段代码看懂数据流

示例的完整组件代码如下(与仓库中的 SyncCustomUser.tsx 保持一致,代码中 [1]~[5] 注释对应下文拆解):

import { useSyncDemo } from '@tldraw/sync'
import { useEffect, useMemo, useRef, useState } from 'react'
import {
	atom,
	computed,
	createUserId,
	Tldraw,
	TldrawOptions,
	TLUserPreferences,
	TLUserStore,
	UserRecordType,
	useTldrawCurrentUser,
} from 'tldraw'
import 'tldraw/tldraw.css'

export default function SyncCustomUserExample({ roomId }: { roomId: string }) {
	// [1] 用户偏好保存在 React state 中,示例里用 useState 模拟认证系统/后端
	const [userPreferences, setUserPreferences] = useState<TLUserPreferences>({
		id: 'user-' + Math.random(),
		name: 'Jimmothy',
		color: 'palevioletred',
		colorScheme: 'dark',
	})

	// [2] 用 atom 镜像 React state,再用 computed 派生出 TLUser 记录
	const userPrefsAtom = useRef(atom<TLUserPreferences>('userPrefs', userPreferences)).current
	useEffect(() => {
		userPrefsAtom.set(userPreferences)
	}, [userPreferences, userPrefsAtom])

	const users: TLUserStore = useMemo(() => {
		const currentUser = computed('currentUser', () => {
			const p = userPrefsAtom.get()
			return UserRecordType.create({
				id: createUserId(p.id),
				name: p.name ?? '',
				color: p.color ?? '',
			})
		})
		return { currentUser }
	}, [userPrefsAtom])

	// [3] 创建多人同步 store,并把用户 store 传进去
	const store = useSyncDemo({ roomId, users })

	// [4] 把偏好与 setter 变成编辑器读写用的 TLCurrentUser
	const user = useTldrawCurrentUser({ userPreferences, setUserPreferences })

	// [5] 用同步 store 与用户对象渲染编辑器
	return (
		<div className="tldraw__editor">
			<Tldraw store={store} user={user} options={options} />
		</div>
	)
}

const options: Partial<TldrawOptions> = { deepLinks: true }

这段代码同时出现在 tldraw 示例源码与示例目录的 README 注释中,官方把每一段要做什么、为什么这样做都写成了行内注释,下面逐一拆解。

三、分步拆解 [1]~[5]:身份如何从 React 流入同步层

[1] 用户偏好:TLUserPreferences

useState 保存本地用户的偏好对象,类型为 TLUserPreferences。示例中只有 idnamecolorcolorScheme 四个字段,但在真实应用中它来自你的用户上下文或后端接口。该类型完整定义在 packages/editor/src/lib/config/TLUserPreferences.ts,可用字段与取值如下:

字段 类型/取值 含义
id string 用户唯一 ID,是后续生成 user 记录的基础
name string | null(可选) 显示给协作者的名字
color string | null(可选) 协作者光标/选择框等用的颜色
colorScheme 'light' | 'dark' | 'system'(可选) UI 明暗主题,示例用 'dark'
locale string | null(可选) 界面语言
animationSpeed number | null(可选) 动画速度
areKeyboardShortcutsEnabled boolean | null(可选) 是否启用键盘快捷键
edgeScrollSpeed number | null(可选) 边缘滚动速度
isSnapMode boolean | null(可选) 默认是否开启吸附
isWrapMode boolean | null(可选) 默认是否开启环绕模式
isDynamicSizeMode boolean | null(可选) 动态尺寸模式
isPasteAtCursorMode boolean | null(可选) 在光标处粘贴
enhancedA11yMode boolean | null(可选) 增强可访问性模式
inputMode 'trackpad' | 'mouse' | null(可选) 输入设备模式
isZoomDirectionInverted boolean | null(可选) 是否反转缩放方向

需要说明的是:这里 id 的可选字段也由同文件中的 userTypeValidator 定义(见 TLUserPreferences.ts),其中 colorScheme 仅接受 'light' | 'dark' | 'system' 三个枚举值,写错会在校验层被拒绝。

[2] 桥接层:React state → atom → computed → TLUserStore

useSyncDemo 需要的是一个 TLUserStore:它的 currentUser 必须是响应式 signal 而非 React state(同步引擎会在每次渲染/订阅变更时读取它)。于是示例用两层信号做桥:

  • atom('userPrefs', userPreferences) 创建镜像原子,再通过 useEffect 在每次 userPreferences 变化时 set 进去;
  • computed('currentUser', ...) 派生一个响应式的 TLUser 记录:读取 atom 中的偏好,再通过 UserRecordType.create(...)createUserId(...) 构造 user 记录。

TLUserStore 在此处的最小形态就是 { currentUser }currentUser 是一个返回 TLUser 的 signal。对应类型定义在 packages/sync/src/useSync.ts。把这段逻辑独立出来而不是直接改 state,正是为了给同步引擎提供"纯响应式"的数据源。

[3] 把用户 store 交给 useSyncDemo

const store = useSyncDemo({ roomId, users })

useSyncDemo 的选项类型 UseSyncDemoOptions 定义在 packages/sync/src/useSyncDemo.ts,关键参数包括:

  • roomId: string——要加入的房间 ID。演示服务器的命名空间是全公开共享的,官方注释建议用公司或项目名做前缀,或用 UUID 保证隐私;
  • users?: TLUserStore——用于身份、presence 与归属信息的用户 store;若不传,则使用基于 localStorage 的默认实现
  • getUserPresence?——可选的 presence 数据定制函数(与 useSyncUseSyncOptions.getUserPresence 一致);
  • host(内部使用)——演示服务器地址。

useSyncDemo 在内部实际是调用 useSync,把 uri 拼接为 ${host}/connect/${encodeURIComponent(roomId)},并对演示服务器做了资源托管与书签解链等封装(见 useSyncDemo.ts)。同步引擎创建时会按"本地的 currentUser 优先、否则回退到 localStorage 匿名偏好"的方式推导当前用户(见 useSync.ts)。

[4] 编辑器回写通道:useTldrawCurrentUser

const user = useTldrawCurrentUser({ userPreferences, setUserPreferences })

useTldrawCurrentUser 把"偏好对象 + setter"包装成编辑器统一读写的 TLCurrentUser,其接口定义为:

interface TLCurrentUser {
	userPreferences: Signal<TLUserPreferences>
	setUserPreferences: (userPreferences: TLUserPreferences) => void
}

实现见 packages/editor/src/lib/config/createTLCurrentUser.ts:当你不传任何选项时,createTLCurrentUser 会默认使用基于 localStorage 的偏好读写(数据键为 TLDRAW_USER_DATA_v3,见 TLUserPreferences.ts);一旦像示例这样传入 userPreferencessetUserPreferences,编辑器内置的名字、颜色、偏好控件就都会走你的 React state 回写,而不是写入 localStorage。这正是"Bring your own identity"的关键一步。

[5] 渲染

<Tldraw store={store} user={user} options={options} />

把同步 store(含房间状态与加载状态管理)和用户对象一起交给 <Tldraw>options 里开启 deepLinks: true,便于多人会话通过 URL 深链进入同一房间。

四、底层支撑:TLUser 记录与 UserRecordType

示例中真正同步出去的"用户"是一条 TLUser 记录,它由 UserRecordType.create 生成。记录结构定义在 packages/tlschema/src/records/TLUser.ts

export interface TLUser extends BaseRecord<'user', TLUserId> {
	name: string
	color: string
	imageUrl: string
	meta: JsonObject
}

其中 id 的类型是 TLUserId,由 createUserId(id: string): TLUserId 生成——它基于你偏好的原始 id 字符串构造出符合 user 记录校验规则的 ID(见 TLUser.ts)。示例只显式传了 idnamecolor 三个字段,imageUrlmeta 则由默认属性补齐为空字符串与 {}

值得注意的细节是 user 记录是"document scope"(文档级作用域)的,见 createUserRecordTypescope: 'document' 的声明:它随文档快照、剪贴板、.tldr 文件一起持久化,从而让归属(attribution)显示名跨看板、跨会话存活。如果想为用户记录附加自定义元数据(如 isAdmindepartment),可以用 createUserRecordType({ meta: { ... } }) 自定义验证器。

五、如何验证:打开两个窗口

示例 README 提供了一个立即可行的验收方式:在两个浏览器窗口中打开该示例。由于每个窗口都会生成随机的 user-' + Math.random() id,但名字与颜色固定为 Jimmothy / palevioletred,你会看到两个同名的 "Jimmothy" 协作者出现在对方的会话中——光标、选择框等 presence 都由各自窗口的 TLUserStore 提供,以此确认身份桥接已生效。

六、从 useSyncDemouseSync:自建服务器的同一套选项

useSyncDemo 面向 tldraw 官方托管的演示服务器(默认 https://demo.tldraw.xyz,可通过环境变量 TLDRAW_BEMO_URL 覆盖,见 useSyncDemo.ts)。官方对其数据有两条明确说明:

  • 演示服务器上的数据约一天后会被删除
  • 数据对任何知道 roomId 的人公开可访问——因此建议用公司名做房间前缀,或使用 UUID 保证隐私。

当你运行自己的同步服务器时,示例中构建 users(即 TLUserStore)的逻辑可以原样移植:README 明确指出 useSync 接受完全相同的 users 选项。这一点在实现层面得到印证:useSyncDemo 本身就是把 useSyncDemo 的选项透传给 useSync 的封装(useSyncDemo.ts),因此 roomIdusersgetUserPresence、schema 相关选项在两者间保持一致。

更进一步,若你的房间里有历史协作者(来自别的会话、name/color 未在你本地 currentUser 中命中),useSync 在未提供自定义 users.resolve 时会尝试从本地 instance_presence 记录中还原其名字与颜色作为兜底(见 useSync.ts)。示例只传了 currentUser,即使用该兜底实现。

七、关联阅读

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

项目优选

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