Loro Mirror: 通过镜像到 CRDTs 使 UI 状态具有协作性

摘要: Loro Mirror 将一个类型化的、不可变的应用程序状态视图与 Loro CRDT 文档保持同步。本地的 setState 编辑会变成细粒度的 CRDT 操作;传入的 CRDT 事件会更新您的状态。您可以保留熟悉的 React 模式,并获得协作、离线编辑和历史记录功能。
CRDT:一种无冲突复制数据类型,允许多个对等方并发编辑,并且仍然可以在没有中央协调的情况下收敛。
本地优先: 数据可离线使用并稍后同步;设备是主要的事实来源。
概述
Loro 是一个用于本地优先应用程序的 CRDT 库。它支持丰富的容器——Text、Map、List/MovableList、MovableTree——具有版本控制、时间旅行和紧凑的更新/快照功能。
尽管 CRDTs 确保 CRDTs 状态收敛,但应用程序仍然需要粘合代码来在 CRDT 文档和 UI 状态之间进行映射,以确保它们的一致性。这不是一项容易的任务。
Loro Mirror 解决了这个边界问题。您只需声明一次模式。Mirror 维护一个不可变的应用程序状态视图,并处理两个方向:
- 事件 → 状态。 Loro 事件更新您的状态。
- 状态 → CRDT。
setState差异变成容器级的 CRDT 操作(插入/删除/移动/文本编辑)。
对于一次更新,如果 k 个项目发生变化,并且每个变化的项目影响其 m 个直接字段,则时间复杂度约为 O(k·m)。(k = 更改的项目数;m = 每个更改项目平均更改的直接字段数。)这类似于 React 的渲染复杂度。
为什么存在这个
没有 Mirror,使用 Loro 的项目需要:
- 将 CRDTs 状态映射到 UI 状态
- 区分 UI 编辑并将其转换为 CRDT 操作
- 订阅 CRDT 事件并修补 UI 状态
此代码是重复的,并且容易出错。Mirror 将其集中在声明性模式之后。
Mirror 提供了什么
- 声明性模式。 根据 Loro 容器描述 UI 状态;Mirror 维护一个不可变的视图。
- 类型化且与框架无关。 可在纯 TypeScript、React(通过
loro-mirror-react)或任何其他支持不可变状态的 UI 框架中工作。 - 细粒度差异。 生成诸如
MovableList中的项目移动和Text中的字符增量之类的操作。
如何使用
- 定义一个描述您的应用程序状态的模式
- 创建一个
LoroDoc和一个 Mirror 存储;提供schema - 通过
setState更新。如果需要,订阅更改。 - 使用 Loro 更新在对等方之间同步;Mirror 会自动将远程增量应用回您的应用程序状态。
基本示例
import { } from "loro-crdt";
import { , , } from "loro-mirror";
// 1) 声明状态形状 – 一个待办事项的 MovableList,具有稳定的容器 ID `$cid`
type = "todo" | "inProgress" | "done";
const = ({
: .(
.({
: .(),
: .<>(),
}),
// $cid 是 Loro 分配的 LoroMap 的容器 ID
() => .,
),
});
// 2) 创建一个 Loro 文档和一个 Mirror 存储
const = new ();
const = new ({
,
: ,
// InitialState 不会写入 LoroDoc
: { : [] },
});
// 3) 订阅(可选)– 了解更新是来自本地还是远程
const = .((, { , }) => {
if ( === .) {
.("远程更新", { , });
} else {
.("本地更新", { , });
}
// 您可以直接使用 `state` 进行渲染,它是一个新的不可变对象,与旧状态共享未更改的字段
();
});
// 4) 要么草稿式突变,要么返回一个新状态
// 草稿式(突变一个草稿)
.(() => {
..({ : "草稿添加", : "todo" });
});
// 不可变返回(构造一个新对象)
.(() => ({
...,
: [...., { : "不可变添加", : "todo" }],
}));
// 5) 使用 Loro 更新在对等方之间同步(与传输无关)
// 示例:内存中的两个文档 – 在实际应用程序中,通过 WS/HTTP/WebRTC 发送 `bytes`
const = new ();
.(.({ : "snapshot" }));
// 连接实时同步(本地更新 → 远程导入)
const = .(() => {
.();
});
// `doc` 上的任何 `store.setState(...)` 现在也会出现在 `other` 中React 示例
import React, { } from "react";
import { } from "loro-crdt";
import { } from "loro-mirror";
import { } from "loro-mirror-react";
type = "todo" | "inProgress" | "done";
const = ({
: .(
.({
: .(),
: .<>(),
}),
() => .,
),
});
export function () {
const = (() => new (), []);
const { , } = ({
,
: ,
: { : [] },
});
function (: string) {
(() => {
..({ , : "todo" });
});
}
return (
<>
< ={() => ("写博客")}>添加</>
<>
{..(() => (
< ={.}>
<
={.}
={() =>
(() => {
const = ..(() => . === .);
// 文本增量将自动计算
if ( !== -1) .[]. = ..;
})
}
/>
<
={.}
={() =>
(() => {
const = ..(() => . === .);
if ( !== -1)
.[]. = .. as ;
})
}
>
< ="todo">待办</>
< ="inProgress">进行中</>
< ="done">已完成</>
</>
</>
))}
</>
</>
);
}撤销/重做
import { UndoManageker } from "loro-crdt";
// 在创建 `doc` 之后,在同一组件内:
const undo = useMemo(() => new UndoManager(doc), [doc]);
// 在 UI 的任何位置添加控件:
<div>
<button onClick={() => undo.undo()}>撤销</button>
<button onClick={() => undo.redo()}>重做</button>
{/* UndoManager 仅恢复您的本地编辑;远程编辑保持不变。 */}
{/* 请参阅文档:<https://loro.dev/docs/advanced/undo> */}
{/* 有关完整的时间旅行,请参阅:<https://loro.dev/docs/tutorial/time_travel> */}
</div>;您将获得什么
- 类型安全、与框架无关的状态
- 每个突变都成为一个最小的变更集(CRDT 增量)——无需手动区分
- 对订阅者的细粒度更新,以实现快速、可预测的渲染
- 内置历史和时间旅行
- 通过更新或快照进行离线优先同步,并通过任何传输(HTTP、WebSocket、P2P)进行确定性冲突解决
- 跨客户端的协作撤销/重做
我们在这里构建了一个示例 PWA 应用程序 https://todo.loro.dev。它在 https://github.com/loro-dev/loro-todo 开源。它是协作式的,无需帐户。数据将本地持久化在 IndexedDB 中,并在云中保存 7 天。您只需共享唯一的 URL 即可与他人共享您的待办事项列表。在代码库中,由于 loro-mirror 的帮助,只有一小部分代码与 Loro 相关。
我们未来的方向
因为 Mirror 拥有应用程序状态和 Loro 文档之间的双向映射,所以我们可以在降低集成成本的同时提升价值。例如:
- 文本。许多界面按行渲染,而 LoroText 的低级 API 是基于索引的。团队通常会手动重新实现行分段并将编辑映射回行。有了 Mirror 在中间,就可以在 LoroText 之上提供可选的行感知事件,以便 UI 接收稳定的、基于行的差异,而无需自定义转换——同时保留底层的 CRDT 保证。
- 树。LoroTree CRDT 已经确保了正确的并发移动,但开发人员仍然需要将树操作转换为应用程序状态补丁。Mirror 携带从树事件到您的状态形状的一流映射,因此消费者可以使用自然的“插入/移动/删除节点”更新。
- 临时补丁。我们将添加
setStateWithEphemeralPatch,以便 Mirror 可以通过EphemeralStore流式传输临时的拖动或缩放交互,让协作者看到实时预览,同时在更改最终确定后保持持久化历史的清洁和去重。
通过使用 loro-mirror 来桥接 CRDTs 和应用程序状态的一致性,并通过声明性地表达模式,我们可以让 AI 帮助开发人员更正确地完成更多工作。这使得 Loro 不仅适用于具有实时协作功能的专业创意工具,也适用于让人们为自己和社区构建实用的迷你工具。
如果这项工作能帮助您构建协作式、本地优先的体验,我们将非常感谢您的赞助。您可以通过 GitHub Sponsors 支持我们。