实时协作前端
协同编辑的核心原理
多人实时协同编辑(Collaborative Real-Time Editing)允许多个用户同时编辑同一份文档,并实时看到彼此的修改。实现这一功能的核心挑战在于冲突解决——当两个用户同时修改同一位置时,系统必须智能地合并这些改动。
Operational Transform (OT)
OT 是协同编辑领域最经典的算法,被 Google Docs 广泛采用。其核心思想是:
- 每个用户的编辑操作被建模为操作(Operation),如
insert(position, text)或delete(position, length) - 当两个操作同时发生时,通过**转换函数(Transform Function)**将后到达的操作调整到前一个操作已经生效的文档状态上
- 客户端无需等待服务器确认即可应用本地操作(乐观更新),并在接收到服务器确认后丢弃或修正
OT 的数学基础是:对于两个并发操作 op1 和 op2,存在转换函数 T 使得:
apply(apply(doc, op1), T(op2, op1)) === apply(apply(doc, op2), T(op1, op2))CRDT (Conflict-free Replicated Data Types)
CRDT 是近年越来越流行的协同编辑方案,被 Figma、Notion 等产品采用。与 OT 不同,CRDT 不依赖中央服务器进行转换,而是通过数据结构本身的性质保证无冲突合并。
CRDT 的两类实现:
| 类型 | 特点 | 代表实现 |
|---|---|---|
| 基于状态(State-based) | 定期同步整个状态,合并时取最大值 | Yjs, Automerge |
| 基于操作(Op-based) | 广播操作,满足交换律的操作可任意排序 | LSEQ, RGA |
Yjs 是前端最流行的 CRDT 库,其内部使用**RGA(Replicated Growable Array)**算法,每个字符分配一个唯一的 ID({clientID, clock}),通过比较 ID 的偏序关系来决定字符的最终顺序。
OT vs CRDT 对比
| 维度 | OT | CRDT |
|---|---|---|
| 数学复杂度 | 较高,需要双向转换函数 | 较低,数据结构自洽 |
| 中心化程度 | 通常需要服务器协调 | 天然支持 P2P |
| 网络要求 | 需要有序的消息传递 | 无序消息也能收敛 |
| 文档大小 | 操作日志较小 | 元数据开销较大 |
| 成熟产品 | Google Docs, Etherpad | Figma, Notion, Room.sh |
| 前端库 | ShareJS, ot.js | Yjs, Automerge |
共享光标与在线用户列表
共享光标是实时协作中最直观的体验元素。它让用户感知到其他人的存在和活动。
光标数据模型
每个共享光标需要传输以下信息:
interface RemoteCursor {
clientId: string;
userId: string;
userName: string;
userColor: string;
position: {
index: number; // 文档中的字符位置
line: number; // 行号(用于显示)
column: number; // 列号(用于显示)
};
selection: { // 选中范围(可选)
start: number;
end: number;
text: string;
};
timestamp: number;
}光标同步策略
共享光标对延迟高度敏感,需要采用以下优化策略:
- 节流(Throttle):光标移动事件非常频繁,需要在发送端节流。通常每 50-100ms 发送一次位置更新
- 插值预测:接收端收到两个位置更新之间,对光标位置进行线性插值,使其移动更加平滑
- 闲置处理:用户停止编辑后,光标在 3 秒后淡出或变为透明
- 颜色分配:为每位在线用户分配固定的颜色,确保用户在文档中可被辨识
// 光标发送节流
let lastCursorSend = 0;
editor.on('cursorActivity', () => {
const now = Date.now();
if (now - lastCursorSend > 60) {
lastCursorSend = now;
sendCursorPosition(getCursorPos(editor));
}
});在线用户列表
用户列表通常展示在文档顶部或侧边栏,包含:
- 用户头像:圆形头像,背景色与光标颜色一致,显示名字缩写
- 在线状态:绿色圆点表示活跃在线
- 正在编辑的段落:显示用户光标所在的行/段落内容预览
- 用户数量:超过显示上限时合并为「+N」
协同编辑冲突解决策略
即使有 OT 或 CRDT 算法,实际产品中仍需要多层次的冲突解决策略。
策略层次
- 算法层:OT 转换函数或 CRDT 数据结构保证最终一致性
- 应用层:
- 段落锁:用户编辑某段落时,临时锁定,其他用户收到通知
- 语义冲突检测:当两人同时修改同一标题时,提示用户手动选择
- 用户层:
- 冲突预览:可视化显示冲突区域
- 手动合并:提供类似 Git 的三路合并界面
编辑锁定策略
在某些场景下(如表格、表单),直接使用 CRDT 可能不够直观。这时可以采用编辑锁定策略:
- 段落级别锁定:用户开始编辑某个段落时,向服务器请求锁定,其他用户看到锁定标识
- 超时释放:锁定超过 30 秒自动释放,防止用户离开后永久锁定
- 强制抢占:管理员或拥有者可强制解除锁定
WebSocket 通信与状态同步
连接管理
实时协作的核心通信协议是 WebSocket,配合心跳机制保持长连接:
class CollaborationSocket {
constructor(url, docId) {
this.url = url;
this.docId = docId;
this.reconnectAttempts = 0;
this.maxReconnect = 10;
this.connect();
}
connect() {
this.ws = new WebSocket(`${this.url}?doc=${this.docId}`);
this.ws.onopen = () => {
this.reconnectAttempts = 0;
this.send({ type: 'join', userId: this.userId });
};
this.ws.onmessage = (evt) => this.handleMessage(JSON.parse(evt.data));
this.ws.onclose = () => this.reconnect();
}
reconnect() {
if (this.reconnectAttempts >= this.maxReconnect) return;
const delay = Math.min(1000 * Math.pow(2, this.reconnectAttempts), 30000);
setTimeout(() => {
this.reconnectAttempts++;
this.connect();
}, delay);
}
}同步协议设计
一个典型的同步协议包含以下消息类型:
| 消息类型 | 方向 | 说明 |
|---|---|---|
join | Client → Server | 加入文档编辑 |
leave | Client → Server | 离开文档 |
op | Client ↔ Server | 编辑操作同步 |
cursor | Client → Server | 光标位置更新 |
ack | Server → Client | 操作确认 |
snapshot | Server → Client | 全量文档快照 |
前端状态同步架构
一个健壮的协同编辑前端需要分层的状态管理:
View Layer (编辑器 UI)
↕
Client State (本地编辑状态, 乐观更新)
↕
Sync Layer (操作队列, 待确认操作)
↕
Transport Layer (WebSocket 连接管理)操作队列是关键设计——所有本地操作先进入队列,发送后等待服务器 ack 确认后才从队列中移除。未确认的操作在网络重连后重新发送,确保不丢失。
互动 Demo
以下是一个协同 Markdown 编辑器演示(纯前端模拟)。左侧编辑区输入 Markdown 内容,右侧实时渲染预览。顶部显示在线用户列表,编辑区有模拟的其他用户共享光标,底部显示文档保存状态。点击「新增协作者」可查看邀请链接。