Angular 集成
@aiao/rxdb-angular 提供了 Angular 集成,包括服务注入、响应式信号 (Signals) 和指令,让你在 Angular 应用中轻松使用 RxDB。
安装
- npm
- Yarn
- pnpm
- Bun
npm install @aiao/rxdb @aiao/rxdb-angular
yarn add @aiao/rxdb @aiao/rxdb-angular
pnpm add @aiao/rxdb @aiao/rxdb-angular
bun add @aiao/rxdb @aiao/rxdb-angular
核心概念
提供 RxDB 实例
使用 provideRxDB 在应用或模块级别提供 RxDB 实例:
import { ApplicationConfig } from '@angular/core';
import { provideRxDB } from '@aiao/rxdb-angular';
import { RxDB, SyncType } from '@aiao/rxdb';
import { Todo } from './entities/Todo';
// 创建 RxDB 实例
const rxdb = new RxDB({
dbName: 'myapp',
entities: [Todo],
sync: { type: SyncType.None, local: { adapter: 'sqlite' } }
});
export const appConfig: ApplicationConfig = {
providers: [
provideRxDB(rxdb)
// 其他 providers
]
};
RxDBService
使用 RxDBService 在组件中注入 RxDB 实例:
import { Component, inject } from '@angular/core';
import { RxDBService } from '@aiao/rxdb-angular';
@Component({
selector: 'app-root',
template: `...`
})
export class AppComponent {
private rxdbService = inject(RxDBService);
ngOnInit() {
const rxdb = this.rxdbService.rxdb;
// 使用 rxdb
}
}
Hooks API
@aiao/rxdb-angular 提供了一系列基于 Angular Signals 的响应式 Hooks。
RxDBResource 接口
所有 Hooks 返回一个 RxDBResource 对象,包含以下信号:
interface RxDBResource<T> {
value: Signal<T>; // 查询结果值
error: Signal<Error | undefined>; // 错误信息
isLoading: Signal<boolean>; // 加载状态
isEmpty: Signal<boolean | undefined>; // 是否为空
hasValue: Signal<boolean>; // 是否有值
}
基础查询 Hooks
useGet
根据 ID 获取单个实体:
import { Component } from '@angular/core';
import { useGet } from '@aiao/rxdb-angular';
import { Todo } from './entities/Todo';
@Component({
selector: 'app-todo-detail',
template: `
@if (isLoading()) {
<div>加载中...</div>
} @else if (error()) {
<div>错误: {{ error()?.message }}</div>
} @else if (!todo()) {
<div>未找到</div>
} @else {
<div>{{ todo()?.title }}</div>
}
`
})
export class TodoDetailComponent {
id = input.required<string>();
private resource = useGet(Todo, () => ({ id: this.id() }));
todo = this.resource.value;
isLoading = this.resource.isLoading;
error = this.resource.error;
}
useFindOne
查找符合条件的第一个实体:
import { Component } from '@angular/core';
import { useFindOne } from '@aiao/rxdb-angular';
import { Todo } from './entities/Todo';
@Component({
selector: 'app-latest-todo',
template: `
@if (isLoading()) {
<div>加载中...</div>
} @else {
<div>{{ todo()?.title || '没有未完成的待办' }}</div>
}
`
})
export class LatestTodoComponent {
private resource = useFindOne(Todo, {
where: { completed: false },
orderBy: [{ field: 'createdAt', sort: 'desc' }]
});
todo = this.resource.value;
isLoading = this.resource.isLoading;
}
useFindOneOrFail
类似 useFindOne,但找不到时会抛出错误:
import { Component } from '@angular/core';
import { useFindOneOrFail } from '@aiao/rxdb-angular';
import { Todo } from './entities/Todo';
@Component({
selector: 'app-required-todo',
template: `
@if (error()) {
<div>未找到匹配的待办事项</div>
} @else {
<div>{{ todo()?.title }}</div>
}
`
})
export class RequiredTodoComponent {
filter = input.required<any>();
private resource = useFindOneOrFail(Todo, () => ({
where: this.filter()
}));
todo = this.resource.value;
error = this.resource.error;
}
useFind
查找多个符合条件的实体:
import { Component } from '@angular/core';
import { useFind } from '@aiao/rxdb-angular';
import { Todo } from './entities/Todo';
@Component({
selector: 'app-todo-list',
template: `
@if (isLoading()) {
<div>加载中...</div>
} @else if (isEmpty()) {
<div>暂无待办事项</div>
} @else {
<ul>
@for (todo of todos(); track todo.id) {
<li>{{ todo.title }}</li>
}
</ul>
}
`
})
export class TodoListComponent {
private resource = useFind(Todo, {
where: {
combinator: 'and',
rules: [{ field: 'completed', operator: '=', value: false }]
},
orderBy: [{ field: 'createdAt', sort: 'desc' }],
limit: 20
});
todos = this.resource.value;
isLoading = this.resource.isLoading;
isEmpty = this.resource.isEmpty;
}
useFindByCursor
使用游标分页查找实体:
import { Component, signal } from '@angular/core';
import { useFindByCursor } from '@aiao/rxdb-angular';
import { Todo } from './entities/Todo';
@Component({
selector: 'app-paginated-todos',
template: `
@if (isLoading()) {
<div>加载中...</div>
} @else {
<ul>
@for (todo of todos(); track todo.id) {
<li>{{ todo.title }}</li>
}
</ul>
<button (click)="loadMore()" [disabled]="!hasMore()">
加载更多
</button>
}
`
})
export class PaginatedTodosComponent {
private cursor = signal<string | undefined>(undefined);
private resource = useFindByCursor(Todo, () => ({
cursor: this.cursor(),
limit: 20,
orderBy: [{ field: 'createdAt', sort: 'desc' }]
}));
todos = this.resource.value;
isLoading = this.resource.isLoading;
hasMore() {
const todos = this.todos();
return todos.length > 0 && todos.length % 20 === 0;
}
loadMore() {
const todos = this.todos();
if (todos.length > 0) {
this.cursor.set(todos[todos.length - 1].id);
}
}
}
useFindAll
查找所有实体:
import { Component } from '@angular/core';
import { useFindAll } from '@aiao/rxdb-angular';
import { Todo } from './entities/Todo';
@Component({
selector: 'app-all-todos',
template: `
@if (isLoading()) {
<div>加载中...</div>
} @else {
<ul>
@for (todo of todos(); track todo.id) {
<li>{{ todo.title }}</li>
}
</ul>
}
`
})
export class AllTodosComponent {
private resource = useFindAll(Todo, {
orderBy: [{ field: 'createdAt', sort: 'desc' }]
});
todos = this.resource.value;
isLoading = this.resource.isLoading;
}
useCount
统计符合条件的实体数量:
import { Component } from '@angular/core';
import { useCount } from '@aiao/rxdb-angular';
import { Todo } from './entities/Todo';
@Component({
selector: 'app-todo-stats',
template: `
<div>
<p>总计: {{ totalCount() }}</p>
<p>已完成: {{ completedCount() }}</p>
<p>未完成: {{ pendingCount() }}</p>
</div>
`
})
export class TodoStatsComponent {
totalCount = useCount(Todo, {}).value;
completedCount = useCount(Todo, { where: { completed: true } }).value;
pendingCount = useCount(Todo, { where: { completed: false } }).value;
}
树结构 Hooks
用于查询树形结构的实体(使用 @TreeEntity 定义)。
useFindDescendants
查找所有后代节点:
import { Component } from '@angular/core';
import { useFindDescendants } from '@aiao/rxdb-angular';
import { Menu } from './entities/Menu';
@Component({
selector: 'app-menu-tree',
template: `
@if (isLoading()) {
<div>加载中...</div>
} @else {
<ul>
@for (item of descendants(); track item.id) {
<li>{{ item.name }}</li>
}
</ul>
}
`
})
export class MenuTreeComponent {
rootId = input.required<string>();
private resource = useFindDescendants(Menu, () => ({
id: this.rootId(),
depth: 3
}));
descendants = this.resource.value;
isLoading = this.resource.isLoading;
}
useCountDescendants
统计后代节点数量:
import { Component } from '@angular/core';
import { useCountDescendants } from '@aiao/rxdb-angular';
import { Menu } from './entities/Menu';
@Component({
selector: 'app-menu-item-count',
template: `<span>子项数量: {{ count() }}</span>`
})
export class MenuItemCountComponent {
id = input.required<string>();
count = useCountDescendants(Menu, () => ({ id: this.id() })).value;
}
useFindAncestors
查找所有祖先节点:
import { Component } from '@angular/core';
import { useFindAncestors } from '@aiao/rxdb-angular';
import { Menu } from './entities/Menu';
@Component({
selector: 'app-breadcrumb',
template: `
<nav>
@for (item of ancestors(); track item.id; let i = $index) {
@if (i > 0) { > }
<span>{{ item.name }}</span>
}
</nav>
`
})
export class BreadcrumbComponent {
currentId = input.required<string>();
ancestors = useFindAncestors(Menu, () => ({
id: this.currentId()
})).value;
}
useCountAncestors
统计祖先节点数量:
import { Component } from '@angular/core';
import { useCountAncestors } from '@aiao/rxdb-angular';
import { Menu } from './entities/Menu';
@Component({
selector: 'app-menu-level',
template: `<span>层级: {{ level() }}</span>`
})
export class MenuLevelComponent {
id = input.required<string>();
level = useCountAncestors(Menu, () => ({ id: this.id() })).value;
}
动态选项
所有 Hooks 都支持函数式选项,可以基于信号动态计算查询参数:
import { Component, signal } from '@angular/core';
import { useFind } from '@aiao/rxdb-angular';
import { Todo } from './entities/Todo';
@Component({
selector: 'app-dynamic-todo-list',
template: `
<input [value]="filter()" (input)="filter.set($any($event.target).value)" />
<ul>
@for (todo of todos(); track todo.id) {
<li>{{ todo.title }}</li>
}
</ul>
`
})
export class DynamicTodoListComponent {
filter = signal('');
private resource = useFind(Todo, () => ({
where: {
combinator: 'and',
rules: [
{ field: 'title', operator: 'like', value: `%${this.filter()}%` }
]
}
}));
todos = this.resource.value;
}
当 filter 信号变化时,Hook 会自动重新订阅并更新数据。
变更检测指令
RxDBEntityChangeDirective 可以自动触发变更检测:
import { Component } from '@angular/core';
import { RxDBEntityChangeDirective } from '@aiao/rxdb-angular';
import { Todo } from './entities/Todo';
@Component({
selector: 'app-todo-item',
standalone: true,
imports: [RxDBEntityChangeDirective],
template: `
<div [rxdbEntityChange]="todo">
<h3>{{ todo.title }}</h3>
<p>完成状态: {{ todo.completed }}</p>
</div>
`
})
export class TodoItemComponent {
todo = input.required<Todo>();
}
当实体数据变化时,指令会自动触发 Angular 的变更检测。
完整示例
import { Component, signal } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { useFind, useCount } from '@aiao/rxdb-angular';
import { Todo } from './entities/Todo';
type FilterType = 'all' | 'active' | 'completed';
@Component({
selector: 'app-todo-app',
standalone: true,
imports: [CommonModule, FormsModule],
template: `
<div class="todo-app">
<h1>待办事项 ({{ activeCount() }}/{{ totalCount() }})</h1>
<div class="todo-input">
<input
[(ngModel)]="newTodoTitle"
(keyup.enter)="handleAdd()"
placeholder="添加新待办..."
/>
<button (click)="handleAdd()">添加</button>
</div>
<div class="filters">
<button
[class.active]="filter() === 'all'"
(click)="filter.set('all')"
>
全部
</button>
<button
[class.active]="filter() === 'active'"
(click)="filter.set('active')"
>
未完成
</button>
<button
[class.active]="filter() === 'completed'"
(click)="filter.set('completed')"
>
已完成
</button>
</div>
@if (isLoading()) {
<div>加载中...</div>
} @else {
<ul class="todo-list">
@for (todo of todos(); track todo.id) {
<li class="todo-item">
<input
type="checkbox"
[checked]="todo.completed"
(change)="handleToggle(todo)"
/>
<span [class.completed]="todo.completed">
{{ todo.title }}
</span>
<button (click)="handleDelete(todo)">删除</button>
</li>
}
</ul>
}
</div>
`,
styles: [`
.todo-app {
max-width: 600px;
margin: 0 auto;
padding: 20px;
}
.todo-input {
display: flex;
gap: 10px;
margin-bottom: 20px;
}
.todo-input input {
flex: 1;
padding: 8px;
border: 1px solid #ddd;
border-radius: 4px;
}
.filters {
display: flex;
gap: 10px;
margin-bottom: 20px;
}
.filters button {
padding: 8px 16px;
border: 1px solid #ddd;
background: white;
border-radius: 4px;
cursor: pointer;
}
.filters button.active {
background: #007bff;
color: white;
border-color: #007bff;
}
.todo-list {
list-style: none;
padding: 0;
}
.todo-item {
display: flex;
align-items: center;
gap: 10px;
padding: 10px;
border-bottom: 1px solid #eee;
}
.todo-item span {
flex: 1;
}
.todo-item span.completed {
text-decoration: line-through;
color: #999;
}
.todo-item button {
padding: 4px 8px;
background: #dc3545;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
`]
})
export class TodoAppComponent {
filter = signal<FilterType>('all');
newTodoTitle = '';
private todosResource = useFind(Todo, () => ({
where: this.filter() === 'all'
? undefined
: { completed: this.filter() === 'completed' },
orderBy: [{ field: 'createdAt', sort: 'desc' }]
}));
todos = this.todosResource.value;
isLoading = this.todosResource.isLoading;
totalCount = useCount(Todo, {}).value;
activeCount = useCount(Todo, { where: { completed: false } }).value;
async handleAdd() {
if (!this.newTodoTitle.trim()) return;
const todo = new Todo();
todo.title = this.newTodoTitle;
await todo.save();
this.newTodoTitle = '';
}
async handleToggle(todo: Todo) {
todo.completed = !todo.completed;
await todo.save();
}
async handleDelete(todo: Todo) {
await todo.remove();
}
}
类型安全
所有 Hooks 都是完全类型安全的,TypeScript 会自动推断实体类型和查询选项:
// TypeScript 会自动推断 todo 的类型为 Signal<Todo | undefined>
const { value: todo } = useGet(Todo, { id: '123' });
// TypeScript 会验证查询选项的有效性
const { value: todos } = useFind(Todo, {
where: {
combinator: 'and',
rules: [
{ field: 'title', operator: 'like', value: '%test%' },
{ field: 'completed', operator: '=', value: true }
]
},
// TypeScript 会提示可用的字段和操作符
orderBy: [{ field: 'createdAt', sort: 'desc' }]
});
性能优化
使用 computed 优化查询选项
import { Component, signal, computed } from '@angular/core';
import { useFind } from '@aiao/rxdb-angular';
import { Todo } from './entities/Todo';
@Component({
selector: 'app-optimized-list',
template: `...`
})
export class OptimizedListComponent {
filter = signal('');
private options = computed(() => ({
where: {
combinator: 'and',
rules: [
{ field: 'title', operator: 'like', value: `%${this.filter()}%` }
]
}
}));
private resource = useFind(Todo, this.options);
todos = this.resource.value;
}
OnPush 变更检测策略
与 Signals 配合使用 OnPush 策略以获得最佳性能:
import { Component, ChangeDetectionStrategy } from '@angular/core';
import { useFind } from '@aiao/rxdb-angular';
import { Todo } from './entities/Todo';
@Component({
selector: 'app-todo-list',
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
@for (todo of todos(); track todo.id) {
<div>{{ todo.title }}</div>
}
`
})
export class TodoListComponent {
todos = useFind(Todo, {
orderBy: [{ field: 'createdAt', sort: 'desc' }]
}).value;
}
注意事项
- 生命周期管理:Hooks 会自动管理订阅的生命周期,在组件销毁时自动取消订阅
- Signals 响应式:所有返回的属性都是 Angular Signals,可以直接在模板中使用
- 错误处理:始终检查
error()信号来处理查询错误 - 加载状态:使用
isLoading()来显示加载状态 - 空状态:使用
isEmpty()来判断查询结果是否为空 - 变更检测:使用 OnPush 策略和 Signals 可以获得最佳性能
- 函数式选项:使用函数式选项或
computed来创建动态查询