如何读写 Excalidraw 的 .excalidraw JSON 文件格式(elements / appState / files)?
如果你的应用需要处理 Excalidraw 的场景数据——把用户保存的 .excalidraw 文件(明文 JSON)加载进画布,或者把画布当前内容持久化回 .excalidraw 文件——你需要理解这个文件的三个核心字段:elements、appState 和 files,并知道读写两侧分别调用哪些 API。本文基于 Excalidraw 仓库内的开发者文档,给出一条可执行的路径:安装依赖 → 解析文件 → 用 initialData/restore 写入画布 → 用 excalidrawAPI 读回场景并组装成合规 JSON。
准备条件
按 安装文档,Excalidraw 是作为组件嵌入你的项目,使用 npm 或 yarn 安装:
npm install react react-dom @excalidraw/excalidraw
# or
yarn add react react-dom @excalidraw/excalidraw
另外注意:Excalidraw 会占满容器 width/height 的 100%,所以渲染它的容器必须具有非零尺寸,否则画布不可见,你也会无法目视验证加载结果。
.excalidraw 文件由哪些字段组成
按 JSON Schema 文档,当场景保存到本地文件时,.excalidraw 文件使用如下格式:
| 属性 | 说明 | 值 |
|---|---|---|
type |
Excalidraw schema 的类型 | "excalidraw" |
version |
Excalidraw schema 的版本 | number |
source |
Excalidraw 应用的来源 URL | "https://excalidraw.com" |
elements |
画布上元素的对象数组 | 包含 excalidraw element 对象的数组 |
appState |
附加的应用状态/配置 | 包含应用状态属性的对象 |
files |
image 元素的数据 |
图片数据对象,格式为 { [fileId]: fileData } |
文档给出的 JSON 示例(节选,version: 2 为文档示例值):
{
"type": "excalidraw",
"version": 2,
"source": "https://excalidraw.com",
"elements": [
{
"id": "pologsyG-tAraPgiN9xP9b",
"type": "rectangle",
"x": 928,
"y": 319,
"width": 134,
"height": 90
/* ...other element properties */
}
],
"appState": {
"gridSize": 20,
"viewBackgroundColor": "#ffffff"
},
"files": {
"3cebd7720911620a3938ce77243696149da03861": {
"mimeType": "image/png",
"id": "3cebd7720911620a3938c.77243626149da03861",
"dataURL": "data:image/png;base64,iVBORWOKGgoAAAANSUhEUgA=",
"created": 1690295874454,
"lastRetrieved": 1690295874454
}
}
}
在源码层面,这个结构与 ExportedDataState 类型对应(type / version / source / elements / appState / files),而读入一侧对应 ImportedDataState——后者所有字段可选,因此你解析得到的 JSON 中缺失字段时,需要交给下一节的 restore 补全,而不是自己猜默认值。
读取:把 JSON 写入画布
第一步:用 initialData 挂载场景
<Excalidraw /> 组件提供 initialData prop,用于加载初始场景。它必须是一个对象,或一个解析为对象的 Promise,包含以下可选字段:
| 字段 | 类型 | 说明 |
|---|---|---|
elements |
ExcalidrawElement[] |
Excalidraw 挂载时使用的元素 |
appState |
AppState |
Excalidraw 挂载时使用的 AppState |
scrollToContent |
boolean |
挂载后是否把最近元素滚动到居中;默认不滚动。当 scrollToContent 为 false 时,请同时传入 initialData.appState.scrollX 和 initialData.appState.scrollY,滚动位置才能保留 |
files |
BinaryFiles |
添加到场景的 files |
最小可用的读取示例(来自文档的 live 示例,矩形坐标、颜色等值照抄文档):
function App() {
return (
<div style={{ height: "500px" }}>
<Excalidraw
initialData={{
elements: [
{
type: "rectangle",
version: 141,
versionNonce: 361174001,
isDeleted: false,
id: "oDVXy8D6rom3H1-LLH2-f",
fillStyle: "hachure",
strokeWidth: 1,
strokeStyle: "solid",
roughness: 1,
opacity: 100,
angle: 0,
x: 100.50390625,
y: 93.67578125,
strokeColor: "#000000",
backgroundColor: "transparent",
width: 186.47265625,
height: 141.9765625,
seed: 1968410350,
groupIds: [],
},
],
appState: { zenModeEnabled: true, viewBackgroundColor: "#a5d8ff" },
scrollToContent: true,
}}
/>
</div>
);
}
由于 initialData 支持 Promise 形式,从后端或本地读取 .excalidraw 文件后再解析 JSON、以 Promise 传给 initialData 也是文档允许的形态(文件加载部分由你自己的实现提供,文档未规定具体取数方式)。
第二步:用 restore 补全缺失字段
外部 .excalidraw 文件里的元素可能缺少新增版本引入的属性。Restore 工具文档 提供三个互补的函数,均从 @excalidraw/excalidraw 导入:
import { restore, restoreAppState, restoreElements } from "@excalidraw/excalidraw";
restore(data, localAppState, localElements, opts):确保 elements 和 appState 都有合适的值,缺失时设为默认值,是restoreElements+restoreAppState的组合。localAppState为Partial<AppState>,localElements为ExcalidrawElement[],两者都可传null/undefined。restoreAppState(appState, localAppState):确保appState的所有 key 都有合适 value,缺失的 key 设为默认值。若传入了localAppState,则用它的值替代appState中缺失(undefined)的项,而不是用默认值——文档建议用它来避免覆盖你已持久化的用户默认设置。restoreElements(elements, localElements, opts):确保元素属性完整,缺失属性设为默认值。传入localElements时,已有元素会复用version(并自增)并重新生成versionNonce;文档说明当导入的元素可能已存在于场景中、且你依赖 element version 检测更新时应使用它。
opts 有三个开关,含义都直接来自文档:
| 参数 | 类型 | 文档说明 |
|---|---|---|
refreshDimensions |
boolean |
是否重新计算文本元素尺寸。这是潜在的高开销操作,如果你在协作等紧密循环中恢复元素,可以考虑关闭 |
repairBindings |
boolean |
是否修复元素绑定,确保不存在指向缺失 bound text 的容器、或指向缺失容器的 bound text |
normalizeIndices |
boolean |
是否归一化元素的分数字母索引(fractional indices),防止使用过旧/过长的索引引发问题 |
写回:从画布读回 elements / appState / files
场景的读取通过 excalidrawAPI 完成。按 excalidrawAPI 文档,先通过回调拿到 API 并保存到 state:
export default function App() {
const [excalidrawAPI, setExcalidrawAPI] = useState(null);
return <Excalidraw excalidrawAPI={(api) => setExcalidrawAPI(api)} />;
}
与“写出 .excalidraw JSON”直接相关的三个 API:
getSceneElements():返回场景中所有未删除的元素(NonDeleted<ExcalidrawElement>[]);如需包含已删除元素用getSceneElementsIncludingDeleted()。getAppState():返回当前AppState。getFiles():返回场景中的 files。文档特别提醒:它可能包含没有任何元素引用的文件,所以持久化前应对比已存储的元素——即只保留仍被元素引用的files条目。
三者的结果加上 type / version / source 头部字段,就是上一节定义的 .excalidraw 文件结构,可整体写入文件存储。若需要随场景变化持续同步,可用 excalidrawAPI.onChange(callback) 订阅变更事件,回调签名为 (elements: readonly ExcalidrawElement[], appState: AppState, files: BinaryFiles) => void,返回一个 unsubscribe 函数。
程序化写入场景时的撤销行为
如果你不是通过 initialData 挂载,而是在运行时用 excalidrawAPI.updateScene(sceneData) 更新场景(sceneData 含 elements、appState、captureUpdate 等字段),用 captureUpdate 控制撤销/重入栈行为:
| 值 | 文档说明 |
|---|---|
CaptureUpdateAction.IMMEDIATELY |
应被捕获的更新,大多数本地更新使用该值,会立即进入本地 undo/redo 栈 |
CaptureUpdateAction.EVENTUALLY |
不应立即捕获的更新,适用于异步多步过程中的中间状态 |
CaptureUpdateAction.NEVER |
永不应记录的更新,如远程更新或场景初始化 |
文档同时注明:对 collaborators 或未观察(非 ObservedAppState)的 AppState 部分的更新永远不会进入 undo/redo 栈,与传入的 captureUpdate 无关。
验证与边界
写回后的验证标准来自格式定义本身:解析你持久化的 JSON,确认顶层含 type: "excalidraw"、数字 version、source,且 elements 为数组、appState 为对象、files 为 { [fileId]: fileData } 结构(见 JSON Schema 文档)。加载侧的验证则是挂载后画布上出现 initialData.elements 中的元素(上面 live 示例即演示了这一效果)。
两个容易踩的边界:
- 剪贴板格式 ≠ 文件格式。复制选区到剪贴板时 JSON 结构类似,但
type是"excalidraw/clipboard",且属性不同(无source,元素数组与files语义类似)。如果你的应用要接受剪贴板粘贴的数据,不能直接按.excalidraw文件的字段处理。 - 滚动位置保留:如前述,
scrollToContent: false时必须同时提供appState.scrollX/appState.scrollY,否则滚动位置不会被保留。
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证件照制作算法。Python08
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