目的/背景

之前已经有一些同学看了一些,已经有一些尝试,但内部一直没有个比较系统的落地文档,本篇主要是偏横向调研的文档,内容比较多,这里不涉及源码,旨在为最后技术选型和架构做支撑,对团队内做普及,也借此契机整合一下工作分配。

参考

市面竞品分析

产品名称 富文本 协同算法 架构 优势 扩展机制 脑图插件
腾讯文档 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

一个是大家可能都熟知的contentEditable,它是dom元素的一个属性,设置它,可以让这个dom变得可被编辑。它帮我们解决了富文本问题有:

document.execCommand

允许许运行命令来操纵可编辑内容区域的元素,

document.execCommand('backColor') // 修改背景颜色
document.execCommand('bold') // 加粗
document.execCommand('copy') // 复制

可以分两点来说

  1. 自定义命令: 性能稍差,代码量相对较大。为什么说性能稍差代码量较大呢,那是因为execCommand 本身自己实现的命令也就那一些,并没有覆盖完全,比如我要实现一个创建表格的功能,是没有这个功能的,那怎么办呢,只能靠自己实现了

首先创建表格添加行删除行添加列删除列设置表头取消表头删除表格等等操作,一个功能就要去实现一块代码,具体多少代码得看个人实现的方式了,当然,自己实现功能也就意味着bug可控(想怎么做就怎么做),也可自定义样式、兼容性可控。

  1. 原生命令:

性能佳,bug不可控,兼容性差,实现的功能有限,只接受有限的 commands

已成为历史的产物,各浏览器的差异在这个API的实现上发挥的淋漓尽致,已经废弃掉不再更新。

Range 和 selection 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富文本输入框
  1. 使用document.execCommand操作命令 | 轻量级编辑器 典型代表:wangEditor | 优:短时间内快速研发劣:可定制空间非常有限 | | L1 | 1. 基于浏览器的contentditable富文本输入框
  2. 自主实现操作命令 | 典型代表:1. draft.js(初始化了一个编辑区)、TinyMCE等 | 优:在浏览器的基础上,能满足大部分业务劣:无法突破浏览器本身的排版效果 | | L1.5 | 1.通过其他hack的方式使用一个不可见 textarea 监听用户输入,并定位输入法浮层来实现输入
  3. 没有完全脱离浏览器排版引擎 | 钉钉文档类似的国内国内自研文档哪个 | | | L2 | 1. 自主实现富文本输入框
  4. 只依赖少量浏览器API | Google Doc、其他的还有Office Word Online、WPS文字在线版,腾讯文档 | 优:都自己实现,可控度都掌握在开发者劣:技术难度大 |

Untitled

wangEditor

轻量级编辑器,基于以上两个浏览器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()
    
	}
}

Draft.js

   meta(facebook)的开源项目,背靠react团队,所以和react思想深度绑定,是react首选的富文本编辑器。它的架构在当时比较新颖,脱离了常用的 contenteditable 的方案,直接按照 React 的模式去做的,是通过拦截光标和键盘等操作,然后更新到内部 immutable 的 state 上面,然后在 render 出来。通过 immutable 来提升渲染性能(但据说硬伤仍在于性能和体验)

Slate.js

受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.',
}

Quill.js

{
  ops: [
    { insert: 'Gandalf', attributes: { bold: true } },
    { insert: ' the ' },
    { insert: 'Grey', attributes: { color: '#cccccc' } }
  ]
}

ProseMirror

基于 ContentEditable 的所见即所得 HTML 编辑器,支持协作和自定义文档模式,由多个单独的模块组成,理念: ProseMirror 试着在 Markdown 编辑体验和传统的 WYSIWYG (所见即所得,例如word)编辑体验中寻找一种融合的方法。它通过实现一个比普通的 HTML 具有更多的限制和结构化的 WYSIWYG 风格的接口来做到这点。

Tiptap

基于ProseMirror,所以ProseMirror有的优点他都有,商业化成熟,由专门的公司支持,开源,商业化维护程度高,不止于因为纯为爱发电的开发者跑路。

L2级别的富文本

上面的介绍的这些都属于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

Untitled

wps word 使用svg实现一整套排版和选区,内部的元素也都是svg

Untitled

Untitled

其他:

基于块的富文本: Notion,AFFiNE (blocksuite),基于块的富文本,表现能力更强,抛弃传统的流式布局,采用类似grid现代布局,比如可以方便实现多栏布局这种,实现上面L0级别富文本中的图片文字环绕排版;写作更沉浸式,通过指令的形式创建块,抛弃了传统的菜单属性,而是把菜单属性当做工具栏融入每一个块,对于协作的敏感度也更好控制。

缺点来说:

AFFiNE: There can be more than Notion and Miro.

Untitled

协同

问题

为什么要有协同算法?常见的一种场景:

Untitled

Untitled