Fabric.js Document Engine

搜索文档

查找页面或章节

EN

自动保存

查看 Markdown

在用户编辑时保存,并让他们知道保存成功了。

Fabric.js 自动保存需要两样东西:一个存储适配器和 autosave 配置项。之后引擎会追踪未保存的改动,并在用户编辑时保存。

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

autosave-demo.tsx
"use client";
 
import { isDocumentEngineError } from "fabricjs-document-engine";
import {
  useDocumentEngine,
  useDocumentState,
} from "fabricjs-document-engine/react";
import { createLocalStorage } from "fabricjs-document-engine/storage";
import { CircleIcon, SquareIcon, Trash2Icon } from "lucide-react";
import { useTranslations } from "next-intl";
import { useEffect } from "react";
 
import {
  DemoCanvas,
  DemoFrame,
  newShape,
  seedShapes,
  Status,
  Toolbar,
  ToolButton,
} from "./demo-ui";
import { useFabricCanvas } from "./use-fabric-canvas";
 
const storage = createLocalStorage({ prefix: "docs-demo-v2:" });
const DOCUMENT_ID = "autosave-demo";
const TONE = {
  saved: "ok",
  saving: "busy",
  unsaved: "idle",
  error: "error",
} as const;
 
export default function AutosaveDemo() {
  const t = useTranslations("demos");
  const { elementRef, canvas } = useFabricCanvas();
  const engine = useDocumentEngine(canvas, {
    storage,
    autosave: { delay: 600 },
    document: { id: DOCUMENT_ID },
  });
  const state = useDocumentState(engine);
 
  // Open the saved drawing. On a first visit, start from a small one.
  useEffect(() => {
    engine?.load(DOCUMENT_ID).catch((error) => {
      if (
        !isDocumentEngineError(error) ||
        error.code !== "DOCUMENT_NOT_FOUND"
      ) {
        console.error(error);
        return;
      }
      engine.canvas.add(...seedShapes());
      engine.clearHistory();
    });
  }, [engine]);
 
  // "conflict" cannot happen with one tab, so it shares the error line.
  const status =
    state?.saveStatus === "conflict" ? "error" : (state?.saveStatus ?? "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"))}
        />
        <ToolButton
          icon={Trash2Icon}
          label={t("clear")}
          onClick={() => canvas?.remove(...canvas.getObjects())}
        />
      </Toolbar>
 
      <Status tone={TONE[status]}>
        {t(`autosave.${status}`, { revision: state?.revision ?? 0 })}
      </Status>
    </DemoFrame>
  );
}

开启

import { createDocumentEngine } from "fabricjs-document-engine";
import { createLocalStorage } from "fabricjs-document-engine/storage";
 
const engine = createDocumentEngine({
  canvas,
  storage: createLocalStorage(),
  autosave: { delay: 1000, maxWait: 10000 },
});
  • delay:没有编辑多少毫秒后保存。
  • maxWait:第一次出现未保存改动后,最多等这么久就保存,即使用户还在编辑。

autosave: true 使用上面的默认值。

什么算未保存的改动

每一步记录下来的撤销步骤、每次撤销和重做,以及每次调用 updateMetadata(),都会把文档标记为已改动。engine.isDirty() 告诉你是否有未保存的内容。保存进行中的编辑,会等到下一次保存时才算保存。

你的代码用 object.set() 做的改动不会触发 Fabric.js 事件。用 engine.transaction() 包起来才会被记录,详见撤销和重做。

显示保存状态

save:status 事件带着完整的状态:

engine.on("save:status", ({ status, lastSavedAt, error }) => {
  statusLabel.textContent = status;
});

status 的值是 saved、unsaved、saving、error 或 conflict 之一。在 React 里,useDocumentState(engine).saveStatus 给你同样的值。按无障碍指南的做法,把状态文字放在 aria-live 区域里。

同一时间只保存一次

保存进行中再调用 save(),会排队一次用最新内容的后续保存。响应不可能乱序到达,所以又慢又旧的保存永远不会覆盖新内容。如果保存期间用户打开了另一份文档,这次保存的响应会被忽略。

失败自动重试

临时性的失败,比如网络断开,会按指数退避重试:

createDocumentEngine({
  canvas,
  storage,
  autosave: true,
  saveRetry: { attempts: 3, baseDelay: 500, maxDelay: 8000 },
});
 
engine.on("save:retry", ({ attempt, delay }) => {
  console.info(`Retry ${attempt} in ${delay} ms`);
});

如果重试也没用,比如没有权限,就在适配器里抛出带 retryable: false 的错误。冲突永远不会重试。

离开前提醒

Fabric.js 自动保存会等用户停下来,所以用户可能在编辑后一秒就关掉标签页。先让浏览器弹出确认:

import { bindUnsavedChangesWarning } from "fabricjs-document-engine";
 
const unbind = bindUnsavedChangesWarning(engine);

崩溃之类仍会丢内容的情况,请加上恢复。