CRDT 实时协作技术在稿定编辑器中的应用

如果无法正常显示,请先停止浏览器的去广告插件。
分享至:
1. 第 十 六 届 D 2 前 端 技 术 论 坛 CRDT 实时协作技术 在稿定编辑器中的应用 王译锋(雪碧)
2. About Me… • 2016 年毕业于中国科技大学,从事前端工作至今 • 喜欢分享前端杂学,交友 ID doodlewind
3. 背景知识 CRDT 库 Yjs 的接入实践 状态同步方案优化 目录 自动化测试与 benchmark 总结
4. 背景知识
5. 业务背景 • 稿定平面设计编辑器:每月服务数万合作设计师与上百万用户的生产力工具 • 区分 C 端个人版与 B 端企业版,后者支持团队协作 • 支持富文本编辑,但主要数据结构仍为树形,元素均为具备较多属性的节点
6. 从状态管理到实时协作:前置知识概述 • 常规「状态管理」在软件工程研究中的抽象:Event Sourcing 模式 • 将 model 操作变为可序列化的 operation 数据,Redux 即为典型 • 构建 operation 栈,在 undo / redo 时移动栈指针应用 operation 即可 • 实时协作场景下的挑战 • 由于网络延迟,分布式产生的 operation 在各客户端可能以不同顺序合并 • 由于用户仅可撤销自身改动,故需能任意「移除」operation 栈底的某项 A B C undo D E undo ……
7. 如何使朴素的 Event Souring 机制适应协作场景? • 引入类似 git 的 rebase 机制 • 服务端唯一确定 operation 顺序,在各客户端 rebase 后应用 • 需依赖中心节点,亦不便支持离线编辑 • 引入对 operation 数据的变换(Operational Transform,即 OT) • 将 operation 在服务端变换,客户端只需应用变换后的 operation • 需引入中心节点,有较多经典案例但工作量大 • 引入特殊的 model 数据结构(Conflict-free replicated data type,即 CRDT) • 每个用户具备唯一 UID,其每次操作均有 version vector(类似逻辑时间戳) • 每个对象字段均关联 version vector,合并时依逻辑时序 last win 即可
8. CRDT 库 Yjs 的接入实践
9. CRDT 库 Yjs 概述 • 源于 YATA 算法,较 RGA 等业界原有 CRDT 算法获得了显著改进 • 最坏时间复杂度为合并远程 insert 时的 O(N^2),其余场景均不超过 O(logN) • 社区已为其实现了 Slate、Quill、ProseMirror 等编辑器的 binding 插件
10. Yjs 接入实践 • 其 API 类似 model 层,提供 YMap、YArray、YText 等 Shared Type • YModel 较难完全透明地与常规 model 同步生命周期,故建议维护 binding 层 • YDoc 可在各客户端实例化,并关联 WebsocketProvider 透明地同步状态
11. 状态双向同步方案 • 为编辑器建模 ac$on • 各类 ac$on 无需可序列化,相当于简易 opera$on • 在 binding 层根据 ac$on 同步修改 YModel 即可 • 本地 ➡ 远程:基于编辑器 ac$on 修改 YModel • 远程 ➡ 本地:基于 YEvent 修改编辑器 model Local Editor Model action YModel Remote YModel Editor Model YEvent
12. 历史记录 • 核心设计准则 • 用户只能撤销自己的改动 • 用户从状态 A 独立撤销 N 次之后再重做 N 次,要能回到 A • Y.UndoManager 符合上述准则 • 为每个用户维护一个本地的 undo 栈 • 对 undo 前后两份 model 状态做 diff,将该 diff 以 YEvent 形式发送 A B C D 独立撤销 C 理想中应获得 A B D 但实践中应获得 A B C D C’ 且只需保证 C C’ 等价于空操作 即可保证 undo 后 redo 的 A B C D C’ C 仍为 A B C D
13. 后端配套方案 • CRDT 无需中心节点,大幅降低了后端开发成本 • 配套后端方案的关键点 • 基于 Yjs 配套的 y-websocket 搭建房间,自动广播房间内各客户端改动 • 可将 YEvent 的 Uint8Array 更新数据缓存至 Redis,令服务端实例横向扩展 • 在 Node 端对 YModel 做序列化操作,即可保存文档版本 • 可参考社区库 Hocuspocos 封装供鉴权、房间新建等业务使用的生命周期钩子
14. 状态同步方案优化
15. 面向嵌套节点树的状态同步优化 • 朴素实现 • 一对一映射 YModel 结构(YModel + YArray 表达力与 JSON 等价) • 更新时以元素为粒度整体更新节点 • 改进 • 支持树形结构扁平化与 fractional-indexing • 支持字段级的 diff 增量更新
16. 树形结构扁平化 • 问题 • 朴素实现会将元素成组操作建模为「先删除子元素,再整体添加新组」 • 对「A 成组 -> B 解组 -> A 撤销 -> B 撤销」路径,会出现重复元素 • 优化方案 • 将 YModel 建模为扁平的 Map<UUID, YModel> 结构 • 为元素维护 parentId 和 index 字段,在 reparent 操作时更改 parentId 即可 • 优化后可支持元素的 reparent 与 shift 语义
17. Fractional-indexing • 问题 • 扁平化后需维护元素 index 字段,更新范围大且可能在撤销后出现重复 • 优化方案 • A B C 元素下标不再为 [0, 1, 2] 形式,而是 { A: 0.25, B: 0.5, C: 0.75 } 结构 • 在 B C 之间插入 D 时,获得 { A: 0.25, B: 0.5, C: 0.75, D: 0.625 }
18. 字段级 diff 增量更新 • 问题 • 若每次同步均全量更新 YModel 字段,每次 undo 时的粒度也只能精确到元素 • 对「A 更改颜色 -> B 更改文字 -> A 撤销」路径,将额外撤销 B 的改动 • 优化方案 • 精确到元素字段级的自动 diff • 仍提交 change_element 类型的 action,附带上更新完成后的元素实例 • 对比 action 中的元素新状态与相应 YModel 的旧状态,仅更新 diff 变化的字段
19. 自动化测试与 benchmark
20. 自动化测试与 benchmark • 开发环境:基于 Cypress + Vite 的 TDD 环境 • 性能测试:基于 Node 的 CLI 压测工具
21. 自动化测试:面向协作场景的测试工具 • 基于 Cypress Component Testing 搭建本地调试页 • 支持完全可视化的 DOM 环境,无需 Node 侧模拟 JSDOM • 支持 Vite,具备无等待的开发体验 • 支持 mount 单个 Vue 组件实例,较传统的 visit URL 形式 E2E 显著更轻 • 使用两个本地 Editor 实例,以交换二进制更新数据的形式通信,无需 WebSocket • 可对比两个实例的 model 状态与 parentId 等字段合法性,及时发现异常状态
22. 性能测试:使用 Node 模拟多用户场景 • 在 Node 端用 y-websocket 实例连入房间,模拟常见编辑器 YModel 更新 • 内存占用较高,2M 模板 100 客户端每 500ms 局部更新 5 分钟,需 3GB 内存 • 房间人数增长后,新用户连入时有显著延迟(对当前 50 人量级足够使用)
23. 总结
24. 总结 • CRDT 方案已在前端实时协作领域具备强竞争力 • CRDT 的设计需离散数学理论,但工程落地所需的仍是基础数据结构知识 • 技术方案应具备理论可靠性,否则正确实现的方案仍可能有重大问题
25. Thanks

Accueil - Wiki
Copyright © 2011-2024 iteam. Current version is 2.137.1. UTC+08:00, 2024-11-17 20:38
浙ICP备14020137号-1 $Carte des visiteurs$