保存就是 engine.toDocument()。Fabric.js 加载 JSON 就是 engine.loadDocument(json)。两者都不需要存储,所以 JSON 放在哪里都可以。
正在加载在线示例,代码在下方。
"use client";
import type { FabricDocument } from "fabricjs-document-engine";
import { useDocumentEngine } from "fabricjs-document-engine/react";
import { CircleIcon, FolderOpenIcon, SaveIcon, SquareIcon } from "lucide-react";
import { useTranslations } from "next-intl";
import { useEffect, useState } from "react";
import {
DemoCanvas,
DemoFrame,
newShape,
seedShapes,
Status,
Toolbar,
ToolButton,
} from "./demo-ui";
import { useFabricCanvas } from "./use-fabric-canvas";
export default function SaveLoadDemo() {
const t = useTranslations("demos");
const { elementRef, canvas } = useFabricCanvas();
const engine = useDocumentEngine(canvas);
const [saved, setSaved] = useState<FabricDocument>();
useEffect(() => {
if (!engine) return;
engine.canvas.add(...seedShapes());
engine.clearHistory();
}, [engine]);
function save() {
// The whole canvas as plain JSON. Store it anywhere.
setSaved(engine?.toDocument());
}
async function load() {
if (saved) await engine?.loadDocument(saved);
}
return (
<DemoFrame>
<DemoCanvas elementRef={elementRef} label={t("canvas")} />
<Toolbar>
<ToolButton
icon={SquareIcon}
label={t("square")}
onClick={() => canvas?.add(newShape("square"))}
/>
<ToolButton
icon={CircleIcon}
label={t("circle")}
onClick={() => canvas?.add(newShape("circle"))}
/>
<span className="ml-auto flex gap-1.5">
<ToolButton
icon={SaveIcon}
label={t("saveLoad.save")}
primary
disabled={!engine}
onClick={save}
/>
<ToolButton
icon={FolderOpenIcon}
label={t("saveLoad.load")}
disabled={!saved}
onClick={() => void load()}
/>
</span>
</Toolbar>
<Status tone={saved ? "ok" : "idle"}>
{saved
? t("saveLoad.saved", { count: saved.objects.length })
: t("saveLoad.hint")}
</Status>
<pre className="h-32 overflow-auto rounded-lg border bg-muted p-3 font-mono text-xs">
{saved ? JSON.stringify(saved, null, 2) : "{}"}
</pre>
</DemoFrame>
);
}保存画布
const document = engine.toDocument();
await fetch(`/api/documents/${document.id}`, {
method: "PUT",
body: JSON.stringify(document),
});结果是一个普通的 FabricDocument 对象。里面有画布尺寸和背景、按叠放顺序排列的所有对象、文档需要的图片和字体,以及你自己的 metadata。每个字段的说明见文档格式。
和 Fabric.js 的 toJSON 相比,这种序列化多了:
- 每个对象都有稳定的
id,编组里的子对象也有; - 自动带上你的自定义对象的属性,不用每次调用都列出来;
- 一个
schemaVersion,以后的版本可以据此升级旧文件。
Fabric.js 加载 JSON
const response = await fetch("/api/documents/plan-42");
await engine.loadDocument(await response.json());在动画布之前,引擎会:
- 校验文档,并指出每个问题的具体路径;
- 升级旧文档和普通的 Fabric.js JSON;
- 拒绝未知的对象类型,而不是把它变成别的东西;
- 加载每张图片和每个字体,缺了图片就失败。
这些都通过后,才会把 JSON 回显到画布上。加载失败会抛出 DocumentEngineError,当前画布保持原样。而用普通的 loadFromJSON,图片出错时画布可能已经被清空了。
加载重叠时
如果用户先打开文档 A,在 A 加载完之前又打开了 B,最终显示 B。A 的加载会以 LOAD_ABORTED 取消,永远不会覆盖 B:
engine.on("load:error", ({ error }) => {
if (error.code === "LOAD_ABORTED") return;
showMessage(`The document could not be opened: ${error.message}`);
});配合存储保存和加载
接入存储适配器后,你按 id 操作,而不是直接处理 JSON:
await engine.load("plan-42");
await engine.save();这同时会启用自动保存和保存冲突检查。内置的选择见存储适配器。
未保存的内容不会被悄悄替换
接入存储适配器后,只要还有未保存的改动,load、loadDocument、importFabricJson 和 newDocument 都会以 UNSAVED_CHANGES 拒绝执行。先保存,或者在用户确认放弃改动时传入 { discardUnsavedChanges: true }。
打开旧的 Fabric.js JSON
Fabric.js 加载 JSON 同样适用于 Fabric.js 5、6、7 的 canvas.toJSON() 普通输出,打开时会自动升级。详见迁移。