☰
@tldraw/tlschema 源码级解析:tldraw 持久化数据的类型系统、运行时校验与迁移机制
2026/10/3 8:22:17 网站建设 项目流程

@tldraw/tlschema 源码级解析:tldraw 持久化数据的类型系统、运行时校验与迁移机制

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

本文以仓库内 packages/tlschema/README.md 为主线,结合该包内 DOCS.md 与src/下真实实现,系统讲解 tldraw SDK 数据层最核心的@tldraw/tlschema包:它定义了编辑器默认持久化数据的 Record 类型、Shape 类型、Asset 类型,以及驱动数据演进的迁移(migration)体系。读完你将掌握如何自定义 shape/asset、如何安全地修改持久化数据结构并编写可双向执行的迁移,以及如何通过createTLSchema组装属于自己的数据模型。

一、包定位:tldraw 数据层的「宪法」

@tldraw/tlschema是 tldraw 编辑器持久化数据的类型定义、schema 迁移与类型元数据所在的基础包。它回答三个问题:

  • 数据长什么样——所有会落盘/同步的记录(shape、asset、page、camera、instance 等)的类型结构;
  • 数据是否合法——提供运行时校验器(validator),保证进入 store 的每条记录符合约定;
  • 数据如何演进——当结构变更(加字段、删类型、合并类型)时,提供能把旧版本数据迁移到新版本的迁移序列。

在包内目录结构上,三类核心代码分别位于:

  • packages/tlschema/src/records/——根 Record 类型(TLShape、TLAsset、TLPage、TLCamera、TLInstance、TLDocument 等);
  • packages/tlschema/src/shapes/——具体 shape 子类型(geo、arrow、text、draw、image、video 等);
  • packages/tlschema/src/assets/——具体 asset 子类型(image、video、bookmark);
  • packages/tlschema/src/styles/——样式属性(StyleProp)机制;
  • packages/tlschema/src/store-migrations.ts与各记录文件的 migrations——迁移序列。

二、三种核心类型:Record、Shape 与 Asset

按 README 的划分,包内主要有三类类型:

1. Record 类型(根记录类型)

Record 是加入Store类的根记录类型,定义在src/records目录。每条记录具备id、typeName与类型专属属性。以 shape 记录为例(src/records/TLShape.ts):

const shape: TLShape = { id: 'shape:abc123', typeName: 'shape', type: 'geo', x: 100, y: 200, rotation: 0, // ... 其他属性 }

所有记录都继承自BaseRecord语义,id使用带前缀的 branded string(如shape:、page:、asset:),在编译期杜绝不同类型的 ID 混用。

2. Shape 类型(形状子类型)

Shape 是根TLShape记录的子类型。它通过「唯一名称 + 自定义 props」定义一种特定形状。内置默认形状在TLDefaultShape联合类型中列出(见 src/records/TLShape.ts 即 src/records/TLShape.ts):

export type TLDefaultShape = | TLArrowShape // 箭头,可绑定到其他形状 | TLBookmarkShape // 书签卡片 | TLDrawShape // 手绘路径 | TLEmbedShape // 内嵌内容(YouTube、Figma 等) | TLFrameShape // 画框容器 | TLGeoShape // 几何形状(矩形、椭圆、三角形等) | TLGroupShape // 编组 | TLImageShape // 位图 | TLLineShape // 多点折线/样条 | TLNoteShape // 便签 | TLTextShape // 富文本 | TLVideoShape // 视频 | TLHighlightShape // 荧光笔笔迹

每个具体 shape 都会继承TLBaseShape提供的公共基础字段(src/shapes/TLBaseShape.ts):

interface TLBaseShape<Type, Props> { id: TLShapeId typeName: 'shape' type: Type x: number // 位置 X y: number // 位置 Y rotation: number // 弧度制旋转角 index: IndexKey // 分数索引,用于排序 parentId: TLParentId // 父级:页面或另一个形状(frame/group) isLocked: boolean // 是否锁定 opacity: TLOpacityType // 透明度 0-1 props: Props // 形状专属属性 meta: JsonObject // 用户自定义元数据 }

3. Asset 类型(资源子类型)

Asset 是根TLAsset记录的子类型,代表图片、视频、书签等外部资源,同样以「唯一名称 + 自定义 props」扩展。默认资产类型为TLImageAsset | TLVideoAsset | TLBookmarkAsset(src/records/TLAsset.ts)。例如一个图片资产:

const imageAsset: TLDefaultAsset = { id: 'asset:image123', typeName: 'asset', type: 'image', props: { src: 'https://example.com/image.jpg', w: 800, h: 600, mimeType: 'image/jpeg', isAnimated: false, name: 'image.jpg', }, meta: {}, }

三、组装 Schema:createTLSchema与默认配置

createTLSchema是把所有类型汇总成TLSchema(即StoreSchema<TLRecord, TLStoreProps>)的入口(src/createTLSchema.ts):

import { createTLSchema, defaultShapeSchemas, defaultBindingSchemas, defaultAssetSchemas } from '@tldraw/tlschema' // 使用全部默认形状/绑定/资产 const schema = createTLSchema() // 在默认形状基础上追加自定义形状 const customSchema = createTLSchema({ shapes: { ...defaultShapeSchemas, myCustomShape: { props: myCustomShapeProps, migrations: myCustomShapeMigrations, }, }, bindings: defaultBindingSchemas, assets: defaultAssetSchemas, })

createTLSchema支持的可选参数(对应源码createTLSchema({ shapes, bindings, assets, user, records, migrations })):

参数类型说明
shapesRecord<string, SchemaPropsInfo>shape 类型注册表,默认defaultShapeSchemas
bindingsRecord<string, SchemaPropsInfo>绑定(如箭头与形状的关系)注册表,默认defaultBindingSchemas
assetsRecord<string, SchemaPropsInfo>asset 类型注册表,默认defaultAssetSchemas
userUserSchemaInfo用户记录的自定义 meta 校验器与迁移
recordsRecord<string, CustomRecordInfo>额外的自定义根记录类型
migrationsreadonly MigrationSequence[]追加的迁移序列

每个SchemaPropsInfo含三部分:props(属性校验器)、meta(元数据校验器)、migrations(数据演进迁移,可为 legacy、props 级或通用序列)。

值得注意的实现细节:createTLSchema会自动收集所有 shape 中使用的StyleProp并检查重复 id——若两个StyleProp实例 id 相同则会抛错Multiple StyleProp instances with the same id。同时,自定义records的名称不能与内建类型名(asset、binding、camera、document、instance、page、shape、user等 12 个)冲突,否则抛错。这保证了 schema 的唯一性与可预测性。

Schema 组装完成后,交给@tldraw/store的Store:

import { Store } from '@tldraw/store' import { TLStoreProps, createTLSchema } from '@tldraw/tlschema' const schema = createTLSchema() const store = new Store({ schema, props: { defaultName: 'Untitled', assets: assetStore, // 你的资产存储实现 onMount: (editor) => { console.log('Editor mounted with store') }, }, })

四、StyleProp:可共享、可记忆的样式属性

StyleProp是 tldraw 数据模型里非常特殊的一类属性(src/styles/StyleProp.ts),它遵守两条规则:

  1. 同一值可同时应用在多个形状上(如全选后统一改颜色);
  2. 最近一次使用的值会被自动记住,并应用到之后新建的形状上。

定义自定义样式属性有两种方式:

import { StyleProp, EnumStyleProp } from '@tldraw/tlschema' import { T } from '@tldraw/validate' // 自由值样式:数值型 const MyWidthStyle = StyleProp.define('myapp:width', { defaultValue: 2, type: T.number, }) // 枚举样式:限定可选值 const MyPatternStyle = StyleProp.defineEnum('myapp:pattern', { defaultValue: 'solid', values: ['solid', 'dashed', 'dotted'], })

每个StyleProp必须有全局唯一 id(官方建议用「应用名/库名:属性名」前缀,如myapp:width)。EnumStyleProp还支持运行期增删枚举值(addValues/removeValues,内部会重建T.literalEnum校验器),便于在运行时扩展内置样式(例如追加自定义颜色)。

在 shape 的 props 定义里,用StyleProp声明的字段即成为样式属性,例如 tldraw 内置的DefaultColorStyle、DefaultFillStyle、DefaultDashStyle、DefaultSizeStyle、DefaultFontStyle等(见 src/styles/)。TLShape中的getShapePropKeysByStyle(src/records/TLShape.ts)会从 props 校验器中提取样式属性到字段的映射,并校验「同一个 StyleProp 在一个 shape 内只能使用一次」。

五、添加迁移:修改持久化结构的标准流程

这是 README 的核心章节。只要修改了本包内任何持久化数据的形状,就必须添加能把旧版本转换为新版本(反之亦然)的迁移。

5.1 修改 Record / Shape / Asset 结构

若变更影响某个记录、形状或资产的结构,在定义该类型同一文件内更新其迁移。官方示例(给TLShape添加ownerId属性):

在TLShape.ts中新增版本号:

const Versions = { RemoveSomeProp: 1, + AddOwnerId: 2, } as const

在TLShape类型中新增字段:

x: number y: number + ownerId: ID<TLUser> | null props: Props parentId: ID<TLShape> | ID<TLPage>

然后添加迁移:

export const shapeTypeMigrations = defineMigrations({ currentVersion: Versions.Initial, firstVersion: Versions.Initial, migrators: { + [Versions.AddOwnerId]: { + // 升级:添加 ownerId 属性 + up: (shape) => ({...shape, ownerId: null}), + // 降级:移除 ownerId 属性 + down: ({ownerId, ...shape}) => shape, + } },

5.2 修改 Store 整体结构

若变更影响整个 store 的结构(重命名/删除类型、合并两种 shape 为一个等),把迁移加在schema.ts的 migrations 里(实际位于 src/store-migrations.ts,并在createTLSchema中注册为storeMigrations)。真实的 store 级迁移序列storeMigrations(sequenceIdcom.tldraw.store)提供了很好的参考:

  • RemoveCodeAndIconShapeTypes:从 storage 中删除type === 'icon' | 'code'的旧 shape;
  • AddInstancePresenceType:新增实例在线状态记录类型(up 为 noop);
  • RemoveTLUserAndPresenceAndAddPointer:删除user/user_presence记录,引入 pointer 记录;
  • RemoveUserDocument:删除废弃的user_document记录;
  • FixIndexKeys:修正分数索引——旧库生成的 index 不允许以0结尾('a0'例外),会把末尾0替换为随机 base62 数字;对 line shape 的points索引同样处理。

可以看到迁移分两种 scope:storage级(遍历整个存储快照,增删记录)与record级(针对单条记录做字段变换)。

5.3 迁移的强制测试

添加迁移后,必须在src/migrations.test.ts中添加对应测试——README 明确写道:"It will complain if you do not!"即测试文件会主动校验「每个新迁移都有测试覆盖」。这是保证迁移正确性的工程护栏。包内测试还包括src/store-migrations.test.ts、src/TLStore.test.ts、src/recordsWithProps.test.ts、src/createTLSchema.test.ts等,共同验证迁移双向可执行与 store 行为。

六、迁移系统的底层原理

6.1 props 迁移如何变成 store 迁移

当你在createTLSchema里注册某个 shape 的migrations时,src/recordsWithProps.ts 的processPropsMigrations会把它统一包装成 store 可执行的MigrationSequence:

  • 未提供 migrations:自动生成retroactive: true的空序列,为未来迁移预留位置;
  • 提供带sequenceId的序列:校验其 sequenceId 必须等于com.tldraw.${typeName}.${subType}(如com.tldraw.shape.geo),不匹配直接断言失败;
  • 提供sequence数组:每条TLPropsMigration经createPropsMigration转换为 store 迁移,其filter只作用于typeName === typeName && type === subType的记录,up/down对record.props做变换;
  • legacy 格式(defineMigrations的migrators对象):按版本号升序转换,并标注"未来将被移除"。

6.2 版本 ID 与双向迁移

迁移 id 遵循命名约定com.tldraw.${typeName}.${subType}/${version}(例如com.tldraw.shape.geo/1)。官方提供了工具函数生成这类 id:

const myShapeVersions = createShapePropsMigrationIds('custom', { AddColor: 1, AddSize: 2, RefactorProps: 3, }) // => { AddColor: 'com.tldraw.shape.custom/1', ... }

每条TLPropsMigration(src/recordsWithProps.ts)包含:

字段说明
id迁移唯一 id
dependsOn?依赖的其他迁移 id
up升级变换(必填)
down?降级变换。官方建议:部署超过几个月的 down 迁移可以退休('retired'或'none'),主要用于平滑浏览器长驻标签页的版本过渡

一个完整的多步迁移序列示例:

const migrations = createShapePropsMigrationSequence({ sequenceId: 'com.myapp.shape.custom', sequence: [ { id: 'com.myapp.shape.custom/1.1.0', up: (props) => ({ ...props, newProperty: 'default' }), down: ({ newProperty, ...props }) => props, }, { id: 'com.myapp.shape.custom/1.2.0', up: (props) => ({ ...props, renamedProperty: props.oldProperty, oldProperty: undefined }), down: (props) => ({ ...props, oldProperty: props.renamedProperty, renamedProperty: undefined }), }, ], })

6.3 根记录迁移实例

根 shape 记录的迁移rootShapeMigrations(sequenceIdcom.tldraw.shape,src/records/TLShape.ts)是理解「升级/降级必须对称」的绝佳案例:

  • AddIsLocked:up 加isLocked: false,down 删字段;
  • HoistOpacity:把透明度从props.opacity(字符串档位'0.1'/'0.25'/'0.5'/'0.75'/'1')提升为记录顶层数值,up 做Number(props.opacity ?? '1')转换,down 再按阈值映射回原字符串档位;
  • AddMeta:up 加meta: {};
  • AddWhite:up 为 noop,down 把props.color === 'white'回退为'black'(旧版不支持白色)。

由此可推断:每一条 up 迁移都应尽可能提供对称的 down 实现,down 可选但「平滑过渡」依赖它。

七、运行时校验与类型安全

7.1 校验器即类型

tlschema 用@tldraw/validate的T命名空间同时表达「运行时校验」与「TypeScript 类型」。createShapeValidator(src/shapes/TLBaseShape.ts)为一种 shape 生成完整记录校验器:

import { T } from '@tldraw/validate' import { createShapeValidator } from '@tldraw/tlschema' const customShapeValidator = createShapeValidator('myshape', { width: T.number.check((n) => n > 0), // 自定义校验:必须为正数 height: T.number.check((n) => n > 0), color: T.string, })

校验器会同时校验基础字段(id必须是shape:前缀、parentId必须以page:或shape:开头、opacity取值范围、index必须是合法 IndexKey 等)与类型专属 props/meta。当记录进入 store 时校验自动执行;T.union('type', ...)会按type字段分发到对应子校验器。

7.2 类型安全 ID

ID 是 branded string,编译期防止不同类型记录混用(src/records/TLShape.ts):

import { TLShapeId, TLPageId, createShapeId } from '@tldraw/tlschema' const shapeId: TLShapeId = createShapeId() // "shape:abc123" const customId: TLShapeId = createShapeId('my-rect') // "shape:my-rect" // const pageId: TLPageId = shapeId // 编译期报错!

isShape/isShapeId则是运行时类型守卫,配合store.get(id)后即可让 TypeScript 自动收窄类型。

7.3 校验失败处理

校验失败时抛出的错误携带path(出错路径)、message、value,便于定位问题;store 层面还通过onValidationFailure与createIntegrityChecker(见 src/TLStore.ts,在createTLSchema中被注册)提供一致性兜底。

八、典型扩展模式

8.1 完整自定义 shape(四步法)

以 DOCS.md 与 src/createTLSchema.ts 为准,完整注册一个自定义 shape:

import { createShapeValidator, createShapePropsMigrationSequence, RecordProps } from '@tldraw/tlschema' import { DefaultColorStyle } from '@tldraw/tlschema' import { T } from '@tldraw/validate' const MY_SHAPE_TYPE = 'myshape' // 1. 通过模块增强把自定义 props 挂到全局映射,获得类型推导 declare module '@tldraw/tlschema' { export interface TLGlobalShapePropsMap { [MY_SHAPE_TYPE]: MyShapeProps } } interface MyShapeProps { color: typeof DefaultColorStyle width: number height: number customData: string } type MyShape = TLShape<typeof MY_SHAPE_TYPE> // 2. 定义 props 校验 const myShapeProps: RecordProps<MyShape> = { color: DefaultColorStyle, width: T.number, height: T.number, customData: T.string, } // 3. 定义迁移(初始版本) const myShapeMigrations = createShapePropsMigrationSequence({ sequenceId: 'com.myapp.shape.myshape', sequence: [ { id: 'com.myapp.shape.myshape/1.0.0', up: (props) => props, down: (props) => props, }, ], }) // 4. 注册进 schema const schema = createTLSchema({ shapes: { ...defaultShapeSchemas, myshape: { props: myShapeProps, migrations: myShapeMigrations }, }, })

注意:TLGlobalShapePropsMap增强为null | undefined时可以禁用某个默认 shape 类型(TLIndexedShapes映射类型会将其过滤为never),而group类型是始终可用、不可覆盖的内核类型——这一逻辑直接体现在 src/records/TLShape.ts 的TLIndexedShapes条件类型中。

8.2 自定义资产与资产存储

通过TLGlobalAssetPropsMap增强注册自定义资产类型,并通过TLAssetStore接口对接存储后端:

const customAssetStore: TLAssetStore = { async upload(asset, file) { return await myCloudStorage.upload(file) }, async resolve(asset, context) { return await myCloudStorage.getUrl(asset.props.src, context) }, async remove(assetIds) { await Promise.all(assetIds.map((id) => myCloudStorage.delete(id))) }, }

8.3 自定义绑定

与 shape 类似,通过TLGlobalBindingPropsMap增强定义绑定类型(如TLArrowBinding,字段含fromId、toId、terminal、normalizedAnchor、isExact、isPrecise,见 src/bindings/TLArrowBinding.ts),再注册进bindings配置。

8.4 自定义用户元数据与自定义记录

createTLSchema支持给user记录添加 meta 校验(如isAdmin: T.boolean)与迁移;也支持通过records选项注册全新的根记录类型(scope 为document/session/presence),但不能与内建类型名冲突(见上文)。

九、与 @tldraw/store / @tldraw/state 的协同

  • 响应式查询:schema 定义好之后,store.query.records('shape')返回的查询可在@tldraw/state的track中响应式使用,形状变化时自动重跑;
  • 迁移入口:StoreSchema.create将记录类型、校验器、迁移序列、onValidationFailure、createIntegrityChecker统一收口(src/createTLSchema.ts),迁移由 store 在加载旧快照时自动按版本执行;
  • 调试:可通过schema.types查看各记录类型的校验器,通过schema.sortedMigrations查看完整迁移序列与 id,排查「旧文档加载失败」类问题时可手动调用migrator.migrateStoreSnapshot({ schema, store })定位失败步骤。

十、最佳实践小结

综合 README 与源码,维护 tlschema 相关代码时建议:

  1. 结构变更必带迁移:任何影响持久化结构的改动(字段增删、类型合并、语义变化)都要在同文件或store-migrations.ts添加版本与迁移,且必须在src/migrations.test.ts补测试;
  2. up/down 对称:每步迁移提供可逆的 down 实现,down 长期不用后可标'retired'退休,但近期版本过渡仍依赖它;
  3. 迁移 ID 有规律:遵循com.tldraw.{recordType}.{subType}/{version}约定,用createShapePropsMigrationIds/createAssetPropsMigrationIds等工具生成;
  4. 复用内置 StyleProp:能复用DefaultColorStyle等内置样式就不要自造,自定义时保证 id 全局唯一;
  5. 充分利用类型推导:通过TLGlobalShapePropsMap等映射增强,让自定义类型获得与内置类型一致的TLShape<'myshape'>推导能力;
  6. 迁移按 scope 选择:单条记录的字段变换用 record 级迁移,整库结构性调整(删除类型、替换记录体系)用 storage 级迁移。

@tldraw/tlschema是理解 tldraw 数据架构的钥匙:它用一套「类型 + 校验器 + 迁移」三位一体的设计,让无限画布应用在数据持续演进的同时,始终保证向后兼容与数据完整性。无论是编写自定义形状、接入自定义资产,还是为长期运营的应用维护数据升级路径,本包都是绕不开的核心。

参考资料

  • packages/tlschema/README.md——包定位、三类类型与添加迁移规范(本文主线)
  • packages/tlschema/DOCS.md——随包发布的详细 API 文档与使用示例
  • packages/tlschema/src/createTLSchema.ts——createTLSchema、defaultShapeSchemas、defaultBindingSchemas、defaultAssetSchemas
  • packages/tlschema/src/records/TLShape.ts——TLBaseShape结构、rootShapeMigrations、ID 工具与 props 迁移工具
  • packages/tlschema/src/recordsWithProps.ts——props 迁移到 store 迁移的转换与版本管理
  • packages/tlschema/src/store-migrations.ts——store 级迁移实例
  • packages/tlschema/src/styles/StyleProp.ts——样式属性机制
  • packages/tlschema/src/migrations.test.ts 及同目录测试——迁移与 store 行为的验证

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

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

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

立即咨询