tldraw `persistenceKey` 全解析:浏览器端文档持久化与多标签页实时同步
persistenceKey 是 tldraw SDK 提供的一行式本地持久化方案:给 <Tldraw> 组件传入一个字符串 key,编辑器便会把整份画布文档自动存入浏览器的 IndexedDB,刷新页面后自动恢复,并通过 BroadcastChannel 让所有使用相同 key 的标签页实时保持同步。本文以官方示例 persistence-key 为主线,结合 editor 包的源码实现,讲解其用法、数据存储结构、同步协议与适用边界,帮助你为 React 应用一键接入“本地自动保存 + 多标签页协作”,或在此之上理解后续接入远程同步(tldraw sync)的基础。
什么是 persistenceKey:一次配置,双重能力
在 tldraw 中,编辑器组件默认使用的文档状态只存在于内存中,刷新页面即会丢失。而官方 persistence-key 示例 演示了最简接入方式——仅向 <Tldraw> 传入一个 persistenceKey,就能同时获得两项能力(原文描述):
- 浏览器内持久化:文档被存储在 IndexedDB 中该 key 对应的位置,下次加载时自动恢复;
- 多标签页同步:所有使用相同 key 的标签页通过一个广播通道保持数据一致。
换句话说,persistenceKey 承担的是“本地自动保存 + 标签间实时同步”的双重角色。在组件 API 上,它被定义在 TldrawEditorWithoutStoreProps 中,源码注释给出了一句话准则(见 TldrawEditor.tsx):
If you would like to persist the store to the browser's local IndexedDB storage and sync it across tabs, provide a key here. Each key represents a single tldraw document.
“每个 key 代表一份 tldraw 文档”这一点,是整个 persistenceKey 设计哲学的出发点。
一分钟上手:完整示例代码解析
官方示例组件只有不到 10 行,却体现了使用 persistenceKey 的所有要点:
import { Tldraw } from 'tldraw'
import 'tldraw/tldraw.css'
export default function PersistenceKeyExample() {
return (
<div className="tldraw__editor">
<Tldraw persistenceKey="persistence-key-example" />
</div>
)
}
要点拆解:
tldraw/tldraw.css:编辑器必需的基础样式,缺失会导致布局错乱;.tldraw__editor包裹容器:tldraw 编辑器要求父容器具备明确尺寸(否则画布无法正确测量),示例类名是该约定在示例库中的统一写法;persistenceKey="persistence-key-example":唯一的“魔法开关”,传入后组件内部自动切换到“本地同步”运行模式。
接入后即可按官方说明做如下验证:
- 在画布上随意绘制一些图形;
- 刷新页面——图形依然存在(数据已从 IndexedDB 恢复);
- 打开第二个标签页访问同一页面——两个标签页中的修改会互相实时出现(经由广播通道同步);
- 若在两个标签页中分别使用不同的
persistenceKey,则它们是彼此独立的两份文档。
用好 key:文档粒度而非应用粒度
persistenceKey 在示例 README 中被明确提示了最重要的设计约束:不同 key 是相互独立的文档,因此 key 应“按文档设置”(例如使用文档 id),而不是“每个应用只用一个 key”。
这背后的原因在源码中非常直观——key 直接参与了两处底层资源的命名:
- IndexedDB 数据库名:见 LocalIndexedDb.ts,库名拼接规则为
STORE_PREFIX + persistenceKey,即TLDRAW_DOCUMENT_v2 + key; - 广播通道名:见 TLLocalSyncClient.ts,通道名为
`tldraw-tab-sync-${persistenceKey}`。
也就是说,key 是数据在存储层与通信层的唯一命名空间。在实际产品中:
- 多文档应用(如笔记、白板列表)应传入各自文档的 document id,让每份画布彼此隔离;
- 单文档应用可直接用一个稳定常量;
- key 变更等于“换了一本全新的笔记本”——旧 key 下的数据不会被清理或迁移到新 key,因此不要在用户已有数据后随意更换 key 的取值规则。
底层原理:从 prop 到 IndexedDB 的完整调用链
理解了“做什么”,再看“怎么做”。当 persistenceKey 被传入时,组件内部并不直接读写 IndexedDB,而是经历了一条清晰的分层调用链:
<Tldraw persistenceKey>
└─ useLocalStore() // 选择同步策略
└─ new TLLocalSyncClient(store) // 持久化 + 跨标签同步客户端
├─ new LocalIndexedDb(key) // IndexedDB 读写封装
└─ BroadcastChannel('tldraw-tab-sync-' + key) // 标签间通信
入口:useLocalStore 决定“内存模式”还是“同步模式”
核心逻辑在 useLocalStore.ts。它通过 useEffect 判断是否提供了 persistenceKey:
- 未提供 key(L26-L32):直接
createTLStore创建普通内存 store,状态标记为not-synced——这正是“刷新即丢”的来源; - 提供了 key:先创建 store,再用它实例化
TLLocalSyncClient(L70-L81),并把 store 状态置为loading,待 IndexedDB 首次加载完成后通过onLoad回调切换为synced-local。
这个 Hook 还做了一件容易被忽略的事:把 assets(图片等二进制资源) 一并接入 IndexedDB。示例文档只说“存储文档”,而源码层面图片资源的 upload 走 client.db.storeAsset,resolve 则把数据库中的 Blob 转成 objectURL 供画布渲染(L38-L64)。这意味着只要使用 persistenceKey,你在画布里拖入的图片也会跟随文档被持久化,不需要额外写资产存储代码。
持久化引擎:TLLocalSyncClient
TLLocalSyncClient.ts 是这套本地方案的中枢,包含三个关键机制:
1. 节流写库(防抖持久化)。编辑器任意文档级变更(由用户操作产生、scope: 'document' 的改动)都会触发一次“调度写库”,但写入本身被节流:常量 PERSIST_THROTTLE_MS = 350 毫秒,即高频拖拽绘制时不会每帧都写数据库,而是合并后写入;若某次写入失败,重试间隔放宽到 PERSIST_RETRY_THROTTLE_MS = 10_000 毫秒。
2. 全量快照与增量 diff 两档写入。doPersist 时(L364-L417):首次或出错恢复时执行 storeSnapshot 全量快照;正常情况则先 squashRecordDiffs 合并积压的 diff,再执行 storeChanges 增量写入。写库期间新产生的变更会进入新的队列,保证数据不丢失。
3. 生命周期兜底。由于写库存在 350ms 节流,用户关闭/刷新标签页时最后一段编辑可能尚未落盘。因此客户端监听了 pagehide 事件和 visibilitychange(页面隐藏时)主动 flush 一次(L147-L167);如果 IndexedDB 写入抛错,除了弹出告警还会强制 window.location.reload(),用“重载 + 全量重写”的方式恢复一致性。
存储层:LocalIndexedDb
LocalIndexedDb.ts 用 idb 库封装了底层操作。值得注意的细节:
- 库名规则
TLDRAW_DOCUMENT_v2 + persistenceKey,因此每个 key 对应独立数据库(互不干扰,也无法互相覆盖); - 代码中还维护了
TLDRAW_DB_NAME_INDEX_v2这样的索引记录,用于清理遗留的旧版数据库(如TLDRAW_ASSET_STORE_v1),说明数据格式是带版本号演进的; - 首次启动时
load()读出的旧数据会经过 store 的 schema 迁移(migrateStoreSnapshot)再合入内存 store(见 TLLocalSyncClient.ts),因此旧版本的文档记录在新版本 SDK 中打开会被自动升级,这正是 SDK 数据向前兼容的落地方式。
多标签页同步协议:diff 广播与版本协商
标签页之间的“实时同步”并不依赖 IndexedDB 轮询,而是基于 BroadcastChannel。相关实现在 TLLocalSyncClient.ts 与 L228-L270,其消息协议只有两类:
| 消息类型 | 含义 | 触发时机 |
|---|---|---|
diff |
携带 RecordsDiff 增量变更与 schema 版本 |
本标签页 store 发生用户文档级变更时立即广播 |
announce |
声明本页 schema 版本 | 客户端连接成功、或发现对方版本落后时 |
一个标签页收到 diff 后,通过 store.applyDiff + mergeRemoteChanges 把对方变更合入自己的 store,本地再经由 React 响应式系统重绘画布——于是“A 页画一笔,B 页立刻显示”。
值得注意的版本协商逻辑:由于多标签页可能运行在不同 schema 版本的代码下(例如灰度发布期间),收到消息的标签页会先调用 getMigrationsSince 比较双方 schema:
- 若自己较旧,则通过
window.location.reload()刷新到新代码(刚启动 5 秒内遇到则不刷新而是直接报错,防止死循环); - 若对方较旧,则回发一条
announce通知对方刷新,并强制执行一次全量写库,防止旧代码把数据写坏(L231-L260)。
这套设计让“不同版本页面打开同一份文档”也能保持数据安全,是 persistenceKey 在简单 API 之下包含的健壮性细节。
单元测试与行为验证
仓库为这套机制提供了测试覆盖,可用来验证关键行为:
- TldrawEditor.test.tsx:专门覆盖了“使用
persistenceKey时依然正确透传assetsprop”的场景,印证了持久化与自定义资源存储可以共存; - TLLocalSyncClient.test.ts:针对同步客户端行为的单元测试。
你可以结合测试文件与上述源码,自行验证 key 隔离、节流写入、刷新恢复等行为是否符合预期。
适用边界与延伸:浏览器之外怎么办
示例 README 的最后一句给出了清晰的边界声明:如果需要把数据持久化到浏览器之外(跨设备、跨用户),请转而参考本仓库的 sync 与 snapshot 相关示例。
这句话指出了两类方案的分工:
persistenceKey:面向“单个浏览器内的本地自动保存 + 同机多标签页协同”,零后端、零配置,适合单机工具型应用与原型验证;- 远程同步:当需要多用户实时协作或跨设备访问时,应使用基于
TLRemoteSyncStore的同步方案。可参考 sync-demo 示例 以及仓库中同步相关的其他 collaboration 示例; - 快照导入导出:若只是想把当前文档状态导出、备份或迁移到另一浏览器,可选用
snapshot/initialData等一次性加载数据的方式。
从架构角度看,persistenceKey 本质上是 tldraw 的 TLSyncClient 架构在“本地存储”这一后端上的轻量实现:它复用了 store 的 diff/迁移机制与客户端-服务端消息协议的思想,只是把“远端服务器”换成了 BroadcastChannel、把“服务器数据库”换成了 IndexedDB。理解了 persistenceKey 的内部结构,再去看远程 sync 的客户端实现会容易得多——这也正是官方将其列为 configuration 入门级示例(frontmatter priority: 1,keywords 涵盖 persistence、local storage、indexeddb、session storage、auto save)的原因:它是理解 tldraw 同步模型的最佳第一课。
小结
一句话总结本文要点:persistenceKey 用“一个字符串 key”换来了“IndexedDB 自动持久化、刷新恢复、多标签页实时同步、二进制资产随文档存取、schema 自动迁移”这一整套本地数据能力,而其正确用法是 以文档为粒度设置 key(如 document id)。如需多人远程协作或跨设备漫游,则应在理解上述本地同步机制后,升级到仓库中的 sync 示例所演示的远程同步方案。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0631
MiniCPM5-2BMiniCPM5-2B 是一款面向端侧、本地部署和资源受限场景的 2B 稠密 Transformer,能够达到同尺寸开源模型 SOTA 水平。Markdown00
video-shotcraftAI宣传片skill,使用 Remotion 制作电影级产品视频:提供106 张镜头配方卡和可复用的视频魔板。适用于 Claude Code 与 Codex以及所有其他智能体Markdown00
HivisionIDPhotos⚡️HivisionIDPhotos: a lightweight and efficient AI ID photos tools. 一个轻量级的AI证件照制作算法。Python09
DragonOSDragonOS is an operating system developed from scratch using Rust, with Linux compatibility. It is designed for **Serverless** scenarios. 使用Rust从0自研内核,具有Linux兼容性的操作系统,面向云计算Serverless场景而设计。Rust00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00