tldraw `persistenceKey` 全解析:浏览器端文档持久化与多标签页实时同步
2026/9/8 19:03:31 网站建设 项目流程

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

【免费下载链接】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

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

  • 未提供 key(L26-L32):直接createTLStore创建普通内存 store,状态标记为not-synced——这正是“刷新即丢”的来源;
  • 提供了 key:先创建 store,再用它实例化TLLocalSyncClient(L70-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.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 入门级示例(frontmatterpriority: 1,keywords 涵盖persistencelocal storageindexeddbsession storageauto save)的原因:它是理解 tldraw 同步模型的最佳第一课。

小结

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

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

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

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

立即咨询