Fabric.js Document Engine

搜索文档

查找页面或章节

EN

保存和加载

查看 Markdown

序列化画布,存到任何地方,再稳稳地加载回来。

保存就是 engine.toDocument()。Fabric.js 加载 JSON 就是 engine.loadDocument(json)。两者都不需要存储,所以 JSON 放在哪里都可以。

正在加载在线示例,代码在下方。

save-load-demo.tsx
"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());

在动画布之前,引擎会:

  1. 校验文档,并指出每个问题的具体路径;
  2. 升级旧文档和普通的 Fabric.js JSON;
  3. 拒绝未知的对象类型,而不是把它变成别的东西;
  4. 加载每张图片和每个字体,缺了图片就失败。

这些都通过后,才会把 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() 普通输出,打开时会自动升级。详见迁移。

下一步