纯前端实现协同光标:WebSocket、位置变换与断线重连

举报
yd_213843956 发表于 2026/10/27 12:50:54 2026/10/27
【摘要】 多人同时编辑时,看到对方的光标和选区是很直观的体验,但实现起来要处理位置变换、心跳和断线重连。这篇拆开讲三个关键部分。

协同编辑里,最容易被感知到的不是合并算法,而是「谁在哪儿」。一个会移动的彩色光标、一个带名字的选区,能让协作变得具体。这篇文章讲三个实现要点:位置怎么表示、消息怎么传、断线怎么恢复。

一、位置不要用像素,用字符偏移

光标位置如果用屏幕坐标表示,窗口一滚动、字号一变化就全错了。正确做法是用文档内的字符偏移,配合区间表示选区:

{
  userId: 'u_42',
  name: '小林',
  color: '#2f7cf6',
  anchor: 128,   // 选区起点
  head: 135      // 光标所在位置
}

anchor 和 head 分开存,既能表示普通光标(两者相等),也能表示反向选区。渲染时把偏移映射成 DOM Range,再用 getBoundingClientRect 定位浮层。

二、别人编辑了,我的位置要跟着变

远端插入或删除文本后,本地保存的偏移会失效。最常用的处理是按操作类型平移:

function transform(pos, op) {
  if (op.type === 'insert' && op.at <= pos) return pos + op.text.length;
  if (op.type === 'delete') {
    if (op.at + op.len <= pos) return pos - op.len;
    if (op.at < pos) return op.at;
  }
  return pos;
}

这只是最小实现。真实场景要考虑并发操作、区间交叉删除等边界,复杂时就要上 OT 或 CRDT。即使最终用库,理解「偏移要随操作变换」这件事也很重要。

三、消息通道:WebSocket + 两类消息

协同光标不需要很高的可靠性,但需要低延迟。WebSocket 比轮询合适,消息可以简单分成两类:

第一类是「状态广播」,带上用户信息和当前位置,服务端转发给同一文档的其他连接;第二类是「心跳」,客户端定时发送,服务端超时未收到就广播该用户离线。

setInterval(() => {
  if (ws.readyState === WebSocket.OPEN) {
    ws.send(JSON.stringify({ type: 'ping', at: Date.now() }));
  }
}, 15000);

光标移动很频繁,发送前要做节流,比如 80~120ms 一次,并合并同一帧内的多次移动,避免把通道打满。

四、断线重连:先恢复状态,再补增量

网络抖动时 WebSocket 会断开。重连不能只重新连上就完事,要先把本地状态和服务端对齐:

重连成功后先拉一次文档版本号和当前在线用户列表,再订阅增量。如果断线期间有本地未提交的编辑,需要带上版本号提交,服务端判断是否冲突。冲突时不要静默覆盖,至少要保留本地副本并提示用户。

function reconnect() {
  const delay = Math.min(30000, 1000 * 2 ** attempt++);
  setTimeout(connect, delay + Math.random() * 300);
}

退避加随机抖动可以避免所有客户端在同一时刻重连,把服务端打垮。

五、渲染细节

光标浮层要用绝对定位,挂在一个不参与排版的容器里,避免影响正文布局。选区高亮可以用 Range.getClientRects() 拿到多行矩形,逐行绘制,不要只画一个大框。

用户离开时记得移除浮层,否则会留下「幽灵光标」。可以给每个用户设置一个最后活跃时间,超过阈值就淡出。

六、小结

协同光标的技术栈不复杂:字符偏移表示位置,操作变换保持位置正确,WebSocket 负责广播,退避重连保证稳定。真正花时间的是边界情况和视觉细节,先把数据结构定对,后面的问题都会简单很多。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。