跳到主要内容

Todo 应用示例

这是一个完整的 Todo 应用示例,展示了如何使用 RxDB 实现常见的 CRUD 操作、分页查询、实时更新等功能。

在线演示

项目还在开发中,可以通过以下方式本地运行:

pnpm nx serve dev-rxdb-angular

完整代码示例

1. 定义 Todo 实体

import { Entity, EntityBase, PropertyType } from '@aiao/rxdb';

@Entity({
name: 'Todo',
properties: [
{ name: 'title', type: PropertyType.string, required: true },
{ name: 'completed', type: PropertyType.boolean, default: false },
{ name: 'description', type: PropertyType.string },
{ name: 'createdAt', type: PropertyType.date, default: () => new Date() },
{ name: 'updatedAt', type: PropertyType.date }
],
indexes: [
{ columns: ['completed'] },
{ columns: ['createdAt'] }
]
})
export class Todo extends EntityBase {}

2. 初始化数据库

import { RxDB, SyncType } from '@aiao/rxdb';
import { RxDBAdapterSqlite } from '@aiao/rxdb-adapter-sqlite';
import { checkOPFSAvailable } from '@aiao/utils';

async function initDatabase() {
const rxdb = new RxDB({
dbName: 'todo-app',
entities: [Todo],
sync: {
local: { adapter: 'sqlite' },
type: SyncType.None
}
});

// 注册 SQLite 适配器
rxdb.adapter('sqlite', async db => {
const available = await checkOPFSAvailable();

return new RxDBAdapterSqlite(db, {
vfs: available ? 'OPFSCoopSyncVFS' : 'IDBBatchAtomicVFS',
worker: available,
workerInstance: available
? new Worker(new URL('./sqlite.worker', import.meta.url), {
type: 'module',
name: 'rxdb-worker'
})
: undefined,
sharedWorker: !available,
sharedWorkerInstance: !available
? new SharedWorker(new URL('./sqlite-shared.worker', import.meta.url), {
type: 'module',
name: 'rxdb-shared-worker'
})
: undefined,
wasmPath: available
? '/wa-sqlite/wa-sqlite.wasm'
: '/wa-sqlite/wa-sqlite-async.wasm'
});
});

// 连接数据库
await rxdb.connect('sqlite').toPromise();

return rxdb;
}

3. CRUD 操作

创建 Todo

async function createTodo(title: string, description?: string) {
const todo = new Todo();
todo.title = title;
todo.description = description;
todo.completed = false;
todo.createdAt = new Date();

await todo.save();
return todo;
}

// 使用
const newTodo = await createTodo('学习 RxDB', '阅读文档并完成示例');
console.log('创建的 Todo:', newTodo.id);

查询 Todo

import { firstValueFrom, switchMap } from 'rxjs';

// 查询所有 Todo
async function getAllTodos() {
return await rxdb.pipe(
switchMap(() => Todo.findAll({
where: { combinator: 'and', rules: [] },
orderBy: [{ field: 'createdAt', sort: 'desc' }]
})),
firstValueFrom
);
}

// 查询未完成的 Todo
async function getIncompleteTodos() {
return await rxdb.pipe(
switchMap(() => Todo.find({
where: {
combinator: 'and',
rules: [
{ field: 'completed', operator: '=', value: false }
]
},
orderBy: [{ field: 'createdAt', sort: 'desc' }],
limit: 20
})),
firstValueFrom
);
}

// 搜索 Todo
async function searchTodos(keyword: string) {
return await rxdb.pipe(
switchMap(() => Todo.find({
where: {
combinator: 'or',
rules: [
{ field: 'title', operator: 'contains', value: keyword },
{ field: 'description', operator: 'contains', value: keyword }
]
},
orderBy: [{ field: 'createdAt', sort: 'desc' }]
})),
firstValueFrom
);
}

更新 Todo

async function toggleTodo(todo: Todo) {
todo.completed = !todo.completed;
todo.updatedAt = new Date();
await todo.save();
return todo;
}

async function updateTodo(todo: Todo, updates: Partial<Todo>) {
Object.assign(todo, updates);
todo.updatedAt = new Date();
await todo.save();
return todo;
}

// 使用
const todo = await Todo.get('todo-id');
await toggleTodo(todo);

删除 Todo

async function deleteTodo(todo: Todo) {
await todo.remove();
}

// 批量删除已完成的 Todo
async function deleteCompletedTodos() {
const completed = await rxdb.pipe(
switchMap(() => Todo.find({
where: {
combinator: 'and',
rules: [
{ field: 'completed', operator: '=', value: true }
]
}
})),
firstValueFrom
);

for (const todo of completed) {
await todo.remove();
}
}

4. 统计与分页

// 获取统计信息
async function getTodoStats() {
const [total, completed, incomplete] = await Promise.all([
rxdb.pipe(
switchMap(() => Todo.count({
where: { combinator: 'and', rules: [] }
})),
firstValueFrom
),
rxdb.pipe(
switchMap(() => Todo.count({
where: {
combinator: 'and',
rules: [{ field: 'completed', operator: '=', value: true }]
}
})),
firstValueFrom
),
rxdb.pipe(
switchMap(() => Todo.count({
where: {
combinator: 'and',
rules: [{ field: 'completed', operator: '=', value: false }]
}
})),
firstValueFrom
)
]);

return { total, completed, incomplete };
}

// 分页查询
async function getTodosByPage(page: number, pageSize: number, completed?: boolean) {
const rules: any[] = [];
if (completed !== undefined) {
rules.push({ field: 'completed', operator: '=', value: completed });
}

const where = { combinator: 'and' as const, rules };

const [items, total] = await Promise.all([
rxdb.pipe(
switchMap(() => Todo.find({
where,
orderBy: [{ field: 'createdAt', sort: 'desc' }],
limit: pageSize,
offset: (page - 1) * pageSize
})),
firstValueFrom
),
rxdb.pipe(
switchMap(() => Todo.count({ where })),
firstValueFrom
)
]);

return {
items,
total,
page,
pageSize,
totalPages: Math.ceil(total / pageSize)
};
}

5. 实时订阅

import { Observable } from 'rxjs';

// 订阅所有未完成的 Todo
function watchIncompleteTodos(): Observable<Todo[]> {
return rxdb.pipe(
switchMap(() => Todo.find({
where: {
combinator: 'and',
rules: [
{ field: 'completed', operator: '=', value: false }
]
},
orderBy: [{ field: 'createdAt', sort: 'desc' }]
}))
);
}

// 在组件中使用
const subscription = watchIncompleteTodos().subscribe(todos => {
console.log('未完成的 Todo:', todos);
// 更新 UI
});

// 清理订阅
// subscription.unsubscribe();

React 示例

import { useEffect, useState } from 'react';
import { firstValueFrom, switchMap } from 'rxjs';

function TodoList() {
const [todos, setTodos] = useState<Todo[]>([]);
const [newTitle, setNewTitle] = useState('');

// 加载 Todo 列表
useEffect(() => {
const subscription = rxdb.pipe(
switchMap(() => Todo.find({
where: { combinator: 'and', rules: [] },
orderBy: [{ field: 'createdAt', sort: 'desc' }]
}))
).subscribe(setTodos);

return () => subscription.unsubscribe();
}, []);

// 添加 Todo
const handleAdd = async () => {
if (!newTitle.trim()) return;

const todo = new Todo();
todo.title = newTitle;
todo.completed = false;
await todo.save();

setNewTitle('');
};

// 切换完成状态
const handleToggle = async (todo: Todo) => {
todo.completed = !todo.completed;
await todo.save();
};

// 删除 Todo
const handleDelete = async (todo: Todo) => {
await todo.remove();
};

return (
<div>
<h1>Todo List</h1>

<div>
<input
type="text"
value={newTitle}
onChange={e => setNewTitle(e.target.value)}
placeholder="输入待办事项..."
/>
<button onClick={handleAdd}>添加</button>
</div>

<ul>
{todos.map(todo => (
<li key={todo.id}>
<input
type="checkbox"
checked={todo.completed}
onChange={() => handleToggle(todo)}
/>
<span style={{ textDecoration: todo.completed ? 'line-through' : 'none' }}>
{todo.title}
</span>
<button onClick={() => handleDelete(todo)}>删除</button>
</li>
))}
</ul>
</div>
);
}

Angular 示例

import { Component, OnInit, OnDestroy } from '@angular/core';
import { Observable, Subscription } from 'rxjs';
import { switchMap } from 'rxjs/operators';

@Component({
selector: 'app-todo-list',
template: `
<div>
<h1>Todo List</h1>

<div>
<input
type="text"
[(ngModel)]="newTitle"
placeholder="输入待办事项..."
/>
<button (click)="addTodo()">添加</button>
</div>

<ul>
<li *ngFor="let todo of todos$ | async">
<input
type="checkbox"
[checked]="todo.completed"
(change)="toggleTodo(todo)"
/>
<span [style.text-decoration]="todo.completed ? 'line-through' : 'none'">
{{ todo.title }}
</span>
<button (click)="deleteTodo(todo)">删除</button>
</li>
</ul>
</div>
`
})
export class TodoListComponent implements OnInit, OnDestroy {
todos$!: Observable<Todo[]>;
newTitle = '';
private subscription?: Subscription;

ngOnInit() {
this.todos$ = rxdb.pipe(
switchMap(() => Todo.find({
where: { combinator: 'and', rules: [] },
orderBy: [{ field: 'createdAt', sort: 'desc' }]
}))
);
}

ngOnDestroy() {
this.subscription?.unsubscribe();
}

async addTodo() {
if (!this.newTitle.trim()) return;

const todo = new Todo();
todo.title = this.newTitle;
todo.completed = false;
await todo.save();

this.newTitle = '';
}

async toggleTodo(todo: Todo) {
todo.completed = !todo.completed;
await todo.save();
}

async deleteTodo(todo: Todo) {
await todo.remove();
}
}

Vue 示例

<template>
<div>
<h1>Todo List</h1>

<div>
<input
v-model="newTitle"
type="text"
placeholder="输入待办事项..."
@keyup.enter="addTodo"
/>
<button @click="addTodo">添加</button>
</div>

<ul>
<li v-for="todo in todos" :key="todo.id">
<input
type="checkbox"
:checked="todo.completed"
@change="toggleTodo(todo)"
/>
<span :style="{ textDecoration: todo.completed ? 'line-through' : 'none' }">
{{ todo.title }}
</span>
<button @click="deleteTodo(todo)">删除</button>
</li>
</ul>
</div>
</template>

<script setup lang="ts">
import { ref, onMounted, onUnmounted } from 'vue';
import { Subscription } from 'rxjs';
import { switchMap } from 'rxjs/operators';

const todos = ref<Todo[]>([]);
const newTitle = ref('');
let subscription: Subscription;

onMounted(() => {
subscription = rxdb.pipe(
switchMap(() => Todo.find({
where: { combinator: 'and', rules: [] },
orderBy: [{ field: 'createdAt', sort: 'desc' }]
}))
).subscribe(value => {
todos.value = value;
});
});

onUnmounted(() => {
subscription?.unsubscribe();
});

async function addTodo() {
if (!newTitle.value.trim()) return;

const todo = new Todo();
todo.title = newTitle.value;
todo.completed = false;
await todo.save();

newTitle.value = '';
}

async function toggleTodo(todo: Todo) {
todo.completed = !todo.completed;
await todo.save();
}

async function deleteTodo(todo: Todo) {
await todo.remove();
}
</script>

功能扩展

1. 添加优先级

@Entity({
name: 'Todo',
properties: [
{ name: 'title', type: PropertyType.string, required: true },
{ name: 'completed', type: PropertyType.boolean, default: false },
{ name: 'priority', type: PropertyType.string, default: 'medium' },
{ name: 'dueDate', type: PropertyType.date }
],
indexes: [
{ columns: ['priority', 'completed'] },
{ columns: ['createdAt'] }
]
})
export class Todo extends EntityBase {}

// 按优先级查询
async function getTodosByPriority(priority: string) {
return await rxdb.pipe(
switchMap(() => Todo.find({
where: {
combinator: 'and',
rules: [
{ field: 'priority', operator: '=', value: priority },
{ field: 'completed', operator: '=', value: false }
]
},
orderBy: [{ field: 'dueDate', sort: 'asc' }]
})),
firstValueFrom
);
}

2. 添加分类标签

@Entity({
name: 'Tag',
properties: [
{ name: 'name', type: PropertyType.string, required: true },
{ name: 'color', type: PropertyType.string }
]
})
export class Tag extends EntityBase {}

@Entity({
name: 'Todo',
properties: [
{ name: 'title', type: PropertyType.string, required: true },
{ name: 'completed', type: PropertyType.boolean, default: false }
],
relations: [
{
name: 'tags',
kind: RelationKind.MANY_TO_MANY,
mappedEntity: 'Tag'
}
]
})
export class Todo extends EntityBase {
// 关联的标签
tags$!: RelationEntitiesObservable<Tag>;
}
```### 3. 撤销/重做(使用分支)

```typescript
// 在重要操作前创建快照
async function performBatchOperation() {
// 创建快照分支
await rxdb.versionManager.createBranch('snapshot-before-batch');

try {
// 执行批量操作
await batchUpdateTodos();

// 如果成功,可以删除快照
await rxdb.versionManager.removeBranch('snapshot-before-batch');
} catch (error) {
// 如果失败,切换回快照
await rxdb.versionManager.switchBranch('snapshot-before-batch');
await rxdb.versionManager.removeBranch('snapshot-before-batch');
throw error;
}
}

最佳实践

  1. 使用实时订阅:利用 RxJS 的 Observable 实现自动 UI 更新
  2. 批量操作使用事务:确保数据一致性
  3. 合理使用索引:提升查询性能
  4. 利用分支功能:实现撤销/重做和数据快照
  5. 错误处理:妥善处理数据库操作可能的错误

参考