# 自定义后端

> 为你自己的 REST API 编写 Fabric.js 后端存储：SQL 修订号检查、清晰的错误、图片上传和版本，一个小适配器搞定。

Source: https://fabricjs-document-engine.jscrate.dev/zh/docs/storage/custom-backend
Last updated: 2026-09-28

Fabric.js 后端存储就是一个带两个函数的对象。这篇文章带你写一个完整的 REST API 适配器，以及让保存保持安全的服务器端逻辑。

## 规则

1. 存储的修订号和 `expectedRevision` 不一致时，拒绝保存。抛出带 `code: "SAVE_CONFLICT"` 的错误，或使用 `createConflictError`。
2. `expectedRevision` 为 `null` 时跳过这个检查，因为用户选择了覆盖。
3. 重试也没用时，比如没有权限，抛出带 `retryable: false` 的错误。
4. 把 `signal` 传给 `fetch`，这样打开另一份文档时，保存会被取消。

## 适配器

```ts
import type { DocumentStorage } from "fabricjs-document-engine";

export const restStorage: DocumentStorage = {
  async loadDocument(id) {
    const response = await fetch(`/api/documents/${encodeURIComponent(id)}`);
    if (response.status === 404) {
      throw Object.assign(new Error("Not found"), {
        code: "DOCUMENT_NOT_FOUND",
      });
    }
    if (!response.ok) throw new Error(`Loading failed with ${response.status}`);
    return response.json();
  },

  async saveDocument(document, { expectedRevision, signal }) {
    const response = await fetch(
      `/api/documents/${encodeURIComponent(document.id)}`,
      {
        method: "PUT",
        signal,
        headers: {
          "Content-Type": "application/json",
          "If-Match":
            expectedRevision === null ? "*" : String(expectedRevision),
        },
        body: JSON.stringify(document),
      }
    );
    if (response.status === 409 || response.status === 412) {
      throw Object.assign(new Error("Someone else saved this document"), {
        code: "SAVE_CONFLICT",
      });
    }
    if ([401, 403, 422].includes(response.status)) {
      throw Object.assign(new Error(`Save refused with ${response.status}`), {
        retryable: false,
      });
    }
    if (!response.ok) throw new Error(`Save failed with ${response.status}`);
    const { revision } = (await response.json()) as { revision: number };
    return { revision };
  },
};
```

`document.revision` 是下一个修订号。如果你的后端自己分配修订号，就以 `{ revision }` 返回。

## 服务器端

把修订号检查和写入放在同一步里，这样两次保存不可能同时通过。用 SQL 写：

```sql
UPDATE documents
SET body = $1, revision = revision + 1
WHERE id = $2 AND ($3::int IS NULL OR revision = $3)
RETURNING revision;
```

没有返回行时，返回 `409`。如果分两条查询先检查再写入，另一次保存就可能插进来。

## 保存时上传图片

只存在于用户标签页里的图片会上传一次，文档里保存返回的地址：

```ts
const engine = createDocumentEngine({
  canvas,
  storage: restStorage,
  assets: {
    upload: async ({ blob }) => {
      const body = new FormData();
      body.append("file", blob);
      const response = await fetch("/api/uploads", { method: "POST", body });
      const { url } = (await response.json()) as { url: string };
      return url;
    },
  },
});
```

这样图片上传就和保存合在一起了。

## 版本

想在 Fabric.js 后端存储里保留[版本](https://fabricjs-document-engine.jscrate.dev/zh/docs/guides/versions)，给适配器加上四个方法：`saveVersion(version)`、`listVersions(documentId)`、`loadVersion(documentId, versionId)` 和 `deleteVersion(documentId, versionId)`。`listVersions` 返回不含 `document` 字段的摘要，最新的在前。

适配器写好后，运行[适配器检查](https://fabricjs-document-engine.jscrate.dev/zh/docs/storage/verify-adapter)。
