RxDB 作为 Horizon 的替代方案:离线优先、客户端响应式数据库实战指南

发布时间:2026/9/20 23:43:48
RxDB 作为 Horizon 的替代方案:离线优先、客户端响应式数据库实战指南
数据库NoSQL嵌入式数据库实时数据库【免费下载链接】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点击查看免费下载Horizon 曾是 RethinkDB 官方的客户端实时数据库方案但因公司停运而停止维护且从未实现离线支持。本文以 RxDB 为主线完整梳理 Horizon 的架构缺陷并逐一给出 RxDB 在响应式查询、离线优先、多标签同步、灵活复制、冲突解决、Schema 校验与迁移、静态加密等方面的对等乃至超越能力帮助团队完成从在线订阅式架构到本地优先数据库的迁移。什么是 HorizonHorizon又称 horizon.io是构建在 RethinkDB 之上的开源后端平台由 RethinkDB 公司于 2016 年 5 月发布。其目标是让前端 JavaScript 开发者无需编写和维护传统的 REST 或 GraphQL API即可直接获得实时数据通道。Horizon 客户端库直接连接 Horizon 服务器进程服务器再与 RethinkDB 通信。开发者通过集合collection和一套返回 RxJS Observable 的流式 API 操作数据。一个典型的 Horizon 应用如下// Horizon: 连接后端 const horizon Horizon(); // 获取集合引用对应 RethinkDB 的表 const messages horizon(messages); // 存储一条文档 messages.store({ id: msg-1, text: Hello, Horizon!, createdAt: new Date() }); // 监听实时变化 —— 返回 RxJS Observable messages.watch().subscribe(allMessages { renderMessages(allMessages); }); // 仅获取一次非实时 messages.fetch().subscribe(allMessages { console.log(allMessages); }); // 排序与分页 messages.order(createdAt, descending).limit(20).watch().subscribe(recent { renderRecentMessages(recent); });watch()方法是核心它连接到 RethinkDB 的 changefeed数据一变就向客户端推送更新后的结果集。这在 2016 年非常有吸引力——在那个时代不借助轮询构建实时应用需要大量基础设施工作。Horizon 还提供认证本地用户名/密码、GitHub OAuth、Google OAuth 及其他 Provider权限系统通过规则控制哪些用户可读/写哪些文档hz命令行工具用于脚手架项目和运行本地开发服务器可嵌入现有 Node.js 应用满足需要在数据层之外编写自定义服务端逻辑的团队。Horizon 的时间线2016 年 5 月Horizon 随hzCLI 与 JavaScript 客户端库公开亮相。2016 年 10 月RethinkDB Inc. 宣布停运公司未能建立起可持续的商业模式。2017 年 2 月Linux 基金会经云原生计算基金会 CNCF接管 RethinkDB 并改以 Apache 2.0 授权但 Horizon 并未获得同等对待。2016 年至今Horizon 未获得任何有意义的更新GitHub 仓库实质归档。停运几乎发生在发布之后不久Horizon 从未有机会成熟。社区从 2016 年起就在 GitHub Issue 中强烈要求的离线支持等规划功能从未落地并在项目被放弃后不了了之。Horizon 做得好的地方对于纯在线实时 Web 应用这一狭窄场景Horizon 显著减少了开发者需要编写的代码量订阅集合、直接在 UI 中渲染结果完全不用写服务端路由。基于 Observable 的 API 在当时相当超前把响应式编程范式带进了客户端-服务端数据层。Horizon 的短板在哪里没有离线支持Horizon 从未为离线场景设计。每次查询、读取、写入都必须与 Horizon 服务器保持实时网络连接。连接一旦断开watch()返回的 Observable 流会停止发射任何store()、update()、remove()调用都会静默失败或直接抛错。离线支持是 Horizon 仓库中最受请求的功能之一但从未被设计更别说实现。对大多数现代应用而言离线能力并非锦上添花用户期望应用在地铁、信号差的建筑、移动数据昂贵的地区都能工作。网络一断应用就坏这样的应用是残缺的。项目已被放弃Horizon 无人维护没有安全更新没有对现代 Node.js 版本的兼容修复没有 TypeScript 类型定义也没有对 issue 和 PR 的响应。2025 年在全新项目里安装 Horizon必须绕开与现代工具链的依赖冲突。底层 RethinkDB 仍偶有社区维护版本但 Horizon 与之独立无法从中获益。任何建立在 Horizon 之上的团队都迟早面临迁移问题。服务端架构无客户端存储Horizon 不在客户端保存任何数据所有数据都存于服务端 RethinkDB。客户端库只是一个订阅机制而非数据库。这意味着离线时应用完全无法提供数据没有本地查询缓存查询参数一变就是一次新的网络请求无法在本地写入数据后再同步关闭并重新打开应用必须重新从服务器拉取全部数据。这种架构与离线优先offline-first模式从根本上互斥——离线优先要求把本地存储当作主要数据源把服务器当作同步目标而非强制依赖。没有冲突解决机制Horizon 没有提供两个用户并发修改同一文档时的冲突解决机制。服务端权威模型决定了由 RethinkDB 的 last-write-wins最后写入胜出行为定夺结果。实践中两个用户同时修改同一文档时其中一方的修改被静默覆盖且双方都毫不知情。对协作类应用这是重大缺陷开发者没有任何 API 能检测冲突、检查两个版本、或应用合并策略。高度固化的结构Horizon 要求特定的项目结构、特定的服务进程hz serve和自带的认证系统。把它集成进已有后端、非 Node.js 服务器、或已有认证层的项目需要大量变通。权限系统围绕 Horizon 自己的用户模型设计难以适配现有用户库或角色体系。RxDB 如何覆盖同样的能力并且更多RxDB 与 Horizon 共享核心思想数据变更应通过响应式、Observable 风格的 API 自动传播到 UI。但 RxDB 把这一能力实现在客户端——一个完整的本地数据库——而不是一个指向远端服务器的订阅机制。客户端响应式查询在 RxDB 中每个查询都是 Observable。订阅查询时立即拿到当前结果集此后无论变更来自本地写入还是复制事件Observable 都会重新发射import { createRxDatabase } from rxdb/plugins/core; import { getRxStorageIndexedDB } from rxdb/plugins/storage-indexeddb; const db await createRxDatabase({ name: myapp, storage: getRxStorageIndexedDB() }); await db.addCollections({ messages: { schema: { title: message schema, version: 0, primaryKey: id, type: object, properties: { id: { type: string, maxLength: 100 }, text: { type: string }, roomId: { type: string }, createdAt: { type: number } }, required: [id, text, roomId, createdAt], indexes: [createdAt] } } }); // 订阅某房间的消息按创建时间排序 db.messages.find({ selector: { roomId: room-42 }, sort: [{ createdAt: asc }] }).$.subscribe(messages { renderMessages(messages); // 立即调用且每次变更都会调用 });这等价于 Horizon 的watch()API但数据来自本地 IndexedDB 存储而非远端服务器。无论在线还是离线UI 的行为完全一致。RxDB 用event-reduce 算法让响应式更新保持高效。核心实现在 src/event-reduce.ts其中calculateNewResults()会检查能否通过直接应用变更事件来更新已有查询结果而无需对存储重跑完整查询src/event-reduce.ts。为保证这一点集合层维护了一个ChangeEventBuffer缓冲最近的变更事件供查询在.exec()时使用src/change-event-buffer.ts默认缓冲上限为 100 条见 src/change-event-buffer.ts。这使得高写入负载下 UI 仍能保持快速响应。完整的离线优先运行用户在没有网络时打开 RxDB 应用一切功能照常读来自本地存储写进本地存储。没有报错、没有等待服务器的加载转圈、没有缺失数据。网络恢复后RxDB 的复制插件自动把本地变更与远端后端同步应用在离线和在线状态间无缝切换单个功能无需任何代码改动。这正是 离线优先架构RxDB 把本地存储当作主要数据源服务器只是同步目标而非正常运行的前提。Horizon 的架构恰恰相反——服务器是唯一数据源离线运行根本不可能。多标签页支持Horizon 每个浏览器窗口维持一条连接。用户打开同一应用的两个标签页每个标签页各自持有与服务器的 WebSocket 连接标签页间的本地状态可能分叉。RxDB 提供 SharedWorker 存储模式让单个数据库实例运行在共享 Worker 中所有标签页共享这一个实例。一个标签页的写入会立即反映到其他所有标签页的响应式查询中无需额外代码import { getRxStorageSharedWorker } from rxdb/plugins/storage-shared-worker; const db await createRxDatabase({ name: myapp, storage: getRxStorageSharedWorker({ workerInput: new SharedWorker( new URL(rxdb/plugins/storage-shared-worker/worker.js, import.meta.url), { type: module } ) }) });对于只需一个标签页执行的后台任务如运行复制RxDB 提供 leader election 插件一个标签页被选为 leader 处理后台工作leader 标签页被关闭时其他标签页自动接管import { RxDBLeaderElectionPlugin } from rxdb/plugins/leader-election; import { addRxPlugin } from rxdb/plugins/core; addRxPlugin(RxDBLeaderElectionPlugin); // 只有 leader 标签页运行复制 await db.waitForLeadership(); startReplication(db);从源码看leader election 基于broadcast-channel库的createLeaderElection()实现并以数据库名 token 为键复用电极器src/plugins/leader-election/index.ts从而保证同一数据库在多个标签页间只会选出一个 leader。灵活复制到任意后端Horizon 要求 Horizon 服务器而 Horizon 服务器又要求 RethinkDB整条技术栈被固定死。RxDB 把存储与复制解耦两者均可独立配置。RxDB 在本地存储数据并通过插件系统复制到后端。你可以复制到应用已使用的任意后端import { replicateRxCollection } from rxdb/plugins/replication; const replicationState await replicateRxCollection({ collection: db.messages, replicationIdentifier: messages-http-v1, pull: { handler: async (checkpoint, batchSize) { const since checkpoint?.updatedAt ?? 0; const response await fetch( /api/messages?since${since}limit${batchSize} ); const data await response.json(); return { documents: data.documents, checkpoint: data.checkpoint }; } }, push: { handler: async (rows) { const response await fetch(/api/messages, { method: POST, body: JSON.stringify(rows), headers: { Content-Type: application/json } }); return response.json(); // 返回冲突文档或 [] } }, live: true, retryTime: 5000 }); // 可观察的复制状态 replicationState.active$.subscribe(active console.log(Syncing:, active)); replicationState.error$.subscribe(err console.error(Sync error:, err));pull handler 从检查点checkpoint之后拉取服务端变更push handler 把本地变更推给服务端。复制协议足够简单任何后端语言都能实现服务端。不要求运行特定服务进程、使用特定数据库、或采纳特定权限模型。这个通用复制插件的实现位于 src/plugins/replication/index.tsreplicateRxCollection()的默认参数正是live true、retryTime 1000 * 5即 5 秒src/plugins/replication/index.ts与上面的示例一致。断线重试的逻辑在 src/plugins/replication/replication-helper.ts 的awaitRetry()浏览器环境下若navigator.onLine为 false会同时等待online事件与retryTime超时二者先到者触发重试。对常见的后端形态RxDB 提供了开箱即用的插件插件适用场景HTTP 复制任意 REST API 端点GraphQL 复制GraphQL API包括 AWS AppSyncWebSocket 复制低延迟服务端推送CouchDB 复制CouchDB 或 PouchDB 服务器Firestore 复制Google Cloud Firestore可插拔的存储后端RxDB 的存储层与查询、复制逻辑分离。同一套应用代码可根据运行环境切换不同的存储引擎环境存储方案浏览器标准IndexedDB浏览器高吞吐OPFSOrigin Private File SystemReact Native / Expo基于 expo-sqlite 或 op-sqlite 的 SQLiteNode.js / ElectronSQLitebetter-sqlite3多标签页浏览器SharedWorker测试 / CIMemory切换存储只需改一行import { getRxStorageOpfs } from rxdb/plugins/storage-opfs; const db await createRxDatabase({ name: myapp, storage: getRxStorageOpfs() // 浏览器中改用 OPFS 获得更高吞吐 });Horizon 没有对等能力数据始终在服务端 RethinkDB没有本地存储、没有存储抽象、也没有在无 RethinkDB 实例时运行应用的办法。冲突解决Horizon 继承了 RethinkDB 的 last-write-wins 冲突模型两个并发写导致静默覆盖开发者没有机制检测、检查或合并冲突版本。RxDB 为每个集合提供可配置的 conflictHandler。复制过程中同一文档的两个版本相遇时conflictHandler 被调用并返回仲裁后的文档await db.addCollections({ messages: { schema: messageSchema, conflictHandler: async ({ newDocumentState, realMasterState }) { // 保留更新时间较新的版本 if (newDocumentState.updatedAt realMasterState.updatedAt) { return { documentData: newDocumentState }; } return { documentData: realMasterState }; } } });对于多用户并发编辑同一文档的协作场景RxDB 支持 基于 CRDT 的冲突解决。CRDT 以确定性方式合并并发编辑无需中心权威判定谁赢import { getCRDTSchemaPart, RxDBcrdtPlugin } from rxdb/plugins/crdt; import { addRxPlugin } from rxdb/plugins/core; addRxPlugin(RxDBcrdtPlugin); const messageSchema { version: 0, primaryKey: id, type: object, properties: { id: { type: string, maxLength: 100 }, text: { type: string }, roomId: { type: string }, crdts: getCRDTSchemaPart() }, crdt: { field: crdts } };CRDT 插件的源码中集合通过crdt字段把增量操作合并进文档并使用storageToken标识操作创建者src/plugins/crdt/index.ts保证跨客户端、跨标签页的操作序列可确定性合并。Schema 校验与 TypeScript 支持Horizon 存储和返回纯 JavaScript 对象不做任何校验。客户端存错字段名或类型RethinkDB 照单全收脏数据还会经 changefeed 传播给所有其他客户端。RxDB 在写入存储前用 JSON Schema 校验每个文档。非法文档在写入步骤就被拒绝不会进入本地存储、也不会经复制扩散try { await db.messages.insert({ id: msg-001, // text 字段必填但缺失 roomId: room-42, createdAt: Date.now() }); } catch (err) { console.error(err); // Schema 校验错误缺少 text }RxDB 还会从 Schema 自动生成 TypeScript 类型。find()、insert()、upsert()等集合方法全部带类型数据库操作拥有 IDE 自动补全与编译期安全。Schema 迁移应用演进时数据模型会变新增必填字段、重命名属性、重构嵌套对象都需要更新既有存量文档。Horizon 没有迁移系统。改变数据形态后你得自己写脚本连上 RethinkDB 逐条手改框架零帮助。RxDB 内置 Schema 迁移系统。Schema 版本号递增时RxDB 会在数据库对应用可用之前自动对所有本地存量文档执行迁移策略await db.addCollections({ messages: { schema: messageSchemaV2, // version: 1 migrationStrategies: { 1: (oldDoc) { // 从 version 0 迁移为 threadId 补默认值 return { ...oldDoc, threadId: oldDoc.threadId ?? main }; } } } });迁移在每个客户端本地独立执行不需要协调的服务端发布也不需要手写数据库更新脚本。静态加密Horizon 通过 WebSocket 在浏览器与 RethinkDB 之间明文传输数据RethinkDB 中也原样存储。设备一旦被攻破浏览器会话遗留的 IndexedDB 数据无需解密即可读。RxDB 内置 加密插件在写入本地存储前对单个文档字段加密读取时解密应用看到的是明文而底层存储只有密文import { wrappedKeyEncryptionCryptoJsStorage } from rxdb/plugins/encryption-crypto-js; import { getRxStorageIndexedDB } from rxdb/plugins/storage-indexeddb; const db await createRxDatabase({ name: myapp, storage: wrappedKeyEncryptionCryptoJsStorage({ storage: getRxStorageIndexedDB() }), password: your-encryption-passphrase }); const schema { version: 0, primaryKey: id, type: object, properties: { id: { type: string, maxLength: 100 }, text: { type: string }, sensitiveData: { type: string } }, encrypted: [sensitiveData] // 在 IndexedDB 中以密文存储 };可观察的变更流Horizon 的watch()从服务端推送完整更新后的结果集。RxDB 在集合和文档两个层级都提供可观察的变更流在本地即可细粒度感知变更事件// 订阅 messages 集合的所有变更 db.messages.$.subscribe(changeEvent { console.log(Operation:, changeEvent.operation); // INSERT, UPDATE, DELETE console.log(Document ID:, changeEvent.documentId); console.log(Document data:, changeEvent.documentData); }); // 订阅某个具体文档的变更 const doc await db.messages.findOne(msg-001).exec(); doc.$.subscribe(updatedDoc { console.log(Document updated:, updatedDoc?.text); });这些事件源自本地数据库本地写入会触发经复制到达的文档也会触发离线时同样照常触发完全不依赖服务器连接。上手 RxDB安装 RxDB 与 RxJSnpm install rxdb rxjs创建数据库、添加集合、写入文档并订阅响应式查询import { createRxDatabase, addRxPlugin } from rxdb/plugins/core; import { RxDBDevModePlugin } from rxdb/plugins/dev-mode; import { getRxStorageIndexedDB } from rxdb/plugins/storage-indexeddb; addRxPlugin(RxDBDevModePlugin); const db await createRxDatabase({ name: chatapp, storage: getRxStorageIndexedDB() }); await db.addCollections({ messages: { schema: { title: message schema, version: 0, primaryKey: id, type: object, properties: { id: { type: string, maxLength: 100 }, text: { type: string }, roomId: { type: string }, createdAt: { type: number } }, required: [id, text, roomId, createdAt], indexes: [roomId, createdAt] } } }); // 写入一条消息 await db.messages.insert({ id: msg-001, text: Hello from RxDB!, roomId: room-42, createdAt: Date.now() }); // 响应式查询立即发射当前状态且每次变更都会重新发射 db.messages.find({ selector: { roomId: room-42 }, sort: [{ createdAt: asc }] }).$.subscribe(messages { console.log(Current messages:, messages.map(m m.text)); });以上流程完全离线可用需要服务端同步时再接上一个复制插件即可。对比总结维度HorizonRxDB项目状态2016 年起被放弃2016 年起持续维护运行位置客户端连接服务器客户端完整数据库离线支持无所有操作都需联网完整离线优先运行响应式查询watch()从 RethinkDB 推送本地存储上的 Observable 查询数据位置远端 RethinkDB 服务器本地存储IndexedDB、OPFS、SQLite后端依赖必须运行 Horizon RethinkDB任意后端或无需后端复制Horizon 专有协议可插拔HTTP、WebSocket、CouchDB、GraphQL、自定义冲突解决Last-write-wins静默覆盖可配置 handler 或基于 CRDT 合并Schema 校验无每次写入强制 JSON Schema 校验Schema 迁移手写脚本内置版本化迁移策略多标签页支持每标签页独立连接SharedWorker所有标签页共享状态静态加密无内置字段级加密插件TypeScript 支持无仅 JavaScript由 Schema 自动生成类型认证内置 OAuth Provider已过时交由既有后端处理无锁定权限Horizon 专属规则系统交由既有后端处理安全更新无已放弃随活跃开发持续跟进许可证Apache 2.0Apache 2.0常见问题QRxDB 能否替换基于 RethinkDB 的现有应用中的 Horizon可以。RxDB 接管客户端数据层RethinkDB 保留在服务端在其前面包一层薄 APIREST 或 WebSocket再用 RxDB 的 自定义复制 或 WebSocket 复制 插件同步客户端 RxDB 与服务端 RethinkDB 的数据。主要区别在于Horizon 本身就是完整的客户端-服务端协议而用 RxDB 时 API 层由你掌控。这让你完全掌握认证、限流和数据访问规则而不是受制于 Horizon 专属的权限模型。QRxDB 的响应式模型与 Horizon 的watch()API 相比如何Horizon 的watch()接入 RethinkDB 的 changefeed 系统并推送更新后的结果集——集合中任一文档变化Horizon 就把整个更新后的数组发给订阅者。RxDB 在 API 层面类似订阅查询立即得到当前结果集相关数据变化时 Observable 重新发射更新后的数组。区别在数据来源Horizon 从远端服务器拉取watch()需要实时连接RxDB 从本地存储发射响应式查询在线离线行为一致。RxDB 用 event-reduce 算法高效计算结果集更新src/event-reduce.ts、src/change-event-buffer.ts避免每次变更都重跑完整查询高写入负载下也能保持响应式更新流畅。QRxDB 能配合 React、Vue、Angular 等框架使用吗可以。RxDB 与框架无关其响应式查询返回 RxJS Observable可与任意框架集成。RxDB 为 React 提供便捷 hooksuseRxQuery、useRxDocument把 Observable 订阅封装进 React 状态模型Angular 可直接用 async pipe 消费 RxJS ObservableVue 中普通 Observable 订阅可与ref、reactive搭配。Horizon 在 API 层面同样与框架无关但其依赖早已过时不 fork 就很难与现代框架版本集成。QRxDB 在离线后如何重连RxDB 的复制插件持续运行并自动重试。网络不可用时 pull/push handler 失败RxDB 等待retryTime毫秒后重试默认 5000ms浏览器环境下还会监听online事件提前恢复见 src/plugins/replication/replication-helper.ts。网络恢复后复制从最近的成功检查点继续。写入不会丢失离线期间写入的文档保存在本地连接恢复后推送到服务器。UI 中的响应式查询全程保持最新——本地写入立即反映无需等待服务器确认。QRxDB 适合生产环境吗RxDB 自 2016 年以来持续活跃开发被多家公司用于生产应用并通过 premium 插件 建立可持续的商业模式以支撑长期维护。与 2016 年后毫无维护的 Horizon 不同RxDB 持续新增存储后端、修复浏览器兼容性问题并改进性能。赞分享数据库NoSQL嵌入式数据库实时数据库【免费下载链接】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点击查看免费下载相关推荐Cachex 实战疑难解答解决分布式缓存 9 大核心问题Cachex 实战疑难解答解决分布式缓存 9 大核心问题 引言你是否正被这些 Cachex 问题困扰 在高并发 Elixir 应用中Cachex 作为功数据库NoSQL嵌入式数据库实时数据库Terraform Stacks 运行时内部架构解析基于隐式数据流求值的声明式语言运行时Terraform Stacks 运行时内部架构解析基于隐式数据流求值的声明式语言运行时 本文面向 Terraform 的维护者与内核研究者系统剖析 Sta数据库NoSQL嵌入式数据库实时数据库RxDB 作为 PouchDB 的现代替代方案响应式查询、可插拔存储与无修订树开销的离线优先数据库RxDB 作为 PouchDB 的现代替代方案响应式查询、可插拔存储与无修订树开销的离线优先数据库 许多开发者起初因为 PouchDB 成熟的 CouchDB数据库NoSQL嵌入式数据库实时数据库上一篇gh_mirrors/fr/frontend-stuff AI 图像生成提示词库分类与搜索下一篇Magika置信度分数解析如何理解AI模型的预测可靠性指标创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考