跳到主要内容

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:

  • useGet
  • useFindOne
  • useFindOneOrFail
  • useFind
  • useFindByCursor
  • useFindAll
  • useCount

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 是本仓库的硬约束,框架惯例允许容器形态与命名差异,不允许能力缺失:

能力VueAngularReact
查询useGet / useFind / …同名同名
无限滚动useInfiniteScrolluseInfiniteScrolluseInfiniteScroll
异步操作useAction → ComputedRef<boolean>useAction → Signal<boolean>useAction → 渲染快照
持久化状态usePersistedState → Ref<T>usePersistedState / useState → WritableSignal<T>usePersistedState → 快照 + setValue
实体实时变更useEntityChangeRxDBEntityChangeDirective(markForCheck)useEntityChange

时间窗判定(withTimeWindows)与持久化内核(PersistedStateRegistry)都放在 @aiao/utils 里由三端共用,语义不会各自漂移。Angular 的 usePersistedState 是既有 useState 的扁平签名适配,与 Vue / React 侧键格式一致但各自持有内存状态。

完整示例​

仓库中的 apps/dev-rxdb-vue 展示了完整集成。

Fileoverview​

RxDB Vue 集成包 提供 RxDB 数据库的 Vue 3 Composition API 接口

Interfaces​

InterfaceDescription
ActionResource被包装后的异步操作。
EntityChangeOptionsuseEntityChange 的时间窗配置。
EntityChangeResource实体 patch 到 Vue 响应式系统的桥接结果。
GraphPath图路径结果
InfiniteScrollResource无限滚动资源。
NeighborResult图邻居查询结果(基础)
PersistedState某个 namespace + name 的持久化状态。
RxDBDependencyInjectionmakeRxDBDependencyInjector 产出的一组互相隔离的 provider / injector。
RxDBResource基础查询 hooks 返回的响应式资源。
SyncStateResourceuseSyncState 的返回值。

Type Aliases​

Type AliasDescription
GraphQueryResult带资源截断状态的图查询数组。
RxDBInputprovideRxDB 接受的数据库形态(Vue 超集)。
RxDBRefinjector 返回的只读数据库引用;undefined 表示数据库尚未就绪。
RxDBSourceprovideRxDB 接受的数据库形态:实例、Promise,或返回二者之一的工厂。
UseOptions查询选项的入参形态:可以是常量、getter、Ref 或 ComputedRef。

Variables​

VariableDescription
injectRxDB-
injectRxDBRef-
provideRxDB-

Functions​

FunctionDescription
makeRxDBDependencyInjector创建一组类型安全的 RxDB Vue 依赖注入函数。
useAction把一个异步函数包成带加载状态的可调用 action。
useCount统计匹配条件的实体数
useCountNeighbors统计图结构中的邻居实体数
useEntityChange把实体的 patch 流桥接到 Vue 响应式系统,让原地修改能实时反映到视图。
useFind查找匹配条件的多个实体
useFindAll查找所有实体
useFindByCursor使用游标分页查找实体
useFindOne查找匹配条件的单个实体
useFindOneOrFail查找单个实体,找不到则抛错
useGet通过 ID 获取单个实体
useGraphNeighbors查找图结构中的邻居实体
useGraphPaths查找图中两个实体之间的路径
useInfiniteScroll用于游标分页的无限滚动钩子。
usePersistedState创建(或复用)一份持久化到 localStorage 的命名空间状态。
useRepositoryQueryRxDB 仓库查询的核心钩子实现,管理响应式订阅和状态更新。
useRxDB获取当前已就绪的 RxDB 实例。
useRxDBOptional获取当前已就绪的 RxDB 实例,取不到时返回 undefined。
useRxDBRef获取已提供的 RxDB 响应式引用。
useSyncState读取当前数据库的同步状态。