
tldraw 工具库 tldraw/utils 完全指南索引键、媒体处理与通用工具 API 深度解析【免费下载链接】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本文以 tldraw 仓库中由 API Extractor 自动生成的 tldraw/utils API 报告 为骨架结合 源码实现 与配套测试系统梳理这一无限画布 SDK 私有工具包的全部公开与内部 API。读完你将掌握 tldraw 用于图层排序的分数索引Fractional Indexing体系、媒体与文件处理工具、Result 错误处理范式、缓存与定时器设施以及一批可直接复用到任何前端项目的 TypeScript 工具函数。一、包定位tldraw 的瑞士军刀工具层在 tldraw 的 monorepo 结构中packages/utilsnpm 包名tldraw/utils当前版本 5.4.0声明于 package.json扮演着所有上层包tldraw/editor、tldraw/tldraw、tldraw/store、tldraw/sync等共享的基础设施角色。官方 README 对它只有一句朴素描述Utility functions used by tldrawtldraw 使用的工具函数但这份 api-report.api.md 揭示的实际内容远不止此——它涵盖了几何数学、排序索引、媒体探测、文件转换、缓存、定时、重试、错误处理、类型体操等约 60 个导出符号。该报告由 API Extractor 自动生成是包发布时的契约快照每个导出都带有public对外稳定 API或internal仅仓库内部使用的可访问性标记。public的 API 可放心直接使用而internal的导出如assert、retry、ExecutionQueue等虽在报告中可见但按 tldraw 的版本语义不属于对外稳定承诺消费时应视为实现细节。依赖方面该包只引入了 4 个 lodash 子模块package.jsonlodash.isequal、lodash.isequalwith、lodash.throttle、lodash.uniq并原样转发为isEqual、isEqualWith、throttle、uniq四个导出——这是包保持轻量的关键设计。包的 入口文件 还会在加载时调用registerTldrawLibraryVersion从globalThis上读取TLDRAW_LIBRARY_NAME/VERSION/MODULES来登记当前运行库的版本信息便于运行时诊断与版本告警。二、排序核心IndexKey 分数索引与图层重排tldraw 画布中每个形状都带有index属性用于决定形状的绘制与堆叠顺序。这个index的类型就是IndexKey——一个带品牌标记string { __brand: indexKey }的字符串由整数部分 小数部分组成基于著名的 Fractional Indexing 算法 实现。2.1 为什么不用连续整数排序传统做法1、2、3……在中间插入时会出现需要重排所有后续元素的问题。分数索引的做法是每次插入都在相邻两个 key 之间取中点生成新 key只要 key 空间足够稠密base-62 字母表 任意长度小数部分就几乎永远不需要重排已有元素——这对高频拖拽换层、多人协同同时插入的场景至关重要。2.2 tldraw 的实现特化tldraw 将上游fractional-indexing与jittered-fractional-indexing两个包 vendored 并裁剪进 fractionalIndexing.ts两者均为 CC0-1.0 公有领域协议。该文件头注释说明了三处针对热路径的优化固定 base-62 字母表0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz将上游可配置的digits参数特化掉把validateOrderKey中每次调用都会做的digits[0].repeat(26)分配提升为模块常量SMALLEST_INTEGER查表替代 indexOfdigitIndex()用字符码算术code - 48 / - 55 / - 61直接算出 digit 值避免在每次生成 key 时线性扫描字母表JITTER 抖动JITTER_BITS 16即新生成的 key 会在目标区间内做 16 次随机二分游走。每多一位抖动位约增加 0.17 个字符的 key 长度换来并发插入的碰撞概率降低——注释中估算 16 位足以让约 10 个客户端在同一位置同时插入时碰撞率低于 0.1%为多人实时协同留足余量。2.3 公开 API 一览IndexKey体系在 reordering.ts 中对外暴露全部为public函数签名要点行为ZERO_INDEX_KEYIndexKey常量值为a0即第一个合法索引getIndexAbove(below?)below?: IndexKey \| null返回某索引之上的一个新 key默认从null顶端开始getIndexBelow(above?)同上返回某索引之下的一个新 keygetIndexBetween(below, above)两个边界可空返回两索引之间的一个 keygetIndices(n, start?)start默认a1返回[start, ...n 个递增 key]getIndicesAbove/Below/Between批量版本一次生成 n 个均匀分布的 keysortByIndex(a, b)要求{ index: IndexKey }按 index 字典序比较返回 -1/0/1sortByMaybeIndex(a, b)允许index为 null带 null 处理的排序比较器validateIndexKey(index)internal断言字符串是合法 IndexKey非法则抛错源码示例reordering.ts给出的典型输出const indices getIndicesBetween(a0 as IndexKey, a2 as IndexKey, 2) console.log(indices) // [a0V, a1] const index getIndexBetween(a0 as IndexKey, a2 as IndexKey) console.log(index) // a1 const shapes [ { id: b, index: a2 as IndexKey }, { id: a, index: a1 as IndexKey }, ] const sorted shapes.sort(sortByIndex) // [{ id: a, index: a1 }, { id: b, index: a2 }]值得注意的实现细节是 reordering.ts 中的一行generateKeysFn在测试环境NODE_ENV test下使用无抖动的generateNKeysBetween保证测试输出确定可断言生产环境才使用generateNJitteredKeysBetween。这也是配套测试 fractionalIndexing.test.ts 与 reordering.test.ts 能稳定断言输出的原因。2.4 排序家族的其他成员报告中的sortById要求{ id: any }仅返回 -1/1不含相等分支位于 sort.tsrotateArray(arr, offset)、compact、dedupe、partition、last、maxBy、minBy等数组工具集中在 array.ts其中dedupe支持传入自定义相等比较函数rotateArray支持负数偏移实现反向旋转。三、媒体工具箱MediaHelpers 与 PngHelperstldraw/utils提供了一套完备的浏览器媒体处理工具是 tldraw 拖入图片/视频、生成缩略图、导出截图等功能的底层支撑实现位于 lib/media 目录。3.1 受支持的媒体类型常量API 报告中共有 5 个相关常量全部public且经Object.freeze冻结见 media.tsDEFAULT_SUPPORTED_IMAGE_TYPES全部支持的图片类型 ——image/apng、image/avif、image/gif、image/jpeg、image/png、image/svgxml、image/webpDEFAULT_SUPPORT_VIDEO_TYPES视频类型 ——video/mp4、video/quicktime、video/webmDEFAULT_SUPPORTED_MEDIA_TYPES图片 视频的并集DEFAULT_SUPPORTED_MEDIA_TYPE_LIST上述并集拼接成的逗号分隔字符串可直接赋值给input typefile accept...的accept属性内部另有DEFAULT_SUPPORTED_STATIC_IMAGE_TYPESjpeg/png/webp、DEFAULT_SUPPORTED_VECTOR_IMAGE_TYPESsvgxml、DEFAULT_SUPPORTED_ANIMATED_IMAGE_TYPESgif/apng/avif三组细分常量它们被isStaticImageType、isVectorImageType、isAnimatedImageType分别引用。3.2 MediaHelpers 静态方法详解MediaHelpers类media.ts提供以下核心能力loadVideo(src, doc?)创建video元素加载视频设置crossOrigin anonymous在loadeddata事件时 resolve失败时 rejectCould not load videogetVideoFrameAsDataUrl(video, time 0)把视频某帧默认第 0 秒绘制到 canvas 并导出 data URL内部监听loadedmetadata/loadeddata/canplay/seeked事件等待readyState达标后用canvas.toDataURL()取帧实现上用promiseWithResolve构造可外部控制的 Promise并在finally中清理所有监听器避免内存泄漏getImageAndDimensions(src, doc?)加载图片并返回{ w, h, image }。实现中有一个针对 Firefox 的特殊分支media.tsFirefox 对 SVG 不提供naturalWidth/naturalHeight因此需临时把图片挂到 DOM 上用clientWidth/clientHeight测量图片本身会被隐藏visibility:hidden; position:absolute; opacity:0并设置referrerPolicy strict-origin-when-cross-origingetImageSize(blob, doc?)从 Blob 读取图片尺寸返回{ w, h, pixelRatio }。对 PNG 会进一步解析pHYs块见下文 PngHelpers读取 DPI 信息分别尝试 96Windows/Web 基线与 72macOS 基线两种标准若得到大于 1 的整数倍率则按该倍率折算物理尺寸并返回pixelRatiomedia.tsgetVideoSize(blob, doc?)通过usingObjectURLloadVideo读取视频的videoWidth/videoHeightisAnimated(file)按 MIME 类型分发到各格式解析器——GIF、AVIF、WebP、APNG 分别由 gif.ts、avif.ts、webp.ts、apng.ts 中的字节级解析函数判断是否含动画帧每种格式都有配套测试如 gif.test.tsusingObjectURL(blob, fn)创建URL.createObjectURL执行异步函数并在finally中revokeObjectURL是 Blob → URL → 消费 → 回收的标准模式封装isImageType / isStaticImageType / isAnimatedImageType / isVectorImageType基于上述常量集合的 MIME 类型判定。3.3 PngHelpersPNG 二进制级操作PngHelperspng.ts直接操作DataView提供对 PNG 文件格式的底层解析isPng(view, offset)校验 PNG 魔数getChunkType(view, offset)读取 chunk 的类型四字节findChunk(view, type)/readChunks(view)按类型查找或遍历全部 chunk返回{ start, size, dataOffset }parsePhys(view, offset)解析pHYs物理像素块得到{ ppux, ppuy, unit }setPhysChunk(view, dpr?, options?)向 PNG 写入新的pHYs块并返回新的 Blob——这是 tldraw 导出高 DPI PNG 截图的关键步骤。四、文件与网络FileHelpers、fetch、Image4.1 FileHelpersfile.ts 中的FileHelperspublic封装了最常见的文件/Blob 操作全部为静态方法urlToArrayBuffer(url)fetch后取response.arrayBuffer()urlToBlob(url)fetch后取response.blob()urlToDataUrl(url)若 URL 已是data:前缀则原样返回避免重复编码否则先取 Blob 再转 data URLblobToDataUrl(blob)基于FileReader.readAsDataURL返回 base64 data URLblobToText(blob)基于FileReader.readAsText读取 UTF-8 文本rewriteMimeType(blob, newMimeType)重写 MIME 类型。有两个重载Blob/File 各自返回对应类型实现上若类型相同直接返回原对象若是File则构造新File([blob], blob.name, { type })保留文件名否则构造新Blob([blob], { type })。const dataUrl await FileHelpers.urlToDataUrl(https://example.com/image.png) const text await FileHelpers.blobToText(userFile) const jsonFile FileHelpers.rewriteMimeType(file, application/json) console.log(jsonFile.name) // 原文件名被保留4.2 fetch / Image 与存储包导出fetch与Imageinternal实现于 network.ts二者分别是 Web 标准fetch与new Image()的轻量封装Image支持可选宽高参数internal的存储工具storage.tsx提供getFromLocalStorage、setInLocalStorage、deleteFromLocalStorage、clearLocalStorage及SessionStorage同款 4 个方法——这些内部封装统一了存储访问入口便于未来替换实现safeParseUrl(url, baseUrl?)publicurl.ts在无法解析时返回undefined而非抛异常。五、控制流与错误处理Result、assert、retry、ExecutionQueue5.1 Result 判别联合tldraw 在内部大量使用不抛异常的结果类型。ResultT, E是OkResultT | ErrorResultE的判别联合ok字段作为判别符function divide(a: number, b: number): Resultnumber, string { if (b 0) return Result.err(Division by zero) return Result.ok(a / b) } const result divide(10, 2) if (result.ok) { console.log(Result: ${result.value}) // Result: 5 } else { console.error(Error: ${result.error}) }Result常量对象control.ts提供ok(value)、err(error)两个工厂以及实用的all(results)对结果数组做全成功聚合——全部ok时返回所有值的数组只要有一个失败就返回第一个错误类似于Promise.all的同步版本。5.2 assert 与 assertExists两个断言函数internal都经过omitFromStackTrace包装抛错时不污染调用栈便于调试定位真正的问题来源assert(value, message?)TypeScript 断言函数asserts valuevalue 为 falsy 时抛Error(message || Assertion Error)assertExists(value, message?)value 为null/undefined时抛错否则返回去掉空值后的值NonNullableT常用于document.getElementById之类可能返回 null 的取值场景。5.3 retry 与 sleepretry(fn, options?)internalretry.ts为异步操作提供可配置重试const data await retry( async () unreliableApiCall(), { attempts: 5, // 默认 3 waitDuration: 2000, // 每次失败后的等待毫秒数默认 1000 matchError: (error) error instanceof NetworkError, // 只有匹配的错误才重试 abortSignal, // 支持 AbortController 中途取消 } )回调会收到{ attempt, remaining, total }三个计数参数从 0 开始计数。注意matchError不匹配的错误会被直接抛出而不重试abortSignal已中断时会抛出new Error(aborted)。sleep(ms)control.ts是setTimeout的 Promise 封装常与 retry、限流配合使用。5.4 ExecutionQueue 与 promiseWithResolveExecutionQueue(timeout?)internalExecutionQueue.ts一个串行任务队列push(task)返回 Promise保证任务按提交顺序逐个执行前一个完成后才启动下一个可设置超时close()后不再接受新任务isEmpty()查询队列状态。适合需要严格串行的场景如顺序处理同步消息promiseWithResolveT()internalcontrol.ts返回一个额外挂载了resolve/reject方法的 Promise允许在异步流程之外控制其状态如上面 MediaHelpers 取视频帧的做法。六、缓存设施WeakCache 与 LruCache两个缓存类一弱一强覆盖不同场景WeakCacheK extends object, Vpubliccache.ts基于WeakMap的微缓存。键必须是对象值随键的 GC 自动回收无需手动清缓存。核心方法get(item, cb)采用懒计算 记忆化模式——命中直接返回未命中则调用cb(item)计算并存储const cache new WeakCacheHTMLElement, DOMRect() const rect1 cache.get(element, (el) el.getBoundingClientRect()) const rect2 cache.get(element, (el) el.getBoundingClientRect()) // rect1 rect2第二次调用不再触发重排计算LruCacheK, V(maxSize)publicLruCache.ts基于Map插入序迭代实现的简单 LRU最近最少使用缓存。get命中时先delete再set把条目移到最新位置set超过maxSize时淘汰map.keys().next().value即最旧条目。提供get/has/set/size无手动清空方法靠容量上限自动淘汰。配套测试见 LruCache.test.ts。七、时间与帧率控制Timers、FpsScheduler、debounce、throttle7.1 Timers带上下文的定时器管理器Timerspublictimers.ts解决 React 组件/编辑器实例中定时器散落、清理困难的痛点所有setTimeout/setInterval/requestAnimationFrame都按contextId分组登记可一键清理const timers new Timers() timers.setTimeout(autosave, () save(), 5000) timers.setInterval(refresh, () updateData(), 1000) timers.requestAnimationFrame(render, () draw()) // 清理 autosave 上下文的所有定时器 timers.dispose(autosave) // 或拿到绑定好上下文的函数对象少传一个参数 const uiTimers timers.forContext(ui) uiTimers.setTimeout(() console.log(timeout), 1000) uiTimers.dispose()实现要点三个内部Mapstring, number[]记录每个上下文注册的句柄dispose(contextId)遍历清空对应上下文disposeAll()遍历所有上下文构造函数中对自身方法做了bind保证方法作为回调传递时this不丢失。注意该类依赖浏览器window属于 DOM 环境工具。7.2 FpsScheduler 与帧节流FpsScheduler(targetFps?)public按目标帧率调度回调。fpsThrottle(fn)返回节流后的函数保留原函数的cancelthrottleToNextFrame(fn)把调用合并到下一帧updateTargetFps(n)可动态调整目标帧率——适合渲染循环、取色器实时预览等需要控制频率的场景fpsThrottle(fn)与throttleToNextFrame(fn)internal版throttle.ts是同样的独立函数形态。7.3 debounce 与 throttledebounce(callback, wait)publicdebounce.ts返回带cancel()的防抖函数返回值为PromiseUwait 毫秒内多次调用只执行最后一次throttle直接转发自lodash.throttleindex.ts 中export { default as throttle } from lodash.throttle。八、哈希、ID 与字符串工具8.1 FNV-1a 风格哈希hash.ts 提供三个确定性哈希函数相同输入必得相同输出32 位有符号整数转字符串const hash getHashForString(hello world) console.log(hash) // -862545276 const hash1 getHashForObject({ name: John, age: 30 }) const hash2 getHashForObject({ name: John, age: 30 }) console.log(hash1 hash2) // true const fileHash getHashForBuffer(await file.arrayBuffer())getHashForString对字符串逐字符执行hash (hash 5) - hash charCodeAt(i)即经典的 djb2 变体每步hash | 0强制转为 32 位整数getHashForObject先JSON.stringify再哈希——哈希结果依赖键的序列化顺序等价键不同顺序会产生不同哈希getHashForBuffer用DataView.getUint8逐字节处理二进制数据可用于为图片等文件内容生成一致标识。lns(str)是一个自定义的字符串变换/混淆函数把字符串按 1/5、1/4、1/3、1/2 的比例分段搬移、反转并对数字字符做绕 5 翻转1↔6、2↔7……5 保持。它是确定性编码并非加密算法官方注释称之为custom encoding/obfuscation。8.2 uniqueId 与字符串工具uniqueId(size?)publicid.ts生成唯一 ID默认长度对应约 128 位随机数mockUniqueId(fn)/restoreUniqueId()internal用于测试中替换为确定性生成器相关测试见 id.test.tsgetFirstCharacter(str)、iterateGraphemes(str)publicstring.ts后者返回 Unicode 字素簇grapheme迭代器正确处理 emoji 等组合字符不会按 UTF-16 码元切断——这正是 tldraw 文本工具正确统计光标位置的基础。九、类型工具与数学函数9.1 类型体操全家桶API 报告中的类型导出全部public为 tldraw 自身的泛型 API 提供支撑也可独立复用AwaitableTPromiseLikeT | T表示值或值的 PromiseExpandT把交叉/映射类型展开为可读的普通对象类型RecursivePartialT递归可选化RequiredT内部实现为Required_2ExpandOmitT, K { [P in K]-?: T[P] }强制必选MakeUndefinedOptionalT按undefined extends T[K]条件拆键自动把可含 undefined的属性转为可选JSON 类型体系JsonPrimitiveboolean | null | number | string、JsonArray、JsonObject、JsonValue递归定义——是 tldraw 文档/存储序列化层的统一类型基石定义见 json-value.ts。9.2 数学与随机number.ts 提供lerp(a, b, t)线性插值invLerp(a, b, t)反插值把 t 映射回 [0,1] 区间比例modulate(value, rangeA, rangeB, clamp?)把一个数值从 rangeA 区间映射到 rangeB 区间可选钳制rng(seed?)可种子随机数生成器——相同 seed 产生相同序列对可复现的测试与确定性模拟至关重要。十、错误标注、对象工具与杂项错误标注体系public类型 internal函数error.tsErrorAnnotations含tags: Recordstring, bigint | boolean | null | number | string | symbol | undefined与extras: Recordstring, unknownannotateError(error, annotations)把结构化标注挂到错误上getErrorAnnotations(error)读回。这是 tldraw 上报 Sentry 等监控系统时携带上下文信息的机制对象工具internalobject.tsgroupBy、omit、hasOwnProperty、getOwnProperty、filterEntries、mapObjectMapValues、objectMapKeys/Values/Entries/FromEntries含可迭代版本、getChangedKeys返回两对象间变化的键数组、areObjectsShallowEqual、isEqualAllowingForFloatingPointErrors带容差阈值数组侧还有areArraysShallowEqual其他stringEnum(...values)internal生成{K: K}结构的字符串枚举映射、getFirstFromIterable取 Map/Set 首项、bind属性/类方法装饰器双形态的方法绑定、noop、omitFromStackTrace、warnOnce/warnDeprecatedGetter去重告警、性能探针measureDuration/measureAverageDuration/measureCbDurationinternalperf.ts与PerformanceTrackerpublicPerformanceTracker.ts提供start(name)/stop()/recordFrame()/isStarted()的帧率统计、STRUCTURED_CLONE_OBJECT_PROTOTYPE、isNativeStructuredClone与转发的structuredClonevalue.ts、isDefined/isNonNull/isNonNullish窄化工具。十一、测试保障每个工具都有对应测试tldraw/utils的可靠性建立在覆盖完备的单测之上源码目录中几乎每个模块都配有一个*.test.tssrc/libfractionalIndexing.test.ts、reordering.test.ts排序 key 生成与校验、LruCache.test.ts、PerformanceTracker.test.ts、ExecutionQueue.test.ts、debounce.test.ts、throttle.test.ts、retry.test.ts、hash.test.ts、timers.test.ts、file.test.ts、url.test.ts、storage.test.ts、version.test.ts、warn.test.ts、array.test.ts、object.test.ts、string.test.ts、id.test.ts、value.test.ts、control.test.ts、bind.test.ts、iterable.test.ts、sort.test.ts、number.test.ts等以及 lib/media 下的apng.test.ts、avif.test.ts、gif.test.ts、webp.test.ts、media.test.ts五个媒体格式解析测试。运行方式为cd packages/utils yarn test # vitest 监视模式--passWithNoTests yarn test-ci # 一次性运行这解释了前文提到的设计取舍reordering.ts通过NODE_ENV test切换无抖动实现正是为了让这些测试的断言完全确定。十二、结语如何复用这套工具tldraw/utils是一个小而全的通用工具库——虽然官方定位为 SDK 内部私有包但其public导出索引键、媒体常量与 MediaHelpers、FileHelpers、Result、WeakCache/LruCache、Timers、FpsScheduler、哈希、类型工具、数学函数等本身就是一套经过真实画布产品打磨的工程基础设施其设计可以直接借鉴到任何需要可插入排序键内存缓存上下文化定时器或类型安全错误处理的 React 应用中。理解这份包的最佳路径是以 api-report.api.md 为索引对照 src/index.ts 看导出组织再深入 src/lib 逐个模块阅读实现与测试——从 fractionalIndexing.ts 的 JITTER 优化、到 media.ts 的 PNG DPI 解析每一处注释都记录了真实产品场景下的性能与兼容性取舍这正是这套工具库最有价值的部分。【免费下载链接】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),仅供参考