核心在于分离绘制与渲染:将操作抽象为归一化坐标、带时间戳的可序列化事件,通过WebSocket同步,Canvas仅负责本地重放;需支持离线缓存、节流合并、多设备输入适配及性能隔离。

用 Canvas 实现跨设备同步的动态白板,核心不是“画什么”,而是“怎么让多端画笔动作实时一致”。关键在于:Canvas 只负责本地渲染,同步逻辑必须由外部状态管理 + 网络协议支撑,不能依赖 Canvas 自身。
1. 分离绘制逻辑与渲染逻辑
Canvas 是“哑画布”,只执行绘图命令,不保存图形语义。因此不能直接同步 canvas.toDataURL() 或像素数据(体积大、延迟高、无法编辑)。正确做法是把用户操作抽象为可序列化的“绘图事件”:
-
记录操作类型:如
startPath、addPoint、endPath、setColor、setLineWidth -
记录坐标时统一归一化:将触摸/鼠标位置转为相对于白板容器的 [0,1] 归一化坐标(如
x / boardWidth),避免因设备分辨率或缩放导致偏移 - 打时间戳 + 客户端 ID:用于冲突检测和操作排序(例如使用 Lamport timestamp 或 vector clock)
2. 使用轻量协议实现实时同步
推荐 WebSocket(非 HTTP 轮询),服务端用简单广播或 CRDT 协同模型:
- 基础广播模式:任一客户端发出绘图事件,服务端原样转发给其他在线客户端,各端本地重放(需保证重放顺序与接收顺序一致)
- 进阶一致性方案:用 Yjs 或 Automerge 管理白板状态(如一个包含所有笔迹路径的数组),自动处理并发编辑、离线补同步
-
防抖与合并:对快速连续的
addPoint事件做节流(如 15ms 内只发一次批量点),减少网络压力
3. Canvas 渲染适配多设备输入与屏幕特性
同一套事件,在不同设备上要呈现一致效果:
立即学习“前端免费学习笔记(深入)”;
-
响应式 canvas 尺寸:用 CSS 设置 canvas 宽高为 100%,再通过
canvas.width = canvas.offsetWidth * window.devicePixelRatio和height动态设置真实像素,确保高清屏下线条清晰 -
统一输入抽象层:监听
pointerdown/move/up(兼容鼠标、触控、笔),禁用默认行为(e.preventDefault()),并统一提取e.clientX/e.clientY后映射到 canvas 坐标 -
平滑笔迹渲染:不用逐点 lineTo,改用贝塞尔曲线插值(如基于前 3 个点拟合 cubic curve),或启用
ctx.lineCap = 'round'和lineJoin = 'round'消除锯齿感
4. 必备健壮性处理
真实场景中,断网、重连、页面刷新很常见,不能丢失状态:
- 本地持久化缓存:每次收到新事件,先写入 IndexedDB(或 localStorage,适合小数据);重连后比对服务端最新版本号,拉取差量并合并
-
离线操作队列:网络中断时,将新事件暂存内存队列,恢复后按序重发,并标记
isLocalOnly: true避免重复渲染 -
渲染性能隔离:大量路径重放时,用
requestIdleCallback分帧渲染,防止主线程卡死;历史路径可用ctx.drawWindow(非标准)或离屏 canvas 预合成图块
不复杂但容易忽略:Canvas 本身没有“撤销”“选中”“图层”概念——这些必须作为独立状态模块实现,并与绘图事件流解耦。同步的是“意图”,不是“像素”。



















