正文 JSON 保存图片引用,图片字节由媒体服务管理。只下载 JSON,不能保证换浏览器后还看得到图片。默认内存媒体服务适合预览;正式产品需要自己的资源身份、存储和恢复流程。
1. 在创建实例前准备媒体服务
media 是创建时服务,所有已挂载后端与文件服务共享它。各接口的用途如下:
| 接口 | 职责 |
|---|---|
sink.put(asset) | 同步登记完整字节,返回 { kind: 'asset', assetId } |
resolver.resolve(reference) | 根据引用取得字节,可同步或异步返回 |
registrar.register(asset) | 返回 accept() / rollback() lease,管理可能失败的插入 |
createAssetId(kind) | 分配在资源库内长期唯一的 ID |
不要把异步上传函数直接当作 sink.put()。可先由应用下载资源,建立同步工作集;或者上传完成后在一次受保护的提交里登记与插入。
2. 建立可保存的工作集
下面的例程复用 @quirekit/media 内存库,额外记录已接受的资源清单。restored 来自应用读取的资源字节;allocateId 使用服务器发放的全局唯一 ID,若在客户端生成 UUID,使用 uuid 的 v4。
import { createQuireMemoryMediaRepository, type QuireMediaAssetInput, type QuireAssetMediaReference } from "@quirekit/media";
import type { ClassicEditorMediaServices } from "@quirekit/classic-editor";
/**
* Builds the synchronous working set from bytes restored by the host before mounting.
* @param restored - Assets fetched with the document's asset manifest.
* @param allocateId - Host allocator of globally unique persistent IDs, such as server-issued IDs.
* @returns Classic services and an accepted-asset snapshot for durable storage.
*/
export function createDocumentMedia(restored: readonly QuireMediaAssetInput[], allocateId: () => string) {
const repository = createQuireMemoryMediaRepository();
const references = new Map<string, QuireAssetMediaReference>();
for (const asset of restored) { const reference = repository.put(asset); references.set(reference.assetId, reference); }
const media: ClassicEditorMediaServices = {
resolver: repository,
createAssetId: allocateId,
sink: {
/** @param asset - Complete in-memory bytes. @returns Registered reference. */
put(asset) { const reference = repository.put(asset); references.set(reference.assetId, reference); return reference; },
},
registrar: {
/** @param asset - Provisional authoring bytes. @returns Lease accepting only committed assets into the save manifest. */
register(asset) {
const lease = repository.register(asset);
return {
reference: lease.reference,
/** @returns Whether this operation accepted the asset. */
accept() {
const accepted = lease.accept();
if (accepted) references.set(lease.reference.assetId, lease.reference);
return accepted;
},
/** @returns Whether unaccepted new bytes were removed. */
rollback: () => lease.rollback(),
};
},
},
};
return {
media,
/** @returns Owned byte copies, including assets retained for undo; not JSON strings or object URLs. */
snapshotAssets(): readonly QuireMediaAssetInput[] {
return [...references.values()].map(reference => {
const asset = repository.resolve(reference)!;
return { assetId: reference.assetId, kind: asset.kind, bytes: asset.bytes,
mimeType: asset.mimeType, fileName: asset.fileName, intrinsicSize: asset.intrinsicSize };
});
},
};
}
创建时传 media: session.media。snapshotAssets() 返回独立字节副本,供应用上传;它不是可直接塞进普通 JSON 的二进制编码。应用可把字节存入对象存储或 IndexedDB,并另存包含 assetId、内容类型和存储位置的清单。
已经接受的资源仍可能被撤销历史引用。例程保留它们,不根据当前可见图片立即删除;资源回收由文档与历史的保存策略决定。相同 assetId 不能重新指向不同字节。
3. 保存与恢复的顺序
- 捕获同一时点的
instance.getJSON()与session.snapshotAssets()。 - 上传并确认本次正文所需资源;失败时保留未保存状态,不发布引用缺失资源的新版本。
- 提交正文版本及资源清单。版本冲突与原子发布由应用存储协议处理。
- 再次打开时,按清单取回字节,用原 assetId 创建工作集,然后创建 Classic 并传入正文和媒体服务。
普通 JSON.stringify(Uint8Array) 不会得到可直接恢复的媒体包;不要只保存浏览器 object URL。object URL 属于当前浏览器资源,不能作为文档的长期身份。
4. DOCX 与失败清理
官方 DOCX 服务解析时暂存新图片,只有导入仍有效、可以写入当前文档时才登记到 instance.media;正文提交失败会回滚本次新 lease。提供 registrar 能让失败清理有明确所有者;只有 sink 的自定义实现需要自行处理部分登记失败后的清理。
导出时 resolver 必须能返回真实字节,图片在屏幕上显示成功不能替代导出验证。外链引用的抓取策略由应用显式决定;跨域、失效链接和离线访问都要验证。转换警告与失败处理见保存与文件。
验证一次完整生命周期
插入图片后保存,销毁实例,使用新工作集和保存内容重新创建,再导出 DOCX;图片应可解析且 assetId 保持不变。另外验证上传失败、取消导入、重复导入同一资源,以及删除图片后撤销。检查资源字节和正文结构,不只检查页面截图。