实现实时协同画板的关键是确保所有人看到一致画面:前端需序列化操作(如记录路径关键点)、WebSocket轻量稳定传输(带roomId和type字段)、本地优先渲染并过滤重复消息、进阶用CRDT解决并发冲突。

用 WebSocket 配合 Canvas 实现实时协同画板,核心不是“画出来”,而是“让所有人看到一样的画”——这要求前端准确捕获操作、后端可靠转发、所有客户端一致渲染。下面分关键环节讲清楚怎么做。
Canvas 绘图逻辑要可序列化
Canvas 本身只管像素,不能直接同步“一条线”,必须把用户操作转成结构化数据(比如 { type: 'line', x1: 100, y1: 200, x2: 150, y2: 250, color: '#ff6b6b', width: 3 })。自由画笔尤其要注意:不能只发起点和终点,得记录鼠标移动过程中的关键点序列(如每 10ms 或每 5px 记一个点),否则快速拖动会变成折线或断点。
- 监听
mousedown→ 记录起始坐标,开始收集路径点 - 监听
mousemove(且按下状态为 true)→ 持续追加坐标点到当前路径数组 - 监听
mouseup或mouseout→ 发送完整路径数据,清空临时路径 - 所有图形(矩形、圆、文字)都按统一格式封装,确保能被 JSON 序列化
WebSocket 连接与消息规范要轻量稳定
别用原始 WebSocket API 手写重连和心跳——推荐用 Socket.IO(服务端 Node.js + 客户端 JS)或成熟的 SDK 如 irealtime.js。连接建立后,需约定清晰的消息结构:
- 每条消息带
roomId字段,支持多房间隔离 - 操作类型用
type区分:'draw_start'、'draw_move'、'draw_end'、'shape_add'、'clear' 等 - 附带时间戳(
ts)便于后续做延迟补偿或冲突排序 - 避免发送原始 canvas.toDataURL() 图片,太重;只传操作指令
前端渲染必须本地优先+服务端校验
用户画的时候,先立刻在自己屏幕上绘制(本地响应),再发给服务器广播;其他客户端收到后直接复现。但要注意两个常见问题:
-
坐标偏移:canvas 的
getBoundingClientRect()必须每次重算,不能缓存,否则缩放或滚动后位置错乱 -
重复渲染:自己发出的操作也会被自己收到,需加
clientId过滤,避免本地画两次 -
顺序错乱:网络延迟可能导致后发的消息先到,建议服务端按时间戳排序后再广播,或客户端按
ts缓存并等待前序消息
基础协同可用广播,进阶需考虑冲突与一致性
纯涂鸦场景(无图形编辑、无选中/拖拽),靠广播基本够用。但如果支持移动已有图形、修改颜色、删除对象,就必须引入协同模型:
- 简单方案:用唯一 ID 标识每个图形,所有变更都带
id和op(如 'update_attr'、'delete'),服务端做幂等处理 - 严谨方案:引入 CRDT(如
yjs或自研轻量版),对画布状态建模为可合并的数据结构,天然解决并发修改冲突 - 不推荐 OT(操作转换):对图形类操作建模复杂,容易出错,CRDT 更适合白板这类最终一致性优先的场景


















