tldraw 编辑器快照持久化实战:getSnapshot 与 loadSnapshot 的完整用法
2026/9/8 22:22:48 网站建设 项目流程

tldraw 编辑器快照持久化实战:getSnapshot 与 loadSnapshot 的完整用法

【免费下载链接】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 官方示例 Save and load snapshots 展开,讲解如何用getSnapshot()把编辑器内容序列化保存、用loadSnapshot()恢复、以及如何通过snapshot属性让编辑器从内置快照启动。读完后,你将掌握在自研 React 应用中实现画布文档持久化(localStorage、服务端、协作同步)的完整方案,并理解 snapshot 中documentsession两部分数据的边界与底层实现。

1. 示例目标与运行方式

官方示例位于 SnapshotExample.tsx,配套文档为 README.md。示例的核心交互是:

  1. 画布加载时自带内容——通过<Tldraw snapshot={jsonSnapshot}>传入一个内置的 snapshot.json;
  2. 在画布上画一些内容,点击Save snapshot,把当前编辑器状态序列化并存入localStorage
  3. 继续修改画布,点击Load snapshot,把画布恢复回第 2 步保存时的样子(内容 + 相机视角 + 选区)。

示例代码中的工具栏通过useEditor()获取编辑器实例,再拿到editor.store(tldraw 的数据存储层),所有快照 API 都以 store 为操作对象:

import { getSnapshot, loadSnapshot, TLComponents, Tldraw, TldrawUiButton, TLEditorSnapshot, useEditor, } from 'tldraw' import 'tldraw/tldraw.css' import _jsonSnapshot from './snapshot.json' const jsonSnapshot = _jsonSnapshot as any as TLEditorSnapshot function SnapshotToolbar() { const editor = useEditor() const save = () => { const { document, session } = getSnapshot(editor.store) localStorage.setItem('snapshot', JSON.stringify({ document, session })) } const load = () => { const snapshot = localStorage.getItem('snapshot') if (!snapshot) return loadSnapshot(editor.store, JSON.parse(snapshot)) } return ( <div className="tlui-menu snapshot-toolbar"> <TldrawUiButton type="normal" onClick={save}>Save snapshot</TldrawUiButton> <TldrawUiButton type="normal" onClick={load}>Load snapshot</TldrawUiButton> </div> ) } const components: TLComponents = { SharePanel: SnapshotToolbar, // 挂在分享面板位置,避免遮挡画布 } export default function SnapshotExample() { return ( <div className="tldraw__editor"> <Tldraw snapshot={jsonSnapshot} components={components} /> </div> ) }

样式见 snapshots.css,其中“Saved”提示通过data-visible属性控制缩放与透明度过渡,配合一个 1 秒后清除的setTimeout实现保存成功反馈。

2. TLEditorSnapshot:document 与 session 的双结构

getSnapshot(editor.store)返回一个TLEditorSnapshot对象,其类型定义在 TLEditorSnapshot.ts:

export interface TLEditorSnapshot { document: TLStoreSnapshot // 文档数据:页面、形状、资产 session: TLSessionStateSnapshot // 每用户会话状态 }

两部分职责分明,这正是示例文档(README)强调的核心点:

  • document:内容数据。包含所有pageshapeasset等文档范围的记录,以及一份 schema 描述(记录类型的版本号与序列计数)。它是“画布上有什么”。
  • session:每用户的编辑器会话状态,是“这个用户此刻在怎么看画布”。

从 TLSessionStateSnapshot.ts 的接口定义看,session的结构为:

export interface TLSessionStateSnapshot { version: number currentPageId?: TLPageId isFocusMode?: boolean exportBackground?: boolean isDebugMode?: boolean isToolLocked?: boolean isGridMode?: boolean pageStates?: Array<{ pageId: TLPageId camera?: { x: number; y: number; z: number } selectedShapeIds?: TLShapeId[] focusedGroupId?: TLShapeId | null }> }

即:当前页面、各种开关(专注模式、调试模式、网格、工具锁定、导出背景)以及每页的相机(x/y/z)、选中的形状列表和聚焦的组。pageStates是“每页一份”的,因此多页文档恢复后,每个页面都会回到各自的相机与选区。

工程建议(示例源码注释 [1] 原话):在多用户应用中,通常应把documentsession分开存储,让每个用户各自保留自己的session;示例为了演示简单,把两者一起序列化进了localStorage

3. getSnapshot 的底层实现

getSnapshot的实现只有 10 行,位于 TLEditorSnapshot.ts:

export function getSnapshot(store: TLStore): TLEditorSnapshot { const sessionState$ = sessionStateCache.get(store, createSessionStateSnapshotSignal) const session = sessionState$.get() if (!session) { throw new Error('Session state is not ready yet') } return { document: store.getStoreSnapshot(), session, } }

几个值得注意的点:

  • document部分直接调用store.getStoreSnapshot(),即对 store 中全部记录做浅拷贝序列化;
  • session部分来自一个响应式信号createSessionStateSnapshotSignal,见 TLSessionStateSnapshot.ts),它用computedTLINSTANCE_ID记录、每页的CameraRecordTypeInstancePageStateRecordType记录实时计算得出,带isEqual比较以支持响应式订阅;
  • 若实例状态尚未就绪,getSnapshot会抛出Session state is not ready yet。也就是说,不要在编辑器刚创建、store 还未初始化完成时调用它。

4. loadSnapshot 的恢复逻辑与 forceOverwriteSessionState

loadSnapshot(store, snapshot, opts)的完整实现在 TLEditorSnapshot.ts,其恢复流程在一个store.atomic(...)事务中完成:

  1. 兼容旧格式:如果传入的对象带store字段,说明是一个老式TLStoreSnapshot(没有 document/session 之分)。实现会先调用store.schema.migrateStoreSnapshot()做 schema 迁移,再过滤掉非 document 范围的记录,自动把它转成新的{ document, session }结构——这是从源码结构看对旧版.tldr导出的向后兼容处理;
  2. 先捕获“需保留”的状态:在清库前,先pluckPreservingValues取出当前 instance 记录中的粘性字段,并缓存当前 session 快照;
  3. 加载 documentstore.loadStoreSnapshot(snapshot.document)——若提供 document,它会先清空 store 再写入;
  4. 恢复粘性实例/会话状态:把第 2 步捕获的值写回,避免加载快照导致编辑器出现“跳变”;
  5. 恢复 session:若快照带session,调用loadSessionStateSnapshotIntoStore恢复相机、选区、开关等。

关键选项是TLLoadSnapshotOptions.forceOverwriteSessionState

export interface TLLoadSnapshotOptions { forceOverwriteSessionState?: boolean }

默认情况下,isDebugModeisGridMode等会话开关不会被快照覆盖——源码注释说明这些字段被视为用户“粘性(sticky)”偏好,而文档数据则不是。loadSessionStateSnapshotIntoStore内部会做 primary/secondary 合并(见 TLSessionStateSnapshot.ts):默认以现有实例状态优先,仅在需要时用快照值填补;只有显式传true才让快照中的开关值强制生效。

此外,session 快照自带版本号(当前为version: 0)与迁移函数,未来结构变化时可平滑升级;每次加载都会经过 validator 校验,非法数据会被console.warn并跳过。

loadSnapshot支持只传部分结构:你可以只传{ document },之后再单独恢复{ session },也可以完全跳过 session(示例源码注释 [2])。这让“只恢复内容、不恢复视角”成为可能。

5. snapshot 属性:让编辑器带内容启动

<Tldraw>组件接受snapshot属性,编辑器创建时即把快照载入 store,所以示例画布一开始就有内容(注释 [3])。该属性的类型与loadSnapshot一致,是Partial<TLEditorSnapshot> | TLStoreSnapshot——既可以传完整的双结构快照,也可以传旧式 store 快照,底层同样走迁移与过滤逻辑。仓库中另一个使用同类型的是 TldrawImage.tsx,它把快照直接渲染为 SVG 图片(例如用作分享卡片),snapshot属性是其必传项。

示例自带的 snapshot.json 是一个约 30KB 的完整快照文件,其结构可以直观印证前文的类型定义:

{ "document": { "store": { "document:document": { "gridSize": 10, "name": "", "id": "document:document", "typeName": "document" }, "page:page": { "id": "page:page", "name": "Page 1", "index": "a1", "typeName": "page" }, "asset:-2122303015": { "type": "image", "props": { "name": "tldrawFile", "src": "data:image/png;base64,..." }, "typeName": "asset" } // …还有 shape、binding 等文档范围记录 }, "schema": { "schemaVersion": 2, "sequences": { "com.tldraw.store": 4, "com.tldraw.shape": 4, /* …每类记录的序列计数 */ } } }, "session": { "version": 0, "currentPageId": "page:page", "exportBackground": true, "isFocusMode": false, "isDebugMode": true, "isToolLocked": false, "isGridMode": false, "pageStates": [ { "pageId": "page:page", "camera": { "x": -367.04, "y": -293.85, "z": 1 }, "selectedShapeIds": [], "focusedGroupId": null } ] } }

其中schema.sequences为每个记录类型维护序列号,用于后续写入时生成稳定的自增 ID;store中以typeName:id命名的键即 store 记录本身(documentpageassetshapebinding等)。

6. 从示例走向生产:持久化方案要点

  • 保存时机:示例是手动点击按钮保存。生产环境通常监听 store 变更做防抖自动保存(store支持订阅),或提供显式导出为.tldr文件的入口;
  • document 与 session 分存:单用户应用(如本地草稿)可以像示例一样两者一起存localStorage;多用户/协作应用应把document交给同步层(tldraw 的 sync 系列包),session只留在本地;
  • 注意 session 的“粘性”默认值:恢复用户视角与文档内容通常没问题,但isDebugModeisGridMode等开关默认不被覆盖,需要时用forceOverwriteSessionState: true
  • 旧数据兼容:如果你手里有旧版导出的纯 store 快照(带store字段),直接传给loadSnapshotsnapshot属性即可,SDK 会自动迁移并剥离非文档记录;
  • 就绪检查getSnapshot在 session 状态未就绪时会抛错,调用应放在编辑器初始化完成后(如事件回调、按钮点击中),避免在渲染期间立即调用。

7. 小结

tldraw 的快照机制把“内容”与“会话”显式分层:getSnapshot()给出{ document, session }loadSnapshot()在一个原子事务中恢复两者并保留必要的粘性状态,<Tldraw snapshot={...}>则让应用可以从持久化的初始内容直接启动。三者都以TLStore为中心,类型约束、schema 迁移与版本校验齐备,可直接用于本地草稿恢复、文件导入导出或协作同步层的落地。完整可运行代码参见 SnapshotExample.tsx,API 实现参见 TLEditorSnapshot.ts 与 TLSessionStateSnapshot.ts。

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

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

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

立即咨询