rxdb-vue
@aiao/rxdb-vue 把 @aiao/rxdb 的实时查询接入 Vue 3 Composition API。该包提供依赖注入、响应式查询 hooks 和游标无限滚动,不包含 UI 组件。
安装
pnpm add @aiao/rxdb @aiao/rxdb-vue rxjs vue
vue >= 3.5 与 rxjs ^7.8 是 peer dependencies。
提供数据库
在会渲染业务组件的祖先组件中调用 provideRxDB:
<script lang="ts" setup>
import { provideRxDB } from '@aiao/rxdb-vue';
import { database } from './database';
import TodoList from './TodoList.vue';
provideRxDB(database);
</script>
<template>
<TodoList />
</template>
同步消费者可以使用:
import { injectRxDB, useRxDB, useRxDBOptional } from '@aiao/rxdb-vue';
const optionalDatabase = injectRxDB(); // RxDB | undefined;无 provider 时 dev 下有 Vue 告警
const maybe = useRxDBOptional(); // 同上,但缺 provider 不告警 —— 可选读取本就允许没有 provider
const database = useRxDB(); // 无 provider / 未就绪各抛一条不同文案;创建失败原样抛出创建异常
异步初始化
provideRxDB 收 RxDBInput:三端共享的 RxDBSource(实例、Promise,或返回二者之一的工厂)之外,Vue 还多收 Ref<RxDB | undefined> 与 undefined。
Promise / 工厂形态用于「后端按运行环境动态选择」这类场景 —— 静态 import 会把桌面分支打进 web bundle:
provideRxDB(async () => {
const { setupDesktop } = await import('./setup-desktop');
return setupDesktop();
});
Ref 形态是 Vue 独有的:由调用方自己持有、自己决定何时填上。
import type { RxDB } from '@aiao/rxdb';
import { provideRxDB, useRxDBRef } from '@aiao/rxdb-vue';
import { shallowRef } from 'vue';
const databaseRef = shallowRef<RxDB>();
provideRxDB(databaseRef);
const database = useRxDBRef();
// database.value 在初始化完成后更新
injectRxDBRef() 是可选版本;没有 provider 时返回 undefined。useInfiniteScroll 会在 provider 存在但数据库仍为 undefined 时等待,并在 ref 就绪后自动加载。
生命周期所有权:provider 只销毁自己造的东西。 传工厂或 Promise,实例由 provider 等来,作用域销毁时它负责 disconnectAll();传已就绪的实例或你自己的 Ref,它归调用方所有,provider 不碰 —— 否则一个模块级单例会被某个子组件的卸载顺手断掉,而没有人会去重连。这条规则三端逐字相同。
响应式查询
实体静态查询方法返回的 Observable 会驱动 Vue 响应式资源:
import { useCount, useFind, useGet } from '@aiao/rxdb-vue';
import { Todo } from './entities/Todo';
const todo = useGet(Todo, 'todo-1');
const todos = useFind(Todo, {
where: `{ combinator: 'and', rules: [] }`
});
const count = useCount(Todo, {
where: `{ combinator: 'and', rules: [] }`
});
每个查询资源公开:
value:最近一次成功结果;重新查询期间保留旧值(stale-while-revalidate);isLoading:是否有一轮查询正在进行;error:标准化后的Error | undefined;hasValue:value是否属于当前查询条件 —— 重新查询一开始就落回false,收到新值才为true;isEmpty:成功结果是否为空;未返回结果或错误时为undefined。
判断「当前值可信」请看 hasValue,不要用 value !== defaultValue。
资源是 reactive 对象,不要直接解构
基础查询 hooks 返回的是一个 reactive 对象,字段是普通响应式属性而不是 Ref。
直接解构只会读到一次性快照,之后永不更新:
const todo = useGet(Todo, 'todo-1'); // ✅ 整体持有,模板用 todo.value / todo.isLoading
const { value } = toRefs(useGet(Todo, 'todo-1')); // ✅ 先 toRefs 再解构
const { value } = useGet(Todo, 'todo-1'); // ❌ 一次性快照,Observable 后续发值也不会更新
useInfiniteScroll 的返回值形态不同(成员本身就是 ComputedRef,解构安全),两者不要互相类推。
查询参数
查询参数可直接传值,也可传 Ref、ComputedRef 或 getter(类型 UseOptions<T> 已导出)。
参数按内容比较而非按引用:reactive 对象原地改字段会触发重查,换成结构等价的新对象则不会重订阅。
代价是参数必须可序列化 —— 含函数、Symbol 或类实例时会抛 TypeError(游标分页的 after / before 实体除外,它们按 orderBy 字段投影后比较)。
重新查询开始时,isLoading 置为 true,error/hasValue/isEmpty 同步复位,value 保留旧值。
Angular 与 React 侧是同一份契约。
SSR 下(无 window)不会发起订阅,资源停在 isLoading: true 的初始态;客户端 hydrate 后正常查询。
订阅随组件卸载自动清理。
基础查询 API:
useGetuseFindOneuseFindOneOrFailuseFinduseFindByCursoruseFindAlluseCount
Tree API:useFindDescendants、useCountDescendants、useFindAncestors、useCountAncestors。
Graph API:useGraphNeighbors、useCountNeighbors、useGraphPaths。实体仍需安装并配置对应的 RxDB tree/graph 能力。
无限滚动
import { useInfiniteScroll } from '@aiao/rxdb-vue';
import { Todo } from './entities/Todo';
const list = useInfiniteScroll(Todo, {
where: { combinator: 'and', rules: [] },
orderBy: [
{ field: 'createdAt', sort: 'desc' },
`{ field: 'id', sort: 'desc' }`
],
limit: 50
});
list.loadMore();
list.refresh();
useInfiniteScroll 返回:
value: ComputedRef<T[]>isEmpty: ComputedRef<boolean>isLoading: ComputedRef<boolean>error: ComputedRef<Error | undefined>hasMore: ComputedRef<boolean>loadMore()与refresh()
字段全部是只读的 ComputedRef,可以安全解构。hasMore 由最后一页决定:页容量非正(limit <= 0 或 NaN)永远是 false,一次数据都没发就 complete 的流也收敛为 false,因此自动触底不会无界重发。error 原样透传底层 Error。
首屏在组件 mounted 后加载;后续页面自动带上上一页最后一个实体作为 after。options 或异步提供的数据库变化时,旧分页订阅会清理并从第一页重载。已加载页面的 live Observable 可以继续更新该页,但只有最新分页请求会结束全局 loading 或更新 hasMore。
异步操作
useAction 把一个异步函数包成带在途状态的 action:
<script lang="ts" setup>
import { useAction } from '@aiao/rxdb-vue';
const save = useAction((todo: Todo) => repository.save(todo));
</script>
<template>
<button :disabled="save.isPending.value" @click="save.execute(todo)">
`{{ save.isPending.value ? '保存中…' : '保存' }}`
</button>
</template>
isPending 是并发计数而不是布尔开关:N 次调用同时在途时它一直为真,直到最后一个 settle;计数在 finally 里回退,因此失败也会正确复位。有意不做去重与取消 —— 重复点击会真的执行多次,错误原样 reject 给调用方。
持久化状态
usePersistedState 把一份状态持久化到 localStorage,同 namespace + name 始终返回同一个 Ref:
import { usePersistedState } from '@aiao/rxdb-vue';
const theme = usePersistedState('my-app', 'theme', 'dark');
theme.value.value = 'light'; // 立即落盘到 'my-app:theme'
- 后续调用传入的
initialValue会被忽略,但仍参与类型标签校验 —— 同 key 换值类型直接抛错,而不是静默串型。 namespace与name在键里各自转义,不会互相串号;含:或%的旧键在首次读取时一次性迁移。- 是
shallowRef语义:对象原地改字段不触发响应式也不落盘,必须整体赋新值。 - 写盘失败不抛错,内存值照常更新,失败经
persistError暴露 —— 这是唯一能知道数据没落盘的途径。 - SSR 下不读也不写
localStorage,退化成纯内存值;暂不监听storage事件,因此不跨标签页同步。
实体实时变更
实体是原地可变的类实例,引用不变,Vue 的响应式系统看不到它们的字段变化。useEntityChange 把实体的 patches$ 接进渲染依赖:
<script lang="ts" setup>
import { useEntityChange } from '@aiao/rxdb-vue';
const props = defineProps<{ todo: Todo }>();
// 输入停止 200ms 后才重渲染
const live = useEntityChange(() => props.todo, { debounceTime: 200 });
</script>
<template>
<div>{{ live.value?.title }}</div>
</template>
模板必须经由 live.value 读实体,直接读原始实例的字段不会重渲染。debounceTime 与 auditTime 单位是毫秒,同时设置时串联生效(顺序 debounceTime → auditTime),仅正有限值生效:0、负值、NaN、Infinity 一律表示禁用,两者都禁用时 patch 同步透传。两个字段都接受响应式入参。
三端 API 对照
同功能同 API 是本仓库的硬约束,框架惯例允许容器形态与命名差异,不允许能力缺失:
| 能力 | Vue | Angular | React |
|---|---|---|---|
| 查询 | useGet / useFind / … | 同名 | 同名 |
| 无限滚动 | useInfiniteScroll | useInfiniteScroll | useInfiniteScroll |
| 异步操作 | useAction → ComputedRef<boolean> | useAction → Signal<boolean> | useAction → 渲染快照 |
| 持久化状态 | usePersistedState → Ref<T> | usePersistedState / useState → WritableSignal<T> | usePersistedState → 快照 + setValue |
| 实体实时变更 | useEntityChange | RxDBEntityChangeDirective(markForCheck) | useEntityChange |
时间窗判定(withTimeWindows)与持久化内核(PersistedStateRegistry)都放在 @aiao/utils 里由三端共用,语义不会各自漂移。Angular 的 usePersistedState 是既有 useState 的扁平签名适配,与 Vue / React 侧键格式一致但各自持有内存状态。
完整示例
仓库中的 apps/dev-rxdb-vue 展示了完整集成。
Fileoverview
RxDB Vue 集成包 提供 RxDB 数据库的 Vue 3 Composition API 接口
Interfaces
| Interface | Description |
|---|---|
| ActionResource | 被包装后的异步操作。 |
| EntityChangeOptions | useEntityChange 的时间窗配置。 |
| EntityChangeResource | 实体 patch 到 Vue 响应式系统的桥接结果。 |
| GraphPath | 图路径结果 |
| InfiniteScrollResource | 无限滚动资源。 |
| NeighborResult | 图邻居查询结果(基础) |
| PersistedState | 某个 namespace + name 的持久化状态。 |
| RxDBDependencyInjection | makeRxDBDependencyInjector 产出的一组互相隔离的 provider / injector。 |
| RxDBResource | 基础查询 hooks 返回的响应式资源。 |
| SyncStateResource | useSyncState 的返回值。 |
Type Aliases
| Type Alias | Description |
|---|---|
| GraphQueryResult | 带资源截断状态的图查询数组。 |
| RxDBInput | provideRxDB 接受的数据库形态(Vue 超集)。 |
| RxDBRef | injector 返回的只读数据库引用;undefined 表示数据库尚未就绪。 |
| RxDBSource | provideRxDB 接受的数据库形态:实例、Promise,或返回二者之一的工厂。 |
| UseOptions | 查询选项的入参形态:可以是常量、getter、Ref 或 ComputedRef。 |
Variables
| Variable | Description |
|---|---|
| injectRxDB | - |
| injectRxDBRef | - |
| provideRxDB | - |
Functions
| Function | Description |
|---|---|
| makeRxDBDependencyInjector | 创建一组类型安全的 RxDB Vue 依赖注入函数。 |
| useAction | 把一个异步函数包成带加载状态的可调用 action。 |
| useCount | 统计匹配条件的实体数 |
| useCountNeighbors | 统计图结构中的邻居实体数 |
| useEntityChange | 把实体的 patch 流桥接到 Vue 响应式系统,让原地修改能实时反映到视图。 |
| useFind | 查找匹配条件的多个实体 |
| useFindAll | 查找所有实体 |
| useFindByCursor | 使用游标分页查找实体 |
| useFindOne | 查找匹配条件的单个实体 |
| useFindOneOrFail | 查找单个实体,找不到则抛错 |
| useGet | 通过 ID 获取单个实体 |
| useGraphNeighbors | 查找图结构中的邻居实体 |
| useGraphPaths | 查找图中两个实体之间的路径 |
| useInfiniteScroll | 用于游标分页的无限滚动钩子。 |
| usePersistedState | 创建(或复用)一份持久化到 localStorage 的命名空间状态。 |
| useRepositoryQuery | RxDB 仓库查询的核心钩子实现,管理响应式订阅和状态更新。 |
| useRxDB | 获取当前已就绪的 RxDB 实例。 |
| useRxDBOptional | 获取当前已就绪的 RxDB 实例,取不到时返回 undefined。 |
| useRxDBRef | 获取已提供的 RxDB 响应式引用。 |
| useSyncState | 读取当前数据库的同步状态。 |