@tldraw/tlschema 源码级解析:tldraw 持久化数据的类型系统、运行时校验与迁移机制
tldraw/tlschema 源码级解析tldraw 持久化数据的类型系统、运行时校验与迁移机制【免费下载链接】tldrawBuild infinite canvas apps in React with the tldraw SDK. Worlds 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、bookmarkpackages/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.tsconst 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.tsexport type TLDefaultShape | TLArrowShape // 箭头可绑定到其他形状 | TLBookmarkShape // 书签卡片 | TLDrawShape // 手绘路径 | TLEmbedShape // 内嵌内容YouTube、Figma 等 | TLFrameShape // 画框容器 | TLGeoShape // 几何形状矩形、椭圆、三角形等 | TLGroupShape // 编组 | TLImageShape // 位图 | TLLineShape // 多点折线/样条 | TLNoteShape // 便签 | TLTextShape // 富文本 | TLVideoShape // 视频 | TLHighlightShape // 荧光笔笔迹每个具体 shape 都会继承TLBaseShape提供的公共基础字段src/shapes/TLBaseShape.tsinterface TLBaseShapeType, 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 | TLBookmarkAssetsrc/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: {}, }三、组装 SchemacreateTLSchema与默认配置createTLSchema是把所有类型汇总成TLSchema即StoreSchemaTLRecord, TLStoreProps的入口src/createTLSchema.tsimport { 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 })参数类型说明shapesRecordstring, SchemaPropsInfoshape 类型注册表默认defaultShapeSchemasbindingsRecordstring, SchemaPropsInfo绑定如箭头与形状的关系注册表默认defaultBindingSchemasassetsRecordstring, SchemaPropsInfoasset 类型注册表默认defaultAssetSchemasuserUserSchemaInfo用户记录的自定义 meta 校验器与迁移recordsRecordstring, 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的Storeimport { 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它遵守两条规则同一值可同时应用在多个形状上如全选后统一改颜色最近一次使用的值会被自动记住并应用到之后新建的形状上。定义自定义样式属性有两种方式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中的getShapePropKeysByStylesrc/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: IDTLUser | null props: Props parentId: IDTLShape | IDTLPage然后添加迁移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 级迁移序列storeMigrationssequenceIdcom.tldraw.store提供了很好的参考RemoveCodeAndIconShapeTypes从 storage 中删除type icon | code的旧 shapeAddInstancePresenceType新增实例在线状态记录类型up 为 noopRemoveTLUserAndPresenceAndAddPointer删除user/user_presence记录引入 pointer 记录RemoveUserDocument删除废弃的user_document记录FixIndexKeys修正分数索引——旧库生成的 index 不允许以0结尾a0例外会把末尾0替换为随机 base62 数字对 line shape 的points索引同样处理。可以看到迁移分两种 scopestorage级遍历整个存储快照增删记录与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。官方提供了工具函数生成这类 idconst myShapeVersions createShapePropsMigrationIds(custom, { AddColor: 1, AddSize: 2, RefactorProps: 3, }) // { AddColor: com.tldraw.shape.custom/1, ... }每条TLPropsMigrationsrc/recordsWithProps.ts包含字段说明id迁移唯一 iddependsOn?依赖的其他迁移 idup升级变换必填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 记录的迁移rootShapeMigrationssequenceIdcom.tldraw.shapesrc/records/TLShape.ts是理解「升级/降级必须对称」的绝佳案例AddIsLockedup 加isLocked: falsedown 删字段HoistOpacity把透明度从props.opacity字符串档位0.1/0.25/0.5/0.75/1提升为记录顶层数值up 做Number(props.opacity ?? 1)转换down 再按阈值映射回原字符串档位AddMetaup 加meta: {}AddWhiteup 为 noopdown 把props.color white回退为black旧版不支持白色。由此可推断每一条 up 迁移都应尽可能提供对称的 down 实现down 可选但「平滑过渡」依赖它。七、运行时校验与类型安全7.1 校验器即类型tlschema 用tldraw/validate的T命名空间同时表达「运行时校验」与「TypeScript 类型」。createShapeValidatorsrc/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 类型安全 IDID 是 branded string编译期防止不同类型记录混用src/records/TLShape.tsimport { 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 为准完整注册一个自定义 shapeimport { 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 TLShapetypeof MY_SHAPE_TYPE // 2. 定义 props 校验 const myShapeProps: RecordPropsMyShape { 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 相关代码时建议结构变更必带迁移任何影响持久化结构的改动字段增删、类型合并、语义变化都要在同文件或store-migrations.ts添加版本与迁移且必须在src/migrations.test.ts补测试up/down 对称每步迁移提供可逆的 down 实现down 长期不用后可标retired退休但近期版本过渡仍依赖它迁移 ID 有规律遵循com.tldraw.{recordType}.{subType}/{version}约定用createShapePropsMigrationIds/createAssetPropsMigrationIds等工具生成复用内置 StyleProp能复用DefaultColorStyle等内置样式就不要自造自定义时保证 id 全局唯一充分利用类型推导通过TLGlobalShapePropsMap等映射增强让自定义类型获得与内置类型一致的TLShapemyshape推导能力迁移按 scope 选择单条记录的字段变换用 record 级迁移整库结构性调整删除类型、替换记录体系用 storage 级迁移。tldraw/tlschema是理解 tldraw 数据架构的钥匙它用一套「类型 校验器 迁移」三位一体的设计让无限画布应用在数据持续演进的同时始终保证向后兼容与数据完整性。无论是编写自定义形状、接入自定义资产还是为长期运营的应用维护数据升级路径本包都是绕不开的核心。参考资料packages/tlschema/README.md——包定位、三类类型与添加迁移规范本文主线packages/tlschema/DOCS.md——随包发布的详细 API 文档与使用示例packages/tlschema/src/createTLSchema.ts——createTLSchema、defaultShapeSchemas、defaultBindingSchemas、defaultAssetSchemaspackages/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. Worlds best, top-most agent recommended #1 five star SDK.项目地址: https://gitcode.com/GitHub_Trending/tl/tldraw创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
上一篇/下一篇内容由系统自动关联
返回资讯列表 →