RxDB 作为 Horizon 的替代方案:离线优先、客户端响应式数据库实战指南
2026/9/20 23:43:24 网站建设 项目流程
  • 数据库
  • NoSQL
  • 嵌入式数据库
  • 实时数据库

【免费下载链接】rxdb

The 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 校验与迁移、静态加密等方面的对等乃至超越能力,帮助团队完成从"在线订阅式架构"到"本地优先数据库"的迁移。


什么是 Horizon?

Horizon(又称 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.ts,replicateRxCollection()的默认参数正是live = trueretryTime = 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 AppSync
WebSocket 复制低延迟服务端推送
CouchDB 复制CouchDB 或 PouchDB 服务器
Firestore 复制Google Cloud Firestore

可插拔的存储后端

RxDB 的存储层与查询、复制逻辑分离。同一套应用代码,可根据运行环境切换不同的存储引擎:

环境存储方案
浏览器(标准)IndexedDB
浏览器(高吞吐)OPFS(Origin Private File System)
React Native / Expo基于 expo-sqlite 或 op-sqlite 的 SQLite
Node.js / ElectronSQLite(better-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 与 RxJS:

npm 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

常见问题

Q:RxDB 能否替换基于 RethinkDB 的现有应用中的 Horizon?

可以。RxDB 接管客户端数据层,RethinkDB 保留在服务端,在其前面包一层薄 API(REST 或 WebSocket),再用 RxDB 的 自定义复制 或 WebSocket 复制 插件同步客户端 RxDB 与服务端 RethinkDB 的数据。

主要区别在于:Horizon 本身就是完整的客户端-服务端协议,而用 RxDB 时 API 层由你掌控。这让你完全掌握认证、限流和数据访问规则,而不是受制于 Horizon 专属的权限模型。

Q:RxDB 的响应式模型与 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),避免每次变更都重跑完整查询,高写入负载下也能保持响应式更新流畅。

Q:RxDB 能配合 React、Vue、Angular 等框架使用吗?

可以。RxDB 与框架无关,其响应式查询返回 RxJS Observable,可与任意框架集成。RxDB 为 React 提供便捷 hooks(useRxQueryuseRxDocument),把 Observable 订阅封装进 React 状态模型;Angular 可直接用 async pipe 消费 RxJS Observable;Vue 中普通 Observable 订阅可与refreactive搭配。

Horizon 在 API 层面同样与框架无关,但其依赖早已过时,不 fork 就很难与现代框架版本集成。

Q:RxDB 在离线后如何重连?

RxDB 的复制插件持续运行并自动重试。网络不可用时 pull/push handler 失败,RxDB 等待retryTime毫秒后重试(默认 5000ms,浏览器环境下还会监听online事件提前恢复,见 src/plugins/replication/replication-helper.ts)。网络恢复后,复制从最近的成功检查点继续。写入不会丢失:离线期间写入的文档保存在本地,连接恢复后推送到服务器。UI 中的响应式查询全程保持最新——本地写入立即反映,无需等待服务器确认。

Q:RxDB 适合生产环境吗?

RxDB 自 2016 年以来持续活跃开发,被多家公司用于生产应用,并通过 premium 插件 建立可持续的商业模式以支撑长期维护。与 2016 年后毫无维护的 Horizon 不同,RxDB 持续新增存储后端、修复浏览器兼容性问题并改进性能。

  • 数据库
  • NoSQL
  • 嵌入式数据库
  • 实时数据库

【免费下载链接】rxdb

The 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
点击查看免费下载

相关推荐

上一篇:gh_mirrors/fr/frontend-stuff AI 图像生成提示词库:分类与搜索
下一篇:Magika置信度分数解析:如何理解AI模型的预测可靠性指标

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询