Fabric.js 后端存储就是一个带两个函数的对象。这篇文章带你写一个完整的 REST API 适配器,以及让保存保持安全的服务器端逻辑。
规则
- 存储的修订号和
expectedRevision不一致时,拒绝保存。抛出带code: "SAVE_CONFLICT"的错误,或使用createConflictError。 expectedRevision为null时跳过这个检查,因为用户选择了覆盖。- 重试也没用时,比如没有权限,抛出带
retryable: false的错误。 - 把
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 字段的摘要,最新的在前。
适配器写好后,运行适配器检查。