# 上线清单

> Fabric.js 编辑器上线指南：完整的引擎配置、每种失败的错误处理、安全地打开文档，以及上线前的检查清单。

Source: https://fabricjs-document-engine.jscrate.dev/zh/docs/production/production-checklist
Last updated: 2026-09-28

这篇指南把所有功能组合在一起，帮你完成 Fabric.js 编辑器上线：存储、自动保存、恢复、版本、限制和资源，以及每种失败的错误处理。

## 引擎

```ts
import {
  bindKeyboardShortcuts,
  bindUnsavedChangesWarning,
  createDocumentEngine,
} from "fabricjs-document-engine";
import { createIndexedDbRecovery } from "fabricjs-document-engine/recovery";

export function createEditor(canvas: Canvas) {
  const engine = createDocumentEngine({
    canvas,
    storage: restStorage,
    customObjects: [{ fabricClass: Sticker, properties: ["label"] }],
    autosave: { delay: 1500, maxWait: 15000 },
    saveRetry: { attempts: 4, baseDelay: 800, maxDelay: 10000 },
    recovery: { store: createIndexedDbRecovery(), interval: 3000 },
    versions: { autoEvery: 20, keepAuto: 30 },
    history: { limit: 150, maxBytes: 48 * 1024 * 1024 },
    limits: {
      maxObjects: 20000,
      isAllowedUrl: (url) =>
        url.startsWith("https://cdn.example.com/") || url.startsWith("blob:"),
    },
    assets: {
      resolveUrl: (url) => signCdnUrl(url),
      upload: async ({ blob }) => uploadToBucket(blob),
      replaceMissingImage: () =>
        "https://cdn.example.com/placeholders/missing.png",
    },
  });

  const unbindShortcuts = bindKeyboardShortcuts(engine);
  const unbindWarning = bindUnsavedChangesWarning(engine);

  return {
    engine,
    dispose() {
      unbindShortcuts();
      unbindWarning();
      engine.destroy();
    },
  };
}
```

## 打开文档

打开之前，先检查是否有中断的加载和恢复副本：

```ts
async function openDocument(engine: DocumentEngine, id: string) {
  const interrupted = await engine.getInterruptedLoad();
  if (
    interrupted?.documentId === id &&
    !(await confirmUser("This document failed to open last time. Try again?"))
  ) {
    return;
  }

  const copy = await engine.getRecovery(id);
  if (copy && (await confirmUser("Restore unsaved work?"))) {
    await engine.restoreRecovery(id, { discardUnsavedChanges: true });
    return;
  }
  if (copy) await engine.discardRecovery(id);

  await engine.load(id);
}
```

## 错误处理

每种失败都是带稳定 `code` 的 `DocumentEngineError`。按 code 判断，不要按消息文字判断：

```ts
engine.on("load:error", ({ error }) => {
  switch (error.code) {
    case "LOAD_ABORTED":
      return;
    case "MISSING_ASSETS":
      return showMissingImages(error.missingAssets);
    case "UNKNOWN_OBJECT_TYPE":
      return showMessage("This document needs a newer version of the editor.");
    case "UNSUPPORTED_SCHEMA":
      return showMessage(
        "This document was made with a newer version of the editor."
      );
    case "UNSAFE_DOCUMENT":
      return showMessage("This file was blocked for safety reasons.");
    default:
      return showMessage(`The document could not be opened: ${error.message}`);
  }
});

engine.on("save:error", ({ error }) => {
  if (error.code === "SAVE_CONFLICT") return askConflict();
  if (error.code !== "SAVE_CANCELLED") {
    showMessage("Saving failed. Your work is kept on this device.");
  }
});
```

所有错误码都列在[错误码](https://fabricjs-document-engine.jscrate.dev/zh/docs/api/error-codes)里。

## 离开编辑器

```ts
async function closeEditor(editor: ReturnType<typeof createEditor>) {
  if (editor.engine.isDirty())
    await editor.engine.save().catch(() => undefined);
  editor.dispose();
}
```

`destroy()` 还会为未保存的内容写一份恢复副本，所以即使最后这次保存失败，也不会丢内容。

## 在 Node.js 服务器上

文档相关的辅助函数在 Node.js 里无需画布就能运行。存储之前检查每个上传，做法见[安全](https://fabricjs-document-engine.jscrate.dev/zh/docs/guides/security)。

## 清单

Fabric.js 编辑器上线之前，逐项过一遍这份清单：

- [ ] `verifyStorageAdapter(yourStorage)` 通过。见[检查适配器](https://fabricjs-document-engine.jscrate.dev/zh/docs/storage/verify-adapter)。
- [ ] 服务器在同一条查询里比较修订号并写入。
- [ ] 所有打开文档的地方都注册了自定义类。
- [ ] 网络字体通过 `assets.loadFont` 加载。
- [ ] 图片带 CORS 和 `crossOrigin: "anonymous"` 加载，导出才能成功。
- [ ] 为 `SAVE_CONFLICT`、`MISSING_ASSETS`、恢复提示和 `UNSAVED_CHANGES` 准备了界面。
- [ ] 工具栏遵循了[无障碍指南](https://fabricjs-document-engine.jscrate.dev/zh/docs/guides/accessibility)。

通过这份清单，你的 Fabric.js 编辑器上线后就能处理用户真正会遇到的失败。
