富文本编辑器的技术演进
如果无法正常显示,请先停止浏览器的去广告插件。
1. 富⽂文本编辑器器的技术演进
罗龙浩
蚂蚁金服高级前端技术专家,语雀文档编辑器负责人
2. 在此键入姓名
在此键入tittle
3. ⾃自我介绍
2008~2014:业余时间研发 KindEditor,经历 3 个版本的重写
2012~2014:土豆网前端架构师、前端负责人
2014~2018:支付宝行业前端负责人、口碑前端负责人
2018~至今:语雀文档编辑器负责人
4. ⽬目录
一、富文本编辑器介绍
二、语雀文档编辑器面临的问题与解决思路
三、多人实时协同的解决思路
5. 富⽂文本编辑器器 - 常⻅见交互
富文本输入框
- 输入内容
- 选中 & 操作
操作栏
- 顶部工具栏
- 侧边栏
- 内嵌工具栏
6. 富⽂文本编辑器器 - 浏览器器特性
富文本输入框
<div contenteditable=“true”>这里可以编辑</div>
对内容进行操作
document.execCommand(‘bold’);
7. 富⽂文本编辑器器 - 技术类型
类型
描述
典型产品
L0 1、基于 contenteditable
2、使⽤用 document.execCommand
3、⼏几千~⼏几万⾏行行代码 早期的轻量量级编辑器器
L1 1、基于 contenteditable
2、不不⽤用 document.execCommand,⾃自主实现
3、⼏几万⾏行行~⼏几⼗十万⾏行行代码 CKEditor、TinyMCE
Draft.js、Slate
⽯石墨墨⽂文档、腾讯⽂文档
1、不不⽤用 contenteditable,⾃自主实现
2、不不⽤用 document.execCommand,⾃自主实现
3、⼏几⼗十万⾏行行~⼏几百万⾏行行代码 Google Docs
Office Word Online
iCloud Pages
WPS ⽂文字在线版
L2
8. 富⽂文本编辑器器 - 不不同类型的优劣
类型
优势
劣势
L0 技术⻔门槛低,短时间内快速研发 可定制的空间⾮非常有限
L1 站在浏览器器肩膀上,能够满⾜足 99% 业务场景 ⽆无法突破浏览器器本身的排版效果
L2 技术都掌控在⾃自⼰己⼿手中,⽀支持个性化排版 技术难度相当于⾃自研浏览器器、数据库
9. 富⽂文本编辑器器 - L1 编辑器器
传统模式
DOM 树等于数据,调用各种 DOM API 进行操作
典型产品:CKEditor 4、TinyMCE、UEditor
MVC 模式
数据和渲染分离,实现一套操作数据模型的方法,数据变更带动渲染
典型产品:CKEditor 5、Draft.js、Slate
10. 富⽂文本编辑器器 - L1 编辑器器两种模式优劣
传统模式
优势:20 年的历史,代码简单直接,可维护性好,充分利用 contenteditable 特性
劣势:代码写法不符合潮流,都是 10 几年前的技术
MVC 模式
优势:代码写法符合潮流
劣势:引起数据和渲染不同步的问题,因为这个机制需要有完全控制用户输入的前提,
实际上基于 contenteditable 没办法控制用户的所有输入,第三方输入法、壳浏览器
会让用户输入不可控
11. 富⽂文本编辑器器 - L2 编辑器器
自主实现富文本输入框,包含用户输入和排版引擎,可用 DOM、SVG 技术
用户输入:
光标、选区自主实现,光标位置放隐藏 textarea 接受键盘输入,输入完成之后变更数
据、渲染视图
排版引擎:
实现各种个性化的文字排版、图文布局,突破浏览器排版限制
12. 富⽂文本编辑器器 - 总结
L0
L1
L2
技术类型
传统模式
MVC 模式
如何技术选型?
没有编辑器研发团队:推荐基于 CKEditor 4、TinyMCE 二次开发
有几人编辑器研发团队:推荐自研 L1 传统模式编辑器
有几十人编辑器研发团队 & 需要个性化排版:推荐自研 L2 编辑器
13. ⽬目录
一、富文本编辑器介绍
二、语雀文档编辑器面临的问题与解决思路
三、多人实时协同的解决思路
14. 语雀编辑器器 - ⾯面临的问题
疑难杂症多
问题难以修复,页面崩溃、光标错乱、粘贴卡死等
排查链路长
语雀编辑器、Slate、React 一层层往下查
新增功能难
很多个性化需求,在 Slate 架构上实现成本较高
15. 语雀编辑器器 - 根本问题
技术选型问题
1)基于 Slate,是 L1 MVC 模式
2)基于 React 渲染,但 React 是 UI 构建库
16. 语雀编辑器器 - 技术选型
L0
L1
传统模式
L2
MVC 模式
更换技术选型,用 L1 传统模式重写编辑器
为什么没有基于开源编辑器?
第一是 license 问题,第二是我正好具备多年 L1 编辑器研发经验 :)
17. 语雀编辑器器 - 技术⽬目标
高健壮性
采用一切手段保证功能的稳定,努力做到业内问题最少的编辑器
可维护性
编辑器本身代码量很大,后期可维护性是关键,能用简单方式解决问题,尽量简化
可扩展性
具备良好的扩展性,不能因为架构问题,满足不了业务需求
18. 语雀编辑器器 - 开发思路路
数据格式:在 HTML 基础上扩展
卡片机制:承接组件的扩展,在编辑器里独立的一块区域
开发模式:Hybrid 混合开发,编辑区域用原生 JS,UI 层用 React
技术原理:基于 contenteditable,通过 Range API 对选中的内容进行操作
19. 语雀编辑器器 - 数据格式
<h3>heading</h3>
<p>
<strong><anchor />bold<focus /></strong>
<em>italic</em>
<u>underline</u>
<span style="color: #FFFFFF;">fontcolor</span>
<span style="background-color: #000000;">backcolor</span>
<card type="inline" name="image" value="JSON string"></card>
</p>
<p style="text-align: center;">alignment</p>
<ol>
<li>orderedlist</li>
</ol>
<card type=“inline" name="file" value="JSON string"></card>
<card type="block" name="codeblock" value="JSON string"></card>
光标
<cursor />
选区
<anchor />HTML<focus />
卡片组件
<card type=“”
name=“”></card>
20. 语雀编辑器器 - 卡⽚片机制
卡⽚片⼯工具栏
卡⽚片内容区域(contenteditable = false)
Left Cursor
Right Cursor
21. 语雀编辑器器 - 卡⽚片类型
Inline Card
Block Card
22. 语雀编辑器器 - 混合开发模式
红色区域:原生 JS
蓝色区域:React
23. 语雀编辑器器 - 为什什么⽤用混合开发模式?
统一不一定是最佳选择,还是要看带来的业务价值
有两个成功案例:
1)移动端 Hybrid 开发(Native + H5)
2)丰田、本田的 Hybrid 汽车(电机 + 内燃机)
24. 语雀编辑器器 - 丰⽥田混动系统
起步
正常⾏行行驶,有剩余能量量
低速⾏行行驶
急加速
正常⾏行行驶,⽆无剩余能量量
减速,充电
25. 语雀编辑器器 - 键盘输⼊入定制
26. 语雀编辑器器 - contenteditable 问题
光标无法移动到空标签里:<p>|</p>、<span>|</span>
光标漂移到 inline-block 右侧:<p><span style=“display:inline-block” />|</p>
光标无法精确控制:<p><a href=“url”>link|</a>|</p>
无法输入中文:<p><span>emoji</span>|</p>
27. 语雀编辑器器 - contenteditable 解决⽅方案
光标无法移动到空标签里:<p><br />|</p>、<span>​|</span>
光标漂移到 inline-block 右侧:<p><span style=“display:inline-block” />​|</p>
光标无法精确控制:<p><a href=“url”>link|</a>​</p>
无法输入中文:<p><span>emoji</span><span>|</span></p>
28. 语雀编辑器器 - Range 介绍
1、开始位置和结束位置通过 container 和 offset 标记位置
2、在文本之间:container 为 TextNode,offset 为从第一个字符到当前位置的偏移量(第几个字
符)
3、在节点之间:container 为父节点,offset 为从第一个子节点到当前位置的偏移量(第几个子节
点)
4、开始位置等于结束位置, range.collapsed 为 true,也就是光标状态
5、开始位置不等于结束位置,range.collapsed 为 false,也就是选择一段内容的状态
29. 语雀编辑器器 - Range 示例例
<p>a<cursor />bc</p> <p><cursor /></p>
range.startContainer = abc;
range.startOffset = 1;
range.endContainer = abc;
range.endOffset = 1;
range.collapsed = true; range.startContainer = p;
range.startOffset = 0;
range.endContainer = p;
range.endOffset = 0;
range.collapsed = true;
<p><anchor />abc<focus /></p> <p><anchor />abc<focus /></p>
range.startContainer = abc;
range.startOffset = 0;
range.endContainer = abc;
range.endOffset = 3;
range.collapsed = fasle; range.startContainer = p;
range.startOffset = 0;
range.endContainer = p;
range.endOffset = 1;
range.collapsed = false;
30. 语雀编辑器器 - 性能对⽐比
语雀⽂文档 Google Docs 腾讯⽂文档 ⽯石墨墨⽂文档
加载时间 2 秒 3 秒 3 秒 8 秒
粘贴时间 4 秒 7 秒 6 秒 14 秒
操作响应 有点卡 顺畅 ⽐比较卡 ⽐比较卡
测试设备:2015 款 MacBook Pro 15,Chrome 77.0.3865
测试数据:https://shimo.im/docs/keW3LxVd2vQHxUHD/read
声明:由于每个产品的定位、功能复杂度有差异,测试结果好,不不代表编辑器器整体领先,只能说明某⼀一⽅方⾯面有优势。
31. 语雀编辑器器 - 时间节点
2018.08:技术选型,开始研发
2018.09:基础编辑 demo 演示
2018.11:讨论区、评论小型编辑器上线
2019.01:文档编辑器上线
2019.03:旧版编辑器全部替换完成,整体运行平稳
32. 语雀编辑器器 - 总结
一、根据当前主要问题和后续产品方向,选择合适的技术方案
二、对于绝大多数业务,L1 传统模式编辑器是合适的选择
三、利用好现有的资源,可以用 React、Vue 成熟的组件搭建外围的 UI 层
33. ⽬目录
一、富文本编辑器介绍
二、语雀文档编辑器面临的问题与解决思路
三、多人实时协同的解决思路
34. 多⼈人实时协同 - 新的挑战
今年 3 月份,我们 PD 找我说
35. 多⼈人实时协同 - 语雀⽂文档
36. 多⼈人实时协同 - 语雀表格
37. 多⼈人实时协同 - 分析竞品
调研对象:Google Docs、Etherpad、 CKEditor 5、Slate、Quill
结论:都用 OT(Operational Transformation) 或类似的技术,将操作转化成 OP
(operations),发送到协作服务,再转发给其它在线用户。所以都具备原子化的操作
API,所有的高级操作都通过原子化 API 完成,实时协同只需要将这些原子化 API 的调
用信息转化成 OP 即可
38. 多⼈人实时协同 - 开源编辑器器的原⼦子化 API
Quill:insert、delete、retain、format
Slate:insert_text、remove_text、insert_node、merge_node、
remove_node、move_node、set_node、split_node
CKEditor 5:insert、move、detach、merge、split、attribute
39. 多⼈人实时协同 - 想法⼀一
改成 MVC 模式
引入 DataModel、抽象原子化 API,但这个意味着重新开发一套编辑器,工作量巨
大,很可能重回 Slate 老路,丢失我们自己的优势,稳定性、易维护、粘贴性能等
40. 多⼈人实时协同 - 想法⼆二
封装原子化 API
能不能封装 insertNode、removeNode、mergeNode、splitNode 等原子化
API,所有上层操作都基于这些 API,是否可行?
但几乎所有代码都要修改,影响面完全不可控
41. 多⼈人实时协同 - 想法三
DOM diff 方案
能不能每次操作之后直接对比变更前后的 2 个 DOM 树,生成 JSON 格式的 diff,是
否可行?
最大问题是性能,虽然能通过局部 diff 提升性能,但每次操作都要 diff 有点夸张。
42. 多⼈人实时协同 - 想法四
全量 command 机制
引入新的 command 机制,所有的变更都通过 command 完成,变更之后产生对应
的 OP,包含 backward 逆向操作
看起来可行
开始 demo 开发,发现编写 command 是非常复杂的事情,写 backward 逻辑成本
太高
43. 多⼈人实时协同 - 想法五
在 DOM 底层实现
我们的目标是增加实时协同能力,功能的稳定性比较重要,现在的优势不能丢失,日常
迭代和新功能开发还是要持续进行。所以只能在现有代码上进行改进和扩展,不能推翻
重来
只有一条路,在 DOM 底层做文章
其实 DOM 树相当于 DataModel,DOM API 相当于原子化 API
44. 多⼈人实时协同 - ⽣生成 OP
通过浏览器的 MutationObserver API,获取 DOM 树的变更信息
Example
Compatibility
45. 多⼈人实时协同 - OT 服务
采用 ShareDB,实现了 OT 算法,提供一个基于 JSON 的 OT 通用能力
46. 多⼈人实时协同 - 解决⽅方案
OT 服务:基于 ShareDB
数据格式:JSONML
技术原理:通过 MutationObserver API 监听编辑器的 DOM 树变更,生成 JSON
格式的 OP,发送到 ShareDB,更新 JSONML 数据。同时将 OP 发送到其它用户,
将 OP 转化成 DOM 操作方法之后执行
47. 多⼈人实时协同 - OP 格式
OP 格式
JSON
DOM
{p:PATH, li:NEWVALUE} List Insert 插⼊入 Node
{p:PATH, ld:OLDVALUE} List Delete 删除 Node
{p:PATH, oi:NEWVALUE} Object Insert 增加 Element 属性
{p:PATH, od:OLDVALUE} Object Delete 删除 Element 属性
{p:PATH, si:TEXT} String Insert 插⼊入 Text
{p:PATH, sd:TEXT} String Delete 删除 Text
48. 多⼈人实时协同 - 时间节点
2019.04:技术选型,开始研发
2019.08:文档编辑器的多人协同上线
2019.10:表格编辑器的多人协同上线(计划)
49. 多⼈人实时协同 - 总结
一、L1 传统模式编辑器也可以实现多人实时协同
二、如果其它业务中需要多人实时协同的场景,推荐 ShareDB
三、仅仅完成功能,其实不难,是从 0 到 1 的过程
四、要做成完美,非常难,是从 1 到 100 的过程
50.
51.