如何用 onChange 回调把 Excalidraw 场景数据持久化到后端或本地存储?
把 @excalidraw/excalidraw 嵌入自己的应用后,画布上的元素只存在于组件内部状态中,刷新页面就会丢失。Props 文档给出的官方路径是:给 Excalidraw 组件传一个 onChange 回调,在组件因任何变化而更新时拿到场景的 elements、appState 和 files,由你在回调内把它们保存到自己的后端或本地存储,加载时再用 initialData 把场景还原回去。
准备条件
按 安装文档 安装包:
npm install react react-dom @excalidraw/excalidraw
# 或
yarn add react react-dom @excalidraw/excalidraw
两个会影响渲染的前提:
- Excalidraw 会占满父容器的 100% 宽度和高度,渲染它的容器必须是非零尺寸,文档中的示例容器统一给了固定高度(如
style={{ height: "500px" }})。 - 集成文档说明 Excalidraw 不支持服务端渲染。如果你用 Next.js,需要用动态导入并关闭 SSR:
import dynamic from "next/dynamic";
import "@excalidraw/excalidraw/index.css";
const Excalidraw = dynamic(
async () => (await import("@excalidraw/excalidraw")).Excalidraw,
{
ssr: false,
},
);
onChange 回调的三个参数
Props 文档定义了 onChange 的签名和触发时机:
(excalidrawElements, appState, files) => void;
excalidrawElements:场景中所有 Excalidraw 元素数组;appState:场景当前的AppState;files:添加到场景中的BinaryFiles。
文档明确说明该回调在组件因任何变化而更新时触发,并原话建议:“Here you can try saving the data to your backend or local storage for example.”(你可以在这里尝试把数据保存到后端或本地存储。)也就是说,持久化逻辑完全由宿主应用接管,包本身不提供存储实现。
接入 onChange 并验证回调触发
仓库内的示例应用 ExampleApp.tsx 展示了官方示例的接法,它把回调里收到的数据打印到控制台:
onChange: (
elements: NonDeletedExcalidrawElement[],
state: AppState,
) => {
console.info("Elements :", elements, "State : ", state);
},
把这段模式放进你的组件,然后在画布上添加、移动、删除元素,观察浏览器控制台是否随每次变化打印出最新的 elements 与 state——这是示例仓库用来确认回调正常触发的方式。确认回调按预期触发后,再替换为真正的持久化逻辑。
把场景数据写入存储目标
文档只约定了回调会拿到哪三份数据,具体写入方式由你的存储目标决定。以下是在回调内做本地持久化的最小示意(localStorage 的 key 可自行替换):
<Excalidraw
onChange={(elements, appState, files) => {
const scene = { elements, appState, files };
localStorage.setItem("excalidraw-scene", JSON.stringify(scene));
}}
/>
如果是保存到后端,同样是把这三个参数序列化为请求体发出,例如按你的接口约定发送 { elements, appState, files }。注意 files 不要省略:场景里的图片等二进制文件同样需要持久化,否则下次加载时元素会引用不到文件数据。
替代路径:通过 excalidrawAPI 订阅变更
excalidrawAPI 文档提供了等价于 props.onChange 的订阅方式,适合回调需要在组件挂载之后动态注册/注销的场景:
(
callback: (
elements: readonly ExcalidrawElement[],
appState: AppState,
files: BinaryFiles,
) => void
) => () => void
它返回一个 unsubscribe 函数,用于取消订阅。使用时先通过 excalidrawAPI prop 把 API 存到 state,再调用 api.onChange(callback):
export default function App() {
const [excalidrawAPI, setExcalidrawAPI] = useState(null);
useEffect(() => {
if (!excalidrawAPI) {
return;
}
const unsubscribe = excalidrawAPI.onChange(
(elements, appState, files) => {
console.info("Elements :", elements, "State : ", appState);
}
);
return unsubscribe;
}, [excalidrawAPI]);
return (
<div style={{ height: "500px" }}>
<Excalidraw excalidrawAPI={(api) => setExcalidrawAPI(api)} />
</div>
);
}
下次加载时还原场景
持久化只是半边工作,initialData prop 负责把存下的数据加载回画布。initialData 文档支持 elements、appState、files 三个字段,可选对象或一个解析为该对象的 Promise。示意(savedScene 是从存储读出的 { elements, appState, files } 对象):
<Excalidraw
initialData={{
elements: savedScene.elements,
appState: savedScene.appState,
files: savedScene.files,
}}
/>
如果存下的数据来自外部导入(缺字段),restore 工具文档提供了 restoreElements / restoreAppState / restore,它们会为缺失的属性补上默认值,保证元素和状态完整:
import { restoreElements } from "@excalidraw/excalidraw";
仓库示例 ExampleApp.tsx 中 updateScene 的用法演示了这条链路:先 restoreElements(convertToExcalidrawElements([...]), null) 规整元素,再连同 appState 一起传给 excalidrawAPI.updateScene(sceneData) 更新场景。
加载后,在画布上确认之前保存的元素、背景和文件都已出现,说明还原成功;再改动一次画布,确认 onChange 携带的新数据已写入存储目标,整条保存—加载闭环就跑通了。
边界与注意事项
getFiles/ 回调中的files可能包含没有被任何元素引用的文件。API 文档建议:如果你要把文件持久化到存储,应当与已存储的元素做比对后再决定保存哪些文件。initialData文档提示:如果不使用scrollToContent(默认不滚动),需要同时传入initialData.appState.scrollX和initialData.appState.scrollY才能保留滚动位置。- 若需要把持久化数据同时用于协作、在紧循环里频繁恢复元素,
restoreElements的refreshDimensions选项(默认false)可用于关闭文本尺寸重算这类较昂贵的操作,具体见 restore.mdx。 - v0.17.0 起
Ref支持已被移除,接入统一使用excalidrawAPI;ready/readyPromise也已停用(见 excalidrawAPI 文档)。
文档未给出存储节流、增量保存等策略;onChange 在组件每次因变化更新时都会触发,保存频率和后端接口设计需要按你的应用自行处理,本文不预设这些方案。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0629
MiniCPM5-2BMiniCPM5-2B 是一款面向端侧、本地部署和资源受限场景的 2B 稠密 Transformer,能够达到同尺寸开源模型 SOTA 水平。Markdown00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
HivisionIDPhotos⚡️HivisionIDPhotos: a lightweight and efficient AI ID photos tools. 一个轻量级的AI证件照制作算法。Python07
DragonOSDragonOS is an operating system developed from scratch using Rust, with Linux compatibility. It is designed for **Serverless** scenarios. 使用Rust从0自研内核,具有Linux兼容性的操作系统,面向云计算Serverless场景而设计。Rust00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00