首页
/ 拆解 tldraw 的 Tldraw 组件:用 TldrawEditor、TldrawUi 与默认构件手工组装无限画布编辑器

拆解 tldraw 的 Tldraw 组件:用 TldrawEditor、TldrawUi 与默认构件手工组装无限画布编辑器

2026-09-07 10:20:46作者:胡唯隽

在 tldraw 的 React SDK 中,日常使用的 <Tldraw /> 其实只是一个"便利封装",它的下层由三部分构成:不带任何形状、工具与 UI 的 TldrawEditor(画布与编辑器核心)、负责默认菜单/工具栏/面板的 TldrawUi,以及一组可整体替换的默认值(defaultShapeUtilsdefaultBindingUtilsdefaultToolsdefaultShapeTools 等)。本文以官方示例 exploded 示例 为主线,结合 tldraw 包的真实源码,讲解如何把 <Tldraw /> 拆开再亲手装回去,掌握对编辑器的每一个零件进行增删、替换或彻底移除的能力——从只想去掉某个工具/形状,到彻底自绘整套 UI,都能在这一套"拆解思路"下完成。


一、先认清三个层次:TldrawTldrawEditorTldrawUi

<Tldraw />Tldraw.tsx)在官方文档与源码注释中被直接描述为 "convenience wrapper"(便利封装)。把它拆开,是三个可独立理解、独立使用的层次:

层次 职责 是否自带形状/工具/UI
TldrawEditor 画布与编辑器运行时核心 否:没有形状、工具,也没有任何 UI
TldrawUi 默认的菜单、工具栏、面板等界面 否:它只渲染界面壳,不注册形状与工具
一组 defaults defaultShapeUtils 等,即"编辑器开箱即用的内容" 是:全部默认形状与工具都从这里注入

注意,TldrawEditor 实际由 @tldraw/editor 包导出(即仓库 packages/editor 目录下的编辑器内核),而 TldrawUi、各类 default* 常量则由 tldraw 包提供。<Tldraw /> 的职责就是:把这些零散零件拼装起来、补齐默认值、处理挂载副作用,并对外暴露一套"傻瓜式" props。

这解释了官方示例 README 里的那句关键描述:Tldraw 组件底下是 TldrawEditor(一个没有任何形状、工具、UI 的画布与编辑器)、TldrawUi(默认菜单、工具栏与面板),以及一组 defaults。用官方示例 ExplodedExample.tsx 的话说:示例用这些零件组装出了和普通 <Tldraw /> 行为一致的编辑器,但每个部分都变得可见、可替换——从列表里删掉一个 shape util、传入另一套 tools,甚至不渲染 TldrawUi 而换成你自己的界面,都是可行的。


二、源码视角:<Tldraw /> 内部到底替你做了什么

拆解之前,先看清被拆的那个"整体"。阅读 Tldraw.tsxTldraw 函数,可以梳理出它替用户完成的全部装配工作,这也正是 exploded 示例需要逐一手动补回的内容。

1. 合并默认形状、绑定、资源、Overlay 与工具

<Tldraw /> 接受用户传入(通常为空数组)的 shapeUtilsbindingUtilsassetUtilsoverlayUtilstools,然后通过 mergeArraysAndReplaceDefaults 与默认值合并:

  • shapeUtilsWithDefaults:用户传入的 shape util 与 defaultShapeUtilstype 字段合并(同名替换,新名追加);
  • bindingUtilsWithDefaults:与 defaultBindingUtilstype 合并;
  • assetUtilsWithDefaults:与 defaultAssetUtils 合并,并且若用户提供了 maxImageDimensionacceptedImageMimeTypesacceptedVideoMimeTypes,还会通过 configureDefaultAssetUtils 内部调用 ImageAssetUtil.configure / VideoAssetUtil.configure 重新配置图片与视频资源工具;
  • overlayUtilsWithDefaults:与 defaultOverlayUtils 合并;
  • toolsWithDefaults:与 allDefaultTools = [...defaultTools, ...defaultShapeTools]id 合并。

也就是说,即使你只写 <Tldraw />,它也会在渲染时自动带上全部默认工具与形状。而这些默认集合在源码中都是明文可见、可直接导入的常量文件:

  • defaultShapeUtils.tsTextShapeUtilBookmarkShapeUtilDrawShapeUtilGeoShapeUtilNoteShapeUtilLineShapeUtilFrameShapeUtilArrowShapeUtilHighlightShapeUtilEmbedShapeUtilImageShapeUtilVideoShapeUtil,共 12 个形状;
  • defaultShapeTools.tsTextShapeToolDrawShapeToolGeoShapeToolNoteShapeToolLineShapeToolFrameShapeToolArrowShapeToolHighlightShapeTool,共 8 个与形状配套的工具;
  • defaultTools.tsEraserToolHandToolLaserToolZoomToolSelectTool,共 5 个通用工具;
  • defaultBindingUtils.tsdefaultAssetUtils.tsdefaultOverlayUtils.ts:绑定、资源、overlay 三类默认工具的对应集合。

2. 补齐富文本与字体默认配置

<Tldraw /> 还会把 options.text 展开成完整的 TLTextOptions:默认提供 addFontsFromNode: defaultAddFontsFromNode(从 DOM 节点收集字体),并把 tipTapConfig.extensions 默认填充为 tipTapDefaultExtensions,用户传入的配置在展开后被合并进来(Tldraw.tsx 第 242–259 行)。exploded 示例中传回的 options 正是这一结构。

3. 提供资源 URL、翻译与字体的 Provider 层级

<Tldraw /> 返回的 JSX 最外层依次包裹了 AssetUrlsProviderassetUrls 会被 useDefaultUiAssetUrlsWithOverrides 处理)与 TldrawUiTranslationProvider(locale、翻译 overrides 都在这层生效),其内部才渲染 TldrawEditor。原因在源码注释里写得很直白:这样连渲染在 TldrawUi 之外的加载 UI(LoadingScreenSpinner)也能被翻译、能拿到资源 URL。

4. 在挂载时注册副作用与外部内容处理器

最后,Tldraw 还嵌套了一个内部组件 InsideOfEditorAndUiContextTldraw.tsx 第 313–385 行),它在编辑器 onMount 时依次做三件事:

  1. registerDefaultSideEffects(editor)——注册默认的 store 副作用;
  2. registerDefaultExternalContentHandlers(editor, {...})——注册拖放/粘贴文件、URL、embed、SVG 文本等外部内容处理器;
  3. 执行 editor.store.props.onMount(editor) 与用户的 onMount prop。

此外它还会预加载 allDefaultFontFaces 与主题自定义字体,并监听首次编辑事件上报埋点。注意这些默认副作用与外部内容处理器都是 TldrawEditor 自身不会做的,这正是 exploded 示例必须自己补两段代码的根本原因。


三、动手拆装:ExplodedExample 的完整组装代码

官方示例 ExplodedExample.tsx 是"拆解"思想的最小完整实现。整体逻辑分为三个部分:在模块顶层汇总默认工具、组装组件树、在上下文内部补齐副作用。

步骤 1:汇总工具与默认选项

const allDefaultTools = [...defaultTools, ...defaultShapeTools]
const defaultOptions = {
	text: {
		tipTapConfig: {
			extensions: tipTapDefaultExtensions,
		},
		addFontsFromNode: defaultAddFontsFromNode,
	},
}

这一行的 allDefaultToolsTldraw.tsx 第 118 行的内部常量完全一致(选择类/缩放类工具在前,各形状工具在后);defaultOptions 则对应 Tldraw 内部对 TLTextOptions 的默认填充。手动组装时,这两个值是绕不开的"默认内容清单"。

步骤 2:渲染 TldrawEditor + TldrawUi 组件树

export default function ExplodedExample() {
	return (
		<div className="tldraw__editor">
			<TldrawEditor
				initialState="select"
				shapeUtils={defaultShapeUtils}
				bindingUtils={defaultBindingUtils}
				assetUtils={defaultAssetUtils}
				overlayUtils={defaultOverlayUtils}
				tools={allDefaultTools}
				persistenceKey="exploded-example"
				options={defaultOptions}
				assetUrls={defaultEditorAssetUrls}
			>
				<TldrawUi>
					<InsideEditorAndUiContext />
				</TldrawUi>
			</TldrawEditor>
		</div>
	)
}

这段 JSX 里 TldrawEditor 的各个 prop 意义如下:

Prop 传入值 作用
initialState "select" 编辑器启动后进入的根工具状态,保持与 <Tldraw /> 一致
shapeUtils defaultShapeUtils 12 个默认形状实现,决定画布上能创建/渲染哪些形状
bindingUtils defaultBindingUtils 绑定工具(如箭头与形状之间的吸附绑定关系)
assetUtils defaultAssetUtils 图片与视频等资源的上传/解析工具
overlayUtils defaultOverlayUtils Overlay(悬停高亮等覆盖层)工具
tools allDefaultTools 13 个默认工具(5 通用 + 8 形状配套)
persistenceKey "exploded-example" 本地持久化的存储键;同名实例共享一份 localStorage 数据
options defaultOptions 富文本/字体等编辑器选项
assetUrls defaultEditorAssetUrls 图标、字体、贴纸等默认静态资源的 URL 表

替换的入口就在这里:把 shapeUtils 换成你自己的 ShapeUtil 数组即可整体替换形状集;在 tools 数组里删掉某个工具,工具栏就不会出现它;传入 persistenceKey 就能让每次刷新保留画布内容。

步骤 3:在 UI 上下文内补齐挂载副作用

function InsideEditorAndUiContext() {
	const editor = useEditor()
	const toasts = useToasts()
	const msg = useTranslation()

	useEffect(() => {
		registerDefaultExternalContentHandlers(editor, {
			maxImageDimension: 5000,
			maxAssetSize: 10 * 1024 * 1024, // 10mb
			acceptedImageMimeTypes: DEFAULT_SUPPORTED_IMAGE_TYPES,
			acceptedVideoMimeTypes: DEFAULT_SUPPORT_VIDEO_TYPES,
			toasts,
			msg,
		})

		const cleanupSideEffects = registerDefaultSideEffects(editor)

		return () => {
			cleanupSideEffects()
		}
	}, [editor, msg, toasts])

	return (
		<ContextMenu>
			<DefaultContextMenuContent />
		</ContextMenu>
	)
}

为什么这段代码必须嵌在 TldrawUi 内部、且由独立的子组件渲染?因为 useToasts()useTranslation() 都要依赖 TldrawUi 提供的上下文——这正是 <Tldraw /> 把内部逻辑放进 InsideOfEditorAndUiContext 的同一个原因。

这里涉及的两个注册函数分别是"画布内容交互"与"文档数据一致性"的关键:

  • registerDefaultExternalContentHandlers:处理用户拖入/粘贴的文件、URL、embed、SVG 文本等内容。其可选参数含义为:maxImageDimension(图片最大边长,示例取 5000)、maxAssetSize(单资源大小上限,示例为 10MB)、acceptedImageMimeTypes / acceptedVideoMimeTypes(允许的图片/视频 MIME 类型,默认取 DEFAULT_SUPPORTED_IMAGE_TYPESDEFAULT_SUPPORT_VIDEO_TYPES,二者均从 tldraw 顶层导出)。
  • registerDefaultSideEffects:注册默认的 store 副作用,例如在本地用户创建 embed 形状或修改其 URL 时解析真实宽高比,以及在裁剪/编辑状态切换、相机停止时维护悬停形状等(详见 defaultSideEffects.ts)。它返回清理函数,示例在 useEffect 的 cleanup 中调用 cleanupSideEffects() 以便组件卸载时注销副作用。

ContextMenu + DefaultContextMenuContent 则提供了右键菜单,它是 TldrawEditorTldrawUi 之外的"补全件",作为子组件挂载后能包住画布。


四、三种典型改造方向

1. 从默认集合中"减零件"

默认集合都是普通数组,做减法只需过滤:

const myShapeUtils = defaultShapeUtils.filter(
	(util) => util.type !== 'embed' && util.type !== 'bookmark'
)

<TldrawEditor shapeUtils={myShapeUtils} /* ... */ />

去掉 embed 与 bookmark 后,画布上不再存在这两种形状,工具栏中配套工具也不会出现。所有默认工具在默认集中按 id / type 去重或按同键覆盖的合并规则,与 Tldraw.tsx 内部使用的 mergeArraysAndReplaceDefaults 完全一致。

2. 换工具集、换形状实现

因为 defaultShapeUtils 等只是 ShapeUtil/Tool 的,你可以为某个 type 传入自己的实现类——同名 type 会替换默认项,这就是"自定义形状/工具"的最短路径。官方在 configuration 目录 下提供了配套的进阶示例,例如 only-editor(纯 TldrawEditor + 单一自定义形状与工具)、configure-shape-utilcustom-options(自定义编辑器选项),它们展示的正是"自写形状与工具后如何注入",可作为本文之后继续深挖的路标。

3. 不要 TldrawUi,换成自己的界面

既然 TldrawUi 只是渲染在 TldrawEditor 内部的 UI 壳,最彻底的自定义就是不渲染它,改为基于 useEditor()useEditorComponents() 自行组合画布、工具栏与面板。请记住一条硬约束:只要你的界面还需要 toast、翻译或右键菜单,就必须在对应 Provider/上下文内渲染——这也是官方示例把需要 useToasts()/useTranslation() 的注册逻辑放进 TldrawUi 子组件的原因。另外,TldrawEditor 不会替你做字体预加载与首次编辑事件埋点,这些是 TldrawInsideOfEditorAndUiContext 中顺带完成的(Tldraw.tsx 第 336–349 行);完全绕开 Tldraw 时,若需要这些行为需自行补上。


五、小结:一张"拆解清单"

<Tldraw /> 拆开后需要手动还原的五件事,也是排查"手动组装缺了点什么"的对照表:

  1. 注册形状、绑定、资源、Overlay 与工具——通过 TldrawEditor 的五个 prop 传入(shapeUtils/bindingUtils/assetUtils/overlayUtils/tools);
  2. 传入富文本与字体选项——options.text 里带上 tipTapDefaultExtensionsdefaultAddFontsFromNode
  3. 传入持久化与资源 URL——persistenceKey 决定本地存储复用,assetUrls={defaultEditorAssetUrls} 保证默认图标字体可用;
  4. 注册外部内容处理器——registerDefaultExternalContentHandlers,需要 toasts 与翻译上下文;
  5. 注册默认 store 副作用——registerDefaultSideEffects,负责 embed 宽高比解析等一致性维护。

官方把这一思路凝结在 exploded README 中一句话里:这样组装出的编辑器行为上等同于 <Tldraw />,但每个部分都可见、可替换。理解这条拆装路径之后,无论是裁减开箱功能、注入私有形状,还是从零搭建一套全新 UI,都能精确落在正确的注入点与上下文边界上,而不是对整个黑盒组件做粗略的样式覆盖。

热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
docsdocs
暂无描述
Markdown
900
5.83 K
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.15 K
2.77 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
860
1.36 K
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
929
1.85 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.94 K
1.02 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
534
603
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.37 K
1.46 K
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
548
398
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
1.05 K
528