@aiao/rxdb-plugin-storage
v0.0.20
Published
> Implements: [US-502 Storage 插件](../../requirements/stories/plugin/US-502-storage-plugin.md)
Readme
@aiao/rxdb-plugin-storage
Implements: US-502 Storage 插件
基于 RxDB + OPFS 的浏览器本地文件存储插件,负责文件二进制数据写入 OPFS,文件元数据写入 RxDB。
当前范围
- 仅关注浏览器本地文件存储
- 文件内容存放在 OPFS
- 文件元数据存放在 RxDB
- 预览使用 blob URL,而不是自定义协议
- 插件只负责注册和装配,异步初始化由 storage.init() 显式触发
功能特性
- 高性能本地读写:基于 OPFS,适合图片、文档和离线缓存场景
- 元数据可查询:文件名、路径、MIME、大小、时间戳等信息由 RxDB 管理
- 离线优先:文件访问不依赖远程服务
- 预览友好:通过 Blob 和 URL.createObjectURL 生成预览地址
- 可复用实现:适合抽离当前 Angular、React、Vue demo 里的重复 OPFS 逻辑
非目标
以下能力不属于当前实现,README 不再默认承诺:
- 自定义协议,如 rxdb-file://
- 返回 file:// URL
- 远程下载与 stale-while-revalidate
- 缩略图优先加载链路
- 远程版本同步与缓存协商
- LRU 淘汰、流式分块上传、断点续传
- 文件或目录重命名
设计原则
1. 插件层要薄
插件层只做两件事:
- 在 RxDB 实例上注册 storage 服务
- 注册文件元数据实体或 repository
不要把重异步逻辑塞进 install()。当前仓库里的 RxDB 运行时不会等待插件生命周期完成,异步恢复、扫描和预热应放到显式的 init() 中。
2. 文件服务层负责真实 I/O
文件服务层负责:
- OPFS 根目录访问
- 上传、读取、删除
- 下载到用户文件系统
- 创建和回收对象 URL
- 元数据写入和查询
3. UI 层只消费服务
Angular、React、Vue 侧不应该各自维护一套 OPFS 核心逻辑,只保留框架适配层即可。
元数据模型
当前实现只保留最小必要字段,避免把远程同步、缩略图、版本协商混进插件核心。
interface StorageFileMeta {
id: string;
name: string;
mimeType: string;
size: number;
opfsPath: string;
contentVersion: number;
createdAt: number;
updatedAt: number;
}字段说明
- id:文件主键
- name:原始文件名
- mimeType:文件 MIME 类型
- size:文件大小
- opfsPath:文件在 OPFS 中的相对路径
- contentVersion:本地内容版本号,每次覆盖写入递增
- createdAt / updatedAt:时间戳
公开 API
interface RxDBStoragePluginOptions {
rootDir?: string;
previewLimitBytes?: number;
}
interface UploadOptions {
path?: string;
overwrite?: boolean;
}
class RxdbFileStorage {
init(): Promise<void>;
upload(file: File, options?: UploadOptions): Promise<StorageFileMeta>;
read(fileId: string): Promise<Blob>;
preview(fileId: string): Promise<{
url: string;
type: string;
dispose: () => void;
}>;
createObjectUrl(fileId: string): Promise<string>;
revokeObjectUrl(url: string): void;
download(fileId: string, options?: { suggestedName?: string }): Promise<void>;
getMeta(fileId: string): Promise<StorageFileMeta | null>;
list(options?: { path?: string }): Promise<StorageFileMeta[]>;
delete(fileId: string): Promise<void>;
watch(fileId: string): Observable<StorageFileMeta | null>;
clear(path?: string): Promise<void>;
}约束:
- 需要
rxdb.config.sync.local.adapter,否则插件无法把文件元数据写入本地 RxDB - 需要浏览器支持 OPFS,即
navigator.storage.getDirectory() watch()只跟踪元数据变化,不直接监听所有外部 OPFS 改动
使用方式
import { rxDBPluginStorage } from '@aiao/rxdb-plugin-storage';
rxdb.use(rxDBPluginStorage, {
rootDir: 'files',
previewLimitBytes: 50 * 1024 * 1024
});
await rxdb.storage.init();
const meta = await rxdb.storage.upload(file, {
path: '/avatars',
overwrite: true
});
const preview = await rxdb.storage.preview(meta.id);
try {
image.src = preview.url;
} finally {
preview.dispose();
}为什么不使用自定义 URL
浏览器不会天然识别 rxdb-file:// 这样的自定义协议,img、video、audio 等标签也不能直接消费这类地址。除非额外引入协议拦截层或复杂的 service worker 转换逻辑,否则这个设计只会让 API 看起来漂亮,但无法稳定落地。
当前实现直接使用以下链路:
- 从 OPFS 读取 Blob
- 通过 URL.createObjectURL(blob) 生成临时地址
- 组件销毁或内容更新时及时 revoke
这是当前仓库各端 demo 已经验证过的方式,也是最接近浏览器现实的方案。
推荐实现分层
packages/rxdb-plugin-storage/
├── src/plugin.ts # RxDB 插件注册
├── src/storage.service.ts # 与 OPFS / RxDB 交互的核心服务
├── src/file-meta.entity.ts # 文件元数据实体定义
├── src/object-url.ts # blob URL 创建与回收
└── src/index.ts职责划分
- plugin.ts:同步注册 rxdb.storage
- storage.service.ts:显式 init(),封装所有异步 I/O
- file-meta.entity.ts:定义最小元数据模型
- object-url.ts:集中处理 URL.createObjectURL / revokeObjectURL
使用场景
- 离线可用的 Web 应用
- 需要本地缓存大量图片或文档的应用
- 浏览器端文件管理系统
- 基于 RxDB 的本地优先工作流
技术栈
- 存储层:OPFS (Origin Private File System)
- 元数据层:RxDB
- 预览层:Blob + URL.createObjectURL
注意事项
- OPFS 有存储配额,必须处理 QuotaExceededError
- OPFS 不提供原生 rename,目录和文件重命名本质上是复制后删除
- 对象 URL 必须显式回收,否则会造成内存泄漏
- 下载应优先使用 showSaveFilePicker,失败时再降级到 a 标签下载
- 预览能力建议保留体积上限,避免一次性读取超大文件
- watch() 更适合监听元数据变化,不应假设它能捕获所有外部 OPFS 改动
