Fabric.js Document Engine

搜索文档

查找页面或章节

EN

自定义后端

查看 Markdown

为你自己的 API 写一个完整的存储适配器,以及配套的服务器端逻辑。

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

规则

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

适配器

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 写:

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

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

保存时上传图片

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

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 后端存储里保留版本,给适配器加上四个方法:saveVersion(version)、listVersions(documentId)、loadVersion(documentId, versionId) 和 deleteVersion(documentId, versionId)。listVersions 返回不含 document 字段的摘要,最新的在前。

适配器写好后,运行适配器检查。