Fabric.js 自动保存需要两样东西:一个存储适配器和 autosave 配置项。之后引擎会追踪未保存的改动,并在用户编辑时保存。
正在加载在线示例,代码在下方。
"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);崩溃之类仍会丢内容的情况,请加上恢复。