之前已经有一些同学看了一些,已经有一些尝试,但内部一直没有个比较系统的落地文档,本篇主要是偏横向调研的文档,内容比较多,这里不涉及源码,旨在为最后技术选型和架构做支撑,对团队内做普及,也借此契机整合一下工作分配。
市面竞品分析
产品名称 | 富文本 | 协同算法 | 架构 | 优势 | 扩展机制 | 脑图插件 |
---|---|---|---|---|---|---|
腾讯文档 | Tim时期的ace编辑器→canvas自研引擎实现的一套排版 | OT,协同前后端逻辑自己实现,重点参考了google混淆过的前端代码 | 工作量大,自研,自主实现,性能更好,体验上会有一些问题,比如翻译插件的识别,无障碍阅读等 | 流程图是svg,脑图是canvas,都是iframe的形式插入,可以全图编辑,加载文档的时候这两块数据和视图都是延迟异步加载 | ||
金山文档(智能文档) | ProseMirror二次封装→自研? | |||||
飞书 | 基于块的富文本 自研? | |||||
石墨 | quill改造二次封装→自研? | OT | 提供了toB的sdk,做了saas化 | |||
钉钉文档 | slate.js→自研(不可见 textarea 监听用户输入,并定位输入法浮层 ) | OT | ||||
稿定在线设计 | CRDT | Yjs | ||||
语雀 | CodeMirror(在线代码编辑器)→Slate.js→自研,基于浏览器的 Contenteditable 实现了富文本编辑器,通过 Canvas 实现了表格编辑器,通过 SVG 实现了思维导图编辑器(三个独立) | OT | Baas+egg (单体 Web 应用)→ lasS(MySQL、OSS、缓存、搜索等服务)→商业化,迁移阿里云,做服务拆分(微服务类,任务服务类,函数计算类) | 脑图和流程图都是基于svg,也可以全屏编辑,但不是iframe插入,就是一块局部的dom。所以全屏展开编辑的实现方式和腾讯文档也不同 | ||
AFFine | CRDT | |||||
pingcode | CRDT |
其他:
**pingcode CRDT, Yjs**
**superthread: CRDT, Yjs**
这里的富文本只指在浏览器Web环境下的富文本(排除Word,WPS),实现一个富文本,从表现上来就是要实现两大功能:编辑输入和操作命令,编辑输入就是点击可以定位到目标处可编辑可输入,操作命令就是可对选择的区域进行相对应绘制。这两个里面有一些难点:
解决这些问题,浏览器已经为我们提供了几个API:
一个是大家可能都熟知的contentEditable,它是dom元素的一个属性,设置它,可以让这个dom变得可被编辑。它帮我们解决了富文本问题有:
允许许运行命令来操纵可编辑内容区域的元素,
document.execCommand('backColor') // 修改背景颜色
document.execCommand('bold') // 加粗
document.execCommand('copy') // 复制
可以分两点来说
execCommand
本身自己实现的命令也就那一些,并没有覆盖完全,比如我要实现一个创建表格的功能,是没有这个功能的,那怎么办呢,只能靠自己实现了首先创建表格
、添加行
、删除行
、添加列
、删除列
、设置表头
、取消表头
、删除表格
等等操作,一个功能就要去实现一块代码,具体多少代码得看个人实现的方式了,当然,自己实现功能也就意味着bug可控(想怎么做就怎么做),也可自定义样式、兼容性可控。
性能佳,bug不可控,兼容性差,实现的功能有限,只接受有限的 commands
已成为历史的产物,各浏览器的差异在这个API的实现上发挥的淋漓尽致,已经废弃掉不再更新。
往往利用这两个api做选区相关功能,到现在API发展更加丰富,现代的很多富文本必依赖的两个api,selection 基于 range,通过range丰富的api可以对dom做选中,移位等一系列操作,selection api基于range,可以通过selection实现光标的一些处理。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
<title>Static Template</title>
</head>
<body>
<p>选中文本后点加粗按钮</p>
<p><button onclick="bold()">加粗</button></p>
<div
id="editor"
contenteditable="true"
style="padding: 16px; border: 1px solid gray;"
>
Hello
</div>
<script>
function bold() {
// 获取选区
const selection = window.getSelection();
// 获取 DOM
const $editor = document.getElementById("editor");
if (!$editor.contains(selection.anchorNode)) {
// 选区不在编辑器内,无动作
return;
}
// 获取 Range
const range = selection.getRangeAt(0);
// 包裹 strong
const $strong = document.createElement("strong");
range.surroundContents($strong);
}
</script>
</body>
</html>
<textarea id="area" style="width:80%;height:60px">
Focus on me, the cursor will be at position 10.
</textarea>
<script>
area.onfocus = () => {
// 设置零延迟 setTimeout 以在浏览器 "focus" 行为完成后运行
setTimeout(() => {
// 我们可以设置任何选择
// 如果 start=end,则光标就会在该位置
area.selectionStart = area.selectionEnd = 10;
});
};
</script>
https://codepen.io/MingZhao-Zhang/embed/dyLvKep?default-tab=result
现在主流的编辑器总的来说对应的是三种类型,不同的类型其展现的能力、可扩展性、复杂性都各不相同。如下表展示的,从L0 -> L1 -> L2也可以说是:站在浏览器的站在浏览器的头上、站在浏览器的肩上和站在浏览器的脚上,逐渐的脱离浏览器,并且向现代化靠近。典型的就是draft.js、slate。
类型 | 描述 | 代表 | 优劣 |
---|---|---|---|
L0 | 1. 基于浏览器的contenteditable富文本输入框 |
轻量级编辑器,基于以上两个浏览器api实现,适合短时间内快速研发,可定制空间有限。
针对于document.execCommand
的使用情况,以wangEditor为例子来分析。wangEditor核心的文件是command.ts来做命令的封装,下面展示一些关键的伪代码:
/**
* 执行操作的命令
* @param name name
* @param value value
*/
public do(name: string, value?: string | DomElement): void {
// TODO
switch (name) {
case 'insertHTML': // 插入HTML字符串
this.insertHTML(value as string)
break
case 'insertElem': // 插入DOM元素
this.insertElem(value as DomElement)
break
default:
// 默认 command 执行浏览器默认的指令
this.execCommand(name, value as string)
break
}
// TODO
}
/**
* 插入 html
* @param html html 字符串
*/
private insertHTML(html: string): void {
// inserHTML 在IE下是没有的,需要兼容处理
if(isNoIE) {
this.execCommand('insertHTML', html)
} else {
// 通过window.selection获取选取,然后利用insertNode来实现在IE下的insertHTML
range.deleteContents()
range.insertNode()
}
}
meta(facebook)的开源项目,背靠react团队,所以和react思想深度绑定,是react首选的富文本编辑器。它的架构在当时比较新颖,脱离了常用的 contenteditable 的方案,直接按照 React 的模式去做的,是通过拦截光标和键盘等操作,然后更新到内部 immutable 的 state 上面,然后在 render 出来。通过 immutable 来提升渲染性能(但据说硬伤仍在于性能和体验)
插件丰富
遵循与 React 中的受控组件相同的范例,提供了一个 Editor 呈现富文本输入的组件,提供了一个EditorStateAPI 来处理/存储Editor组件中的状态更新。
使用熟悉的声明式 API 抽象出渲染、选择和输入行为的细节
模型是用 immutable-js 构建,提供了一个具有功能状态更新的 API
在淘宝内容平台内部有所实践。
受Draft.js启发,特点旗号是可深度定制,支持完全自定义,与Draft类似,和 React 集成方面有着出色的表现。新版完全拥抱了React,底层从 immutable.js 迁移到 immer,基于 TypeScript 重构。
{
type: 'insert_text',
path: [0, 0],
offset: 15,
text: 'A new string of text to be inserted.',
}
{
ops: [
{ insert: 'Gandalf', attributes: { bold: true } },
{ insert: ' the ' },
{ insert: 'Grey', attributes: { color: '#cccccc' } }
]
}
基于 ContentEditable 的所见即所得 HTML 编辑器,支持协作和自定义文档模式,由多个单独的模块组成,理念: ProseMirror 试着在 Markdown 编辑体验和传统的 WYSIWYG (所见即所得,例如word)编辑体验中寻找一种融合的方法。它通过实现一个比普通的 HTML 具有更多的限制和结构化的 WYSIWYG 风格的接口来做到这点。
基于ProseMirror,所以ProseMirror有的优点他都有,商业化成熟,由专门的公司支持,开源,商业化维护程度高,不止于因为纯为爱发电的开发者跑路。
上面的介绍的这些都属于L0, L1级别的富文本,它们或多或少依赖浏览器富文本的那套浏览器API,加上自己的实现一套操作命令,底层还是依赖浏览器的排版,渲染上无法突破排版限制。
还有一类比较少数的富文本实现:canvas富文本。
这类实现的国内产品有google doc,ms office,wps在线版,腾讯文档,这些有个共同特点就是可以还原ms的word排版,比如word中的文字绕图(在浏览器里只能左绕图右绕图,但ms word里有四周绕图)。
使用canvas就意味着脱离了dom,脱离的浏览器常规的文档流排版,与之相对应的就是巨大的工作量来模拟dom的各种交互和自定义的需求,而且还要重点考虑性能问题(用js快速进行ms word的各种排版,并更新canvas画布)。canvas只有绘图的基本api,要在此之上整合事件系统需要很多工作要做,比如,在canvas文本选中如何做?这几乎就是在2d图形库上实现一个丰富gui的工作。
其实用canvas富文本来命名还不太准确,这类富文本使用canvas只用来去实现复杂的排版效果(ms word),能用html+css的像ms office 和wps都还是会用dom,wps还使用的svg去完成dom做不到的排版效果。
其他缺点:
参考:
腾讯文档用canvas实现的一整套排版和选区,内部的元素能用html的还是html
wps word 使用svg实现一整套排版和选区,内部的元素也都是svg
基于块的富文本: Notion,AFFiNE (blocksuite),基于块的富文本,表现能力更强,抛弃传统的流式布局,采用类似grid现代布局,比如可以方便实现多栏布局这种,实现上面L0级别富文本中的图片文字环绕排版;写作更沉浸式,通过指令的形式创建块,抛弃了传统的菜单属性,而是把菜单属性当做工具栏融入每一个块,对于协作的敏感度也更好控制。
缺点来说:
AFFiNE: There can be more than Notion and Miro.
为什么要有协同算法?常见的一种场景: