首页
/ tldraw `persistenceKey` 全解析:浏览器端文档持久化与多标签页实时同步

tldraw `persistenceKey` 全解析:浏览器端文档持久化与多标签页实时同步

2026-09-07 22:41:05作者:宣海椒Queenly

persistenceKey 是 tldraw SDK 提供的一行式本地持久化方案:给 <Tldraw> 组件传入一个字符串 key,编辑器便会把整份画布文档自动存入浏览器的 IndexedDB,刷新页面后自动恢复,并通过 BroadcastChannel 让所有使用相同 key 的标签页实时保持同步。本文以官方示例 persistence-key 为主线,结合 editor 包的源码实现,讲解其用法、数据存储结构、同步协议与适用边界,帮助你为 React 应用一键接入“本地自动保存 + 多标签页协作”,或在此之上理解后续接入远程同步(tldraw sync)的基础。

什么是 persistenceKey:一次配置,双重能力

在 tldraw 中,编辑器组件默认使用的文档状态只存在于内存中,刷新页面即会丢失。而官方 persistence-key 示例 演示了最简接入方式——仅向 <Tldraw> 传入一个 persistenceKey,就能同时获得两项能力(原文描述):

  1. 浏览器内持久化:文档被存储在 IndexedDB 中该 key 对应的位置,下次加载时自动恢复;
  2. 多标签页同步:所有使用相同 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":唯一的“魔法开关”,传入后组件内部自动切换到“本地同步”运行模式。

接入后即可按官方说明做如下验证:

  1. 在画布上随意绘制一些图形;
  2. 刷新页面——图形依然存在(数据已从 IndexedDB 恢复);
  3. 打开第二个标签页访问同一页面——两个标签页中的修改会互相实时出现(经由广播通道同步);
  4. 若在两个标签页中分别使用不同的 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

  • 未提供 keyL26-L32):直接 createTLStore 创建普通内存 store,状态标记为 not-synced——这正是“刷新即丢”的来源;
  • 提供了 key:先创建 store,再用它实例化 TLLocalSyncClientL70-L81),并把 store 状态置为 loading,待 IndexedDB 首次加载完成后通过 onLoad 回调切换为 synced-local

这个 Hook 还做了一件容易被忽略的事:把 assets(图片等二进制资源) 一并接入 IndexedDB。示例文档只说“存储文档”,而源码层面图片资源的 uploadclient.db.storeAssetresolve 则把数据库中的 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.tsidb 库封装了底层操作。值得注意的细节:

  • 库名规则 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.tsL228-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 时依然正确透传 assets prop”的场景,印证了持久化与自定义资源存储可以共存;
  • 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 涵盖 persistencelocal storageindexeddbsession storageauto save)的原因:它是理解 tldraw 同步模型的最佳第一课。

小结

一句话总结本文要点:persistenceKey 用“一个字符串 key”换来了“IndexedDB 自动持久化、刷新恢复、多标签页实时同步、二进制资产随文档存取、schema 自动迁移”这一整套本地数据能力,而其正确用法是 以文档为粒度设置 key(如 document id)。如需多人远程协作或跨设备漫游,则应在理解上述本地同步机制后,升级到仓库中的 sync 示例所演示的远程同步方案。

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

项目优选

收起
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
860
1.35 K
docsdocs
暂无描述
Markdown
899
5.83 K
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
925
1.85 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.84 K
1.02 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
533
601
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
1.03 K
525
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
395