RxDB RxStorage 层详解:为每种运行环境选择与组合最佳存储引擎
【免费下载链接】rxdbThe local-first database that runs on every JS runtime and replicates with your existing backend - no vendor, no lock-in - https://rxdb.info/项目地址: https://gitcode.com/gh_mirrors/rx/rxdb
RxStorage 是 RxDB 的存储抽象层,它把「数据库内核」与「底层存储引擎」彻底解耦:RxDB 本身并不直接写数据,而是把所有文档读写、查询、变更事件都委托给一个实现了 RxStorage 接口的存储实现,从而可以在浏览器、Node.js、Electron、React Native、Capacitor 等不同环境下自由切换底层数据层。读完本文,你将掌握 RxStorage 的接口模型、官方全部存储实现与包装插件的适用场景,并能通过「存储叠加」组合出面向加密、高查询负载、低写入延迟等不同目标的数据库配置。
为什么需要 RxStorage 抽象层
RxDB 不是一款自包含(self-contained)的数据库。从源码结构看,RxDB 的核心(src/rx-database.ts、src/rx-collection.ts 等)只依赖一个抽象的存储契约,真正的数据存放在某个 RxStorage 接口 实现中。在 src/types/rx-storage.interface.d.ts 中,接口头部的注释直接说明了设计意图:
"RxStorage is an interface that abstracts the storage engine. This allows us to use RxDB with different storage engines."
这种设计的直接收益是:你可以根据 JavaScript 运行环境和性能需求随时换掉底层数据层。例如,在 Capacitor 应用中使用 SQLite RxStorage,在纯浏览器应用中使用 LocalStorage RxStorage 存数据,另外还有面向 Node.js、React Native、NativeScript 等其他运行时的存储。
也正因为创建数据库时必须显式指定storage,RxDB 才可能在同一个 API 之上横跨浏览器、移动端、桌面端与服务端。在 src/rx-database.ts 中,RxDatabase构造器把storage作为公开只读属性保存,且数据库名去重键也由storage.name + '|' + name组成(见 src/rx-database.ts),说明存储引擎的身份(name)是数据库状态的一部分,切换存储即切换数据域。
RxStorage 接口模型:工厂与实例
理解 RxStorage 需要区分两个层次:存储工厂与存储实例,二者都定义在 src/types/rx-storage.interface.d.ts 中。
RxStorage(存储工厂)
一个RxStorage是一个「模块化工厂」,可以创建多个RxStorageInstance对象,其关键成员为:
| 成员 | 说明 |
|---|---|
name | 存储引擎名称,用于检测插件不兼容时抛出正确错误 |
rxdbVersion | 存储所对应的 RxDB 版本,用于多版本存储共存时的回退逻辑(如 storage migration 插件) |
createStorageInstance(params) | 创建一个包含某集合 NoSQL 文档的存储实例 |
接口注释还强调:存储实例的所有数据输入输出必须是structured-cloneable的——文档数据必须是纯 JSON 对象,附件数据使用 Blob,不能使用 Map、Set 等无法结构化克隆的结构。这保证了存储数据可以经由 WebWorker、WASM 进程或 BroadcastChannel 传输,唯一例外是 WebSocket 传输(它会在边界自行序列化)。
RxStorageInstance(存储实例)
createStorageInstance()返回的实例承载了真正的数据操作,每个实例中的文档必须符合同一 schema(且 schema 会被自动补全_meta、_rev、_deleted等元数据字段)。核心方法如下:
| 方法 | 职责 |
|---|---|
bulkWrite(rows, context) | 批量写入文档;每个文档的写入是原子的,但整体不构成事务;若前一版本不是当前最新修订会返回冲突错误;允许部分写入成功、部分失败 |
findDocumentsById(ids, withDeleted) | 按主键批量取文档,withDeleted为 true 时也返回已删除文档;出于性能返回数组而非索引对象 |
query(preparedQuery) | 执行 NoSQL mango 查询并返回结果;查询在进入存储前已由查询规划器归一化 |
count(preparedQuery) | 返回匹配查询的非删除文档数量,语义必须与query()结果数组长度一致 |
getAttachmentData(documentId, attachmentId, digest) | 以 Blob 返回单个附件数据 |
getChangedDocumentsSince?(limit, checkpoint) | (可选)返回指定 checkpoint 之后变更的文档,供备份与复制插件增量同步使用;未实现时 RxDB 会手动查询并用最后返回的文档做 checkpoint |
changeStream() | 返回持续推送所有变更的 Observable 流;若存储支持多实例且持久化,同一databaseName+collectionName的其他实例也必须收到变更(见 src/rx-storage-multiinstance.ts) |
cleanup(minimumDeletedTime) | 清理_deleted标记文档的墓碑以释放磁盘空间;返回true表示全部清理完,false表示因避免长时间阻塞还有剩余 |
close() | 关闭实例并释放内存 |
remove() | 删除整个数据库及其全部数据 |
另外还有一个可选的underlyingPersistentStorage引用:当存储是「内存 + 持久化」的组合(如 memory-synced 存储)时,复制与迁移会运行在底层持久化存储实例上,而不是内存实例上。
快速推荐:按运行环境选存储
原文档给出了一份面向场景的速查推荐:
- 浏览器(Browser):简单搭建、追求小体积用 LocalStorage RxStorage;数据集更大时,用免费的 Dexie.js storage,或者更快、构建体积更小的 IndexedDB RxStorage(需要 👑 premium 访问权限)。
- Electron 与 React Native:有 premium 访问权限时用 SQLite RxStorage,试玩阶段可用 trial-SQLite RxStorage;在 Expo 与 React Native 中追求极致性能,则用 Expo Filesystem RxStorage。
- Capacitor:有 👑 premium 访问权限用 SQLite RxStorage,否则用 LocalStorage storage。
从源码看存储工厂的实现形态
所有官方存储都遵循同样的工厂模式。以开箱即用的内存存储为例,src/plugins/storage-memory/index.ts 中getRxStorageMemory(settings)返回一个name: 'memory'、rxdbVersion: RXDB_VERSION的工厂对象,createStorageInstance()会把构造时传入的settings与实例创建参数params.options合并后交给createMemoryStorageInstance()。值得注意的是该模块顶部维护了一个跨实例共享的COLLECTION_STATESMap(src/plugins/storage-memory/index.ts),注释说明即使存储实例被关闭也保留状态,方便用它模拟文件系统式与多实例行为——这解释了为什么 Memory 存储「在全部环境中都能用」且适合测试。
LocalStorage 存储同样如此:src/plugins/storage-localstorage/index.ts 的getRxStorageLocalstorage(settings)返回一个name为RX_STORAGE_NAME_LOCALSTORAGE的RxStorageLocalstorage实例。如果你刚接触 RxDB,建议从 LocalStorage RxStorage 开始——它最容易上手、构建体积小。
配置示例:用「存储叠加」实现复杂需求
RxStorage 层非常灵活,关键在于包装存储(wrapper storage):每个包装插件都接收一个{ storage: ... }参数并返回一个新的存储,把上游存储包进自己的逻辑里。你可以在createRxDatabase({ storage })处一层层嵌套,组合出满足特定目标的存储配置。
示例一:在浏览器中安全地存储大量数据
假设你要构建一个需要在浏览器里尽可能安全地存储大量数据的应用,可以组合加密、IndexedDB、压缩与 schema 校验四层,既提升安全性又压缩存储体积:
import { wrappedValidateAjvStorage } from 'rxdb/plugins/validate-ajv'; import { wrappedKeyCompressionStorage } from 'rxdb/plugins/key-compression'; import { wrappedKeyEncryptionCryptoJsStorage } from 'rxdb/plugins/encryption-crypto-js'; import { getRxStorageIndexedDB } from 'rxdb-premium/plugins/storage-indexeddb'; const myDatabase = await createRxDatabase({ storage: wrappedValidateAjvStorage({ storage: wrappedKeyCompressionStorage({ storage: wrappedKeyEncryptionCryptoJsStorage({ storage: getRxStorageIndexedDB() }) }) }) });分层顺序是有讲究的:最顶层放 schema 校验,确保 schema 错误清晰可读,且报错内容不包含加密后的密文或压缩后的乱码;加密放在压缩内部,因为对压缩后的数据进行加密更高效(压缩先行缩小了需要加密的数据量)。
从源码看,这些wrapped*函数都只是对原存储的浅层增强。例如 src/plugins/key-compression/index.ts 的wrappedKeyCompressionStorage({ storage })用Object.assign({}, args.storage, { createStorageInstance: ... })生成新存储,只重写createStorageInstance(),其余接口全部透传;src/plugins/encryption-crypto-js/index.ts 的wrappedKeyEncryptionCryptoJsStorage同样如此。这意味着包装存储与被包装存储对外是「同一个 RxStorage 接口」,可以无限层叠,也解释了为何组合顺序能精确控制数据在各层的处理路径。加密与键压缩的完整原理分别见 encryption 与 key-compression。
示例二:高查询负载
如果要针对「复杂查询要跑得飞快」来优化,可以用sharding(分片)叠加worker(工作线程):查询可以并行地跑在多个线程里,而不是单个 JavaScript 进程。由于 worker 初始化可能拖慢首屏加载,再套一层 localstorage-meta-optimizer 来改善初始化时间:
import { getRxStorageSharding } from 'rxdb-premium/plugins/storage-sharding'; import { getRxStorageWorker } from 'rxdb-premium/plugins/storage-worker'; import { getRxStorageIndexedDB } from 'rxdb-premium/plugins/storage-indexeddb'; import { getLocalstorageMetaOptimizerRxStorage } from 'rxdb-premium/plugins/storage-localstorage-meta-optimizer'; const myDatabase = await createRxDatabase({ storage: getLocalstorageMetaOptimizerRxStorage({ storage: getRxStorageSharding({ storage: getRxStorageWorker({ workerInput: 'path/to/worker.js', storage: getRxStorageIndexedDB() }) }) }) });这里workerInput指向 worker 脚本路径,存储本体在 worker 内运行 IndexedDB;分片存储把文档切分到多个数据库实例上,以(如 IndexedDB 这类存储上)换取向上的性能提升;最外层的 meta optimizer 则把 RxDB 建库、建集合所需的纯键值元数据放进 localStorage,缩短首屏初始化。
示例三:写入与简单读取的低延迟
如果应用以简单读写为主、追求低延迟,可以用memory-mapped 存储把数据拉进内存做读写,用OPFS 存储做主线程持久化——初始化时从磁盘把大块数据载入内存的延迟更低。这里刻意不使用 worker,因为主线程与 worker 之间的数据传输会引入额外延迟:
import { getLocalstorageMetaOptimizerRxStorage } from 'rxdb-premium/plugins/storage-localstorage-meta-optimizer'; import { getMemoryMappedRxStorage } from 'rxdb-premium/plugins/storage-memory-mapped'; import { getRxStorageOPFSMainThread } from 'rxdb-premium/plugins/storage-worker'; const myDatabase = await createRxDatabase({ storage: getLocalstorageMetaOptimizerRxStorage({ storage: getMemoryMappedRxStorage({ storage: getRxStorageOPFSMainThread() }) }) });三个示例共同展示了 RxStorage 架构的核心理念:底层引擎决定「数据落在哪里、如何持久化」,包装插件决定「数据如何被变换、在哪里执行」,二者正交组合,即可为几乎任何约束(安全、吞吐、延迟、体积)定制存储栈。
全部 RxStorage 实现一览
核心实现
| 存储 | 说明 |
|---|---|
| Memory | 数据以纯对象保存在 JavaScript 进程内存中,速度极快,可在所有环境使用。Read more |
| LocalStorage | 基于浏览器 localStorage API 的存储,搭建最简单、构建体积小,适合 RxDB 新手入门。Read more |
| 👑 IndexedDB | 基于原生 IndexedDB;对大多数用例而言,与 OPFS 存储并列为浏览器中性能最好的选择。Read more |
| 👑 OPFS | 基于 File System Access API(OPFS);在浏览器中使用 RxDB 时,是除内存存储外性能最好的非内存存储。Read more |
| 👑 Filesystem Node | 最适合在 Node.js 进程或 Electron 中使用 RxDB 的场景。Read more |
存储包装插件(Storage Wrapper Plugins)
| 插件 | 说明 |
|---|---|
| 👑 Worker | 包装任意 RxStorage,把存储跑在浏览器 WebWorker 或 Node.js Worker Thread 中,把 CPU 负载从主进程移出,改善应用可感知性能。Read more |
| 👑 SharedWorker | 包装任意 RxStorage,把存储跑在 SharedWorker(仅浏览器)中,同样实现主进程减负。Read more |
| Remote | 面向「远程存储 + 异步消息通道」的通信而设计,远程端可以是另一个 JavaScript 进程甚至另一台主机;多被 Worker、Electron-ipc 等存储内部使用。Read more |
| 👑 Sharding | 在部分 RxStorage(如 IndexedDB)上,把文档分片到多个数据库实例可带来巨大的性能提升;可包装任意存储为分片存储。Read more |
| 👑 Memory Mapped | 包装任意存储:创建一个用于查询与写入的内存存储,同时把数据落到底层存储做持久化,主要价值是提升查询/写入性能而数据仍在磁盘。Read more |
| 👑 Localstorage Meta Optimizer | 包装任意存储:集合文档仍走原存储,但把 RxDB 建库建集合所需的纯键值元数据放进 localStorage,以优化首屏加载时间;仅限浏览器。Read more |
| Electron IpcRenderer & IpcMain | 在 Electron 中推荐把 RxStorage 跑在主进程、RxDatabase 跑在渲染进程;通过 rxdb electron 插件创建远程 RxStorage 供渲染进程消费。Read more |
第三方引擎类存储
| 存储 | 说明 |
|---|---|
| 👑 Expo Filesystem | 通过 JSI 绑定绕过 RN bridge,把 OPFS 级的高速能力带给 React Native / Expo,是 React Native 上最快的存储引擎。Read more |
| 👑 SQLite | 在Node.js、Electron、React Native、Cordova、Capacitor上都有出色的性能表现。Read more |
| Dexie.js | 基于 Dexie.js 这个 IndexedDB 封装库实现。Read more |
| MongoDB | 服务端使用 RxDB 的选择之一,基于流行的 MongoDB NoSQL 数据库,安全、可扩展、高性能。Read more |
| DenoKV | 在 Deno 中使用 RxDB 的方案,基于 Deno 的 Key Value Store,安全、可扩展、高性能。Read more |
| FoundationDB | 服务端使用 RxDB 的另一个选择,基于 FoundationDB,安全、容错、高性能。Read more |
如何挑选与验证你的存储组合
选型时可以按以下路径推进:
- 确定运行环境:浏览器优先看 IndexedDB / OPFS / Dexie.js / LocalStorage;Node.js 看 Filesystem Node / SQLite / MongoDB / FoundationDB;移动端看 SQLite / Expo Filesystem / LocalStorage;Deno 看 DenoKV。
- 明确性能瓶颈:查询吞吐优先考虑 Worker + Sharding;简单读写低延迟优先考虑 Memory Mapped + OPFS;初始化速度优先考虑 Localstorage Meta Optimizer。
- 确定安全需求:需要加密与压缩时,按「压缩在加密之前、校验在最外层」的顺序叠加包装层。
- 确认许可与访问级别:部分存储(如 IndexedDB、OPFS、SQLite、Sharding、Worker 等)标注 👑,需要 premium 访问权限;LocalStorage、Memory、Dexie.js、Remote 等为免费可用。
- 用测试印证行为:仓库的 test/unit/rx-storage-implementations.test.ts 会以统一的用例矩阵校验各存储实现,test/unit/rx-storage-query-correctness.test.ts 验证查询语义一致性,test/unit/rx-storage-helper.test.ts 覆盖辅助逻辑——自研或选型存储时,可参考这些测试对齐行为契约。
总结
RxStorage 是 RxDB 赖以横跨全部 JavaScript 运行时的架构基石:一个定义在 src/types/rx-storage.interface.d.ts 的接口,加上一组「工厂—实例」实现与可无限叠加的包装插件,让你不必改变任何业务代码就能更换、组合、优化底层数据层。无论是浏览器中的 LocalStorage 快速起步、IndexedDB/OPFS 的高性能持久化,还是 Electron/React Native 中的 SQLite、服务端的 MongoDB/FoundationDB,乃至通过 Worker、Sharding、Memory Mapped、Meta Optimizer 等包装插件实现的线程级并行与低延迟读写,你都可以基于本文的组合模式在createRxDatabase({ storage })一处完成定制。
【免费下载链接】rxdbThe local-first database that runs on every JS runtime and replicates with your existing backend - no vendor, no lock-in - https://rxdb.info/项目地址: https://gitcode.com/gh_mirrors/rx/rxdb
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考