轻量本地响应式清单:基于 Dexie.js 的原子事务与拖拽排序

在构建本地优先(Local-First)的极简清单工具时,前端工程师经常会遇到两个核心技术痛点:
借助 Dexie.js 的事务支持(db.transaction)配合浮点数排序算法(Lexorank / Fractional Indexing),我们可以用极简的代码实现 0 网络依赖、0 掉帧、具备强原子性保障的响应式清单数据层。
+——————————————————————–+
| 基于 Dexie.js 与浮点排序算法的响应式清单架构 |
+——————————————————————–+
| [用户拖拽任务 B 移动到任务 A (order: 1000) 与 任务 C (order: 2000) 之间]|
| | |
| v (无需全量重写数组,单次修改计算浮点中点值: (1000 + 2000) / 2 = 1500)|
| [Dexie 原子读写事务: db.transaction('rw', db.todos, async () => …)]|
| ├── 1. 更新任务 B: orderIndex = 1500 (单行更新, 耗时 < 1ms) |
| └── 2. 事务自动提交 -> 触发 useLiveQuery 原子级响应式重新渲染 |
| | |
| [全过程仅修改 1 条数据库记录,0 全量数组遍历重排,性能达到 $O(1)$] |
+——————————————————————–+
1. 声明强类型本地清单数据库
import Dexie, { Table } from "dexie";
export interface TodoItem {
id: string;
text: string;
orderIndex: number; // 浮点排序索引
completed: boolean;
createdAt: number;
completedAt?: number;
}
export class TodoDatabase extends Dexie {
todos!: Table<TodoItem, string>;
constructor() {
super("TideTodoDB");
this.version(1).stores({
todos: "id, orderIndex, completed, createdAt",
});
}
}
export const todoDb = new TodoDatabase();
2. $O(1)$ 浮点插值排序与原子事务实现
export async function reorderTodoItem(
draggedId: string,
prevItem: TodoItem | null,
nextItem: TodoItem | null
) {
let newOrder = 1000;
if (!prevItem && nextItem) {
// 拖到最顶部:取下一项 order 的一半
newOrder = nextItem.orderIndex / 2;
} else if (prevItem && !nextItem) {
// 拖到最底部:取上一项 order + 1000
newOrder = prevItem.orderIndex + 1000;
} else if (prevItem && nextItem) {
// 插入两项中间:取算术平均中点
newOrder = (prevItem.orderIndex + nextItem.orderIndex) / 2;
}
// 执行原子事务单行更新
await todoDb.transaction("rw", todoDb.todos, async () => {
await todoDb.todos.update(draggedId, { orderIndex: newOrder });
});
}
3. React useLiveQuery 响应式列表渲染
import React from "react";
import { useLiveQuery } from "dexie-react-hooks";
import { todoDb } from "@/db/todoDb";
export const ResponsiveTodoList: React.FC = () => {
// 响应式订阅:永远按 orderIndex 升序返回
const todos = useLiveQuery(() => todoDb.todos.orderBy("orderIndex").toArray(), []);
if (!todos) return <div className="text-xs text-stone-400">正在翻开清单本…</div>;
return (
<div className="space-y-2">
{todos.map((item) => (
<div
key={item.id}
className="flex items-center justify-between p-3 bg-[#FAF7F0] border border-[#2D2B2A] rounded-xl shadow-[2px_2px_0px_#2D2B2A]"
>
<span className={`text-sm ${item.completed ? "line-through text-stone-400" : "text-[#2D2B2A]"}`}>
{item.text}
</span>
</div>
))}
</div>
);
};
用最高效的数学与事务机制,让本地数据层在任何高频拖拽下都稳如磐石。

