tldraw 能快速给 React 应用增加无限画布,但把 <Tldraw /> 放进 Next.js 页面,只完成了不到百分之五。真正的产品封装从加载、资源、持久化、滚动归属、响应式控制、全屏和授权部署开始。
本文拆解一套真实的浏览器本地封装:白板数据保存在用户浏览器中,编辑器字体与图标由站点自行托管,支持中英文界面,宽屏使用纵向工具栏,并且在普通页面中优先让网页滚动,进入全屏后再把滚轮交回画布。
先划清 tldraw 与产品外壳的职责
把 tldraw 当作产品外壳里的编辑器运行时。页面布局、加载与重试、隐私提示、全屏、统计边界和导航属于应用;图形、选择、相机、撤销历史、页面和原生导出属于 tldraw。
边界明确后,就不会一边重复实现库已经做好的编辑能力,一边忽略画布周围真正影响用户体验的产品行为。
| 能力 | 负责方 |
|---|---|
| 图形、工具、选择、多页面 | tldraw |
| 路由、标题、帮助文案、异常状态 | 应用外壳 |
| 文档持久化 | persistenceKey 或自定义 store |
| 协作与身份认证 | 业务后端和同步架构 |
| 静态资源与 CSP | 应用部署层 |
只在浏览器加载 tldraw
画布编辑器依赖浏览器 API,而且 JavaScript 体积不小。把封装组件标记为 client component,在该工具的 route layout 引入 tldraw/tldraw.css,等页面 hydration 后再动态 import 运行时。保存组件函数时要使用 setState(() => Component),否则 React 可能把组件函数当作状态更新器执行。
effect 内还要记录组件是否仍然挂载,避免慢速加载完成后更新已经卸载的页面。加载失败时可以递增加载次数,让用户点击按钮重新请求 chunk。
// Client component: load the editor only after hydration.
useEffect(() => {
let active = true;
import('tldraw').then(({ Tldraw, DefaultToolbar }) => {
if (!active) return;
setEditor(() => Tldraw);
setToolbar(() => DefaultToolbar);
});
return () => { active = false; };
}, []);
return Editor ? (
<Editor
assetUrls={assetUrls}
locale={locale === 'zh' ? 'zh-cn' : 'en'}
persistenceKey="my-whiteboard"
cameraOptions={{ wheelBehavior: fullscreen ? 'pan' : 'none' }}
/>
) : <LoadingState />;把编辑器运行时资源全部本地化
白板可能在开发环境一切正常,上线后却因为严格 CSP、离线部署或外部资源不可用而丢失字体、翻译、图标和嵌入缩略图。通过 assetUrls 把运行时资源统一映射到同源的版本化目录。
资源映射应集中在一个工具模块里。升级 tldraw 时,对比新版资源清单与本地文件,把缺失资源变成构建期错误,而不是等生产工具栏出现空白才发现。
const base = '/tldraw-assets';
export const assetUrls = {
icons: { 'align-left': `${base}/icons/align-left.svg` },
fonts: { tldraw_sans: `${base}/fonts/IBMPlexSans-Medium.woff2` },
translations: { 'zh-cn': `${base}/translations/zh-cn.json` },
embedIcons: { youtube: `${base}/embed-icons/youtube.png` },
};- 复制图标雪碧图或实际需要的独立图标。
- 包含文字工具会用到的全部字体粗细和斜体。
- 准备语言切换器可能选择的 locale JSON。
- 开放嵌入能力时,同时托管 embed icons。
用 persistenceKey 实现单用户本地白板
稳定的 persistenceKey 让编辑器在刷新后从浏览器本地恢复白板,很适合无需账号的单用户工具,画布内容也不必提交到应用 API。key 要带产品前缀,避免同一域名下的多个白板互相覆盖。
本地持久化不是云端备份。清除站点数据、切换浏览器配置或使用无痕模式,都可能让文档消失。如果产品承诺账号同步、多人协作、历史版本或跨设备访问,就应把它们设计成明确的后端能力,不能暗示 persistenceKey 已经做到。
- 说明本地保存了什么,以及用户如何清除。
- 不要在客户端可见的 key 里放用户密钥。
- 提供导出能力,让用户可以自行备份。
- 升级库导致数据结构变化时,准备版本标识或迁移策略。
解决画布滚轮与网页滚动冲突
无限画布天然希望独占滚轮做平移和缩放,但嵌在长页面中间的白板不应该截断网页纵向滚动。一种实用策略是:普通嵌入状态把 wheelBehavior 设为 none,把没有修饰键的滚轮位移转发给 window.scrollBy;进入全屏后再把滚轮恢复为画布 pan。
Ctrl 或 Command 加滚轮通常用于编辑器缩放,应保留给画布;零位移事件无需转发。除普通鼠标外还要测试触控板,因为横向位移和惯性滚动差异很大。
通过 components 定制工具栏
components prop 允许应用替换部分编辑器 UI,而不需要 fork tldraw。对于纵向空间充足的工作区,可以用 DefaultToolbar 包一层 vertical orientation,把常用工具放在左侧,同时释放顶部横向空间,再用少量 CSS 把它定位在画布框内。
优先组合默认组件,不要复制内部工具栏实现。默认组件会继承库里的快捷键、工具状态与可访问性行为,复制版本在升级后很容易漂移。
- 按真实画布高度设置最小和最大可见工具数。
- 保证触摸和键盘用户仍能访问工具说明。
- 先使用编辑器提供的组件 API,再考虑覆盖内部 class。
- CSS 覆盖尽量只处理定位和产品语义色。
实现有降级能力的全屏
原生 requestFullscreen 隔离效果最好,但浏览器、iframe 或 Permissions Policy 都可能拒绝。捕获异常后,可以把容器切换为 fixed inset-0 作为 CSS 全屏降级;降级状态下锁住 body 滚动,并监听 Escape 退出。
无论原生全屏还是 CSS 降级,布局稳定后都应触发一次 resize。画布编辑器会缓存视口尺寸,如果仍使用旧尺寸,工具栏可能被裁掉,相机中心也会偏移。
上线前处理异常、授权与升级
chunk 下载过程中显示真实 loading surface,import 失败后展示可理解的错误和重试按钮。根据所用版本确认 tldraw 的授权要求,并按官方部署规则配置客户端 license key。浏览器下发的 key 不是秘密,真正约束来自域名限制和许可证条款。
固定依赖版本,升级前回归本地持久化和导出,并在 Network 面板检查是否仍有意外外部资源。如果未来加入协作,应把身份认证、文档访问控制、在线状态、冲突解决与保存周期当作独立架构项目。
总结
生产级 tldraw 封装的重点是集成工程:只在客户端加载、确定性托管资源、明确本地持久化语义、设计滚轮归属、通过组件 API 定制,以及可靠处理视口变化。应用外壳做好这些边界,图形编辑能力则交给编辑器运行时。