在线文档技术-编辑器篇
这是在线文档技术的第二篇文章,本文将对目前市面上所有的主流编辑器和在线文档进行一次深入的剖析和研究,从而使大家对在线文档技术有更深入的了解,也让更多人能够参与其开发与设计中来。
注意:出于对主流文档产品的尊重,故在分析这阶段会比较简单,旨在让开发者发现重点,不会深入去解析其前端代码实现。
主流编辑器分析
Slate
架构简析
Slate 是一个完全可订制的富文本编辑器框架,其所有的逻辑都是通过插件来实现的,用户拥有高度的自由,不会被 slate 多定制的规则所约束。
为什么要编写 slate,作者在其编写文档中这样写道,“在发明 Slate 之前,我尝试了大量的富文本编辑器库,我发现使用它们构建简单的 demo 是没有问题的,但是当你开始构建类似于 Medium,Dropbox Paper 或者 Google Docs 这样的项目,你会遇到深层次的问题:编辑器的 “schema” 是硬编码的,编程式转换文档是非常复杂的,对 HTML,MarkDown 等内容的序列化支持看起来像是事后加上的,重新发明一个视图层似乎是效率低并且有局限的,协同编辑不是预先设计好的,代码仓库是庞大的,并非小而可复用的,无法构建复杂,嵌套的文档。”
在 Slate 的世界里,插件是“一等公民”,all in plugin,可以通过插件定制所需要的任何功能,而不受到约束;同时 slate 保持了与 DOM 相同的数据模型,这使得在 DOM 上能做的操作,在 slate 中也可以实现;slate 在设计之处就做了明确的边界划分,将核心和定制版边界描述的十分清晰;同时 slate 在设计之初就考虑到了协同,使用者不需要在接入协同的时候去做彻底重构,可以简单的实现接入。
Slate Core部分架构图如下所示:
CreateEditor为外部可以直接调用的创建编辑器的方法,常见实用操作如下所示。
const editor = useMemo(() => withHistory(withReact(createEditor())), [])
上述代码就是创建了一个基于React渲染的且支持undo,redo的编辑器,我们可以通过创建方法发现,其扩展功能的方法就是实现一个类似withReact的东西,这个就是slate世界的一等公民:“插件”。
Slate Core部分的代码非常的简洁,仅仅提供了插件扩展能力以及基本的数据流操作能力,其是默认不提供渲染层的,这一点需要我们去进行编写,当然作为一个前端工程师,我相信这并不难。
接着深入createEditor方法,我们会发现createEditor是一个完备的编辑器方法,在内部实现了包括数据原子操作,属性操作,节点操作等编辑器所需的所有基础能力,可以说其架构能力非常之强,具体方法接口如下所示,由于源码中作者为进行接口抽象,这里为了减少代码量,我这里直接用的Interface的方式进行展示。
// IBaseEditor slate的基础能力,基本上都是对原子数据的操作
type interface IBaseEditor {apply(op: Operation); // 应用opaddMark(key: string, value: any); // 添加属性deleteBackward(unit: TextUnit);deleteForward(unit: TextUnit);deleteFragment(direction?: 'forward' | 'backward');getFragment();insertBreak(): void; // 插入回车insertSoftBreak(): void; // 插入软回车insertFragment(fragment: Node[]); // 插入FragmentinsertNode(node: Node); // 插入节点insertText(text: string); // 插入文字removeMark(key: string); // 移除属性getDirtyPaths(op: Operation): Path[]; // 获取脏区
}
插件扩展
既然我们说在Slate中插件是一等公民,那么我们如何编写一个插件呢?其方式也是非常的简单,这里我直接copy一个官方React渲染插件进行说明。
- 继承扩展BaseEditor,主要是自己定制化的能力。
- withReact实现接口定制的能力。
- 最后withReact(createEditor());完成插件的使用。
// ReactEditor 扩展基础的编辑器能力
export interface ReactEditor extends BaseEditor {}export const withReact = (editor: T,clipboardFormatKey = 'x-slate-fragment'
): T & ReactEditor => {// 实现对应的编辑器方法
};// 最后创建并使用编辑器
const editor = withReact(createEditor())
通过上述操作我们就可以完成一个基于Slate的插件开发工作。
基本数据结构
在 slate 官网中提到,其拥有近似于浏览器 DOM 的 API,其模型为基于 DOM 的一颗嵌套树,其命名,事件定义均符合浏览器标准,能够让开发者很轻易的理解和上手,同时也降低了浏览器处理变更产生的成本。
export type Node = Editor | Element | Text;
export interface Element {children: Node[];[key: string]: unknown;
}
export interface Text {text: string;[key: string]: unknown;
}
- Element 类型含有 children 属性,可以作为其他 Node 的父节点
- Editor 可以看作是一种特殊的 Element ,它既是编辑器实例类型,也是文档树的根节点
- Text 类型是树的叶子结点,包含文字信息
用户可以自行拓展 Node 的属性,例如通过添加 type 字段标识 Node 的类型(paragraph, ordered list, heading 等等),或者是文本的属性(italic, bold 等等),来描述富文本中的文字和段落。
其中[key: string]: unknown用于属性添加。
原子操作类型
Slate提供的原子操作类型有9种,涵盖了文字处理,节点处理,选区处理等。
Prosemirror
Prosemirror的设计就更改的模块化了,不像是Slate数据层的处理是在一起的,Prosemirror所有的模块都被剥离为独立的模块,数据Op,Transform,state,View都独立存在。其设计思路高度定制化,使得用户的代码可以完全控制项目,让开发更像是乐高积木,而不是一个整体的汽车。
架构解析
Prosemirror的核心模块主要分为四部分,开发者可以独立维护各自部分。
prosemirror-model
定义了编辑器的基础模型,用于描述编辑器内容的数据结构。prosemirror-state
提供描述编辑器整个状态的数据结构,类似于Context的玩意儿。prosemirror-view
视图层,提供一个用户组件界面,使得用户可以直接操作编辑。prosemirror-transform
包含以可以记录和重放的方式修改文档的功能(基础编辑能力的实现),这是模块中事务的基础state
,并且使撤消历史记录和协作编辑成为可能。
大概架构设计如下所示:
View为渲染的视图层,State为编辑器状态管理,编辑器的核心能力都在这里可以实现,类似于Context的玩意儿,Transform和Model咱们就不细细分析了,大家的思路大差不差。
同时在数据结构的设计上Prosemirro也是遵循了和浏览器DOM相似的原则,具体图如下所示(图片来自于Prosemirror官方文档)。
Ace
相比前面的Prosemirror和Slate,Ace的完成度就相当的高了,代码量也相当的庞大。其特征包括(来自github/ace的readme文件):
- Syntax highlighting for over 120 languages (支持超过120种语言的语法高亮)
- Over 20 themes (支持超过20种主题)
- Automatic indent and outdent(自动缩进的能力)
- An optional command line (支持可选命令)
- Handles huge documents (能够处理巨大的文档)
- Fully customizable key bindings including vim and Emacs modes(可定制快捷键)
- Search and replace with regular expressions(支持使用正则表达式搜索和替换)
- Highlight matching parentheses(突出显示)
- Toggle between soft tabs and real tabs(选项卡切换)
- Displays hidden characters(字符的隐藏和显示)
- Drag and drop text using the mouse(支持鼠标拖拽文本)
- Line wrapping(换行)
- Code folding(代码折叠)
- Multiple cursors and selections(多光标和选择)
- Live syntax checker(语法检查)
- Cut, copy, and paste functionality(复制粘贴)
通过上面的的特征描述,我们可以毫不客气的说,ace编辑器只需要简单改改就可以直接进行商业化,此编辑器已经具备的完备的能力。
因为篇幅关系,我这里就不再向前两个一样详细分析了,如果要做产品,我会比较推荐这个。
主流在线文档产品分析
腾讯文档
腾讯文档是基于多人实时在线编辑技术的文档协作与文件共享平台。同时提供包含帐号、品类、盘、管理后台、API、安全等能力与企业内部系统进行无缝集成,从而实现自动化文档工作流。
因为腾讯文档的品类众多,所以我们暂时只分析其在线编辑器部分,首先我们先通过Chrome的搜索能力,搜索一些关键词看看其有么有使用开源的编辑器。通过DOM我们可以发现,腾讯文档的编辑器不同于前面所分析的各种编辑器,而是采用的Canvas渲染,至于这种渲染的优劣我也不太好评价。
我们通过前端页面渲染的DOM可以发现,虽然这里展示主要是依赖Canvas渲染,其还是有部分是挂的DOM,至于原因我也不知道,估计是其业务比较复杂有些能力Canvas无法支持,所以选择外挂的形式进行补充。
打开Chrome的控制台选择Network,我们会发现腾讯文档的数据更新与同步是采用WS的形式进行传递的,这一点和Google Doc不同,GoogleDoc是采用轮询API的方式进行同步的。
金山云文档
金山文档是由珠海金山办公软件有限公司发布的一款可多人实时协作编辑的文档创作工具软件。 [1] 金山文档可应用于常见的办公软件,如文字Word、表格Excel、演示PPT。
金山文档做了两套在线文档Doc,我们先分析第一个。
这个在线文档我们很容易在节点中发现其使用了开源编辑器Prosemirror,基本是按照Prosemirror的结构来展示的。
在数据交换上,金山文档和腾讯文档类似都采用WS的方向进行通讯,其中一个WS通道负责心跳检测,另一个负责数据交换。
飞书文档
飞书文档作为后起之秀,其设计,用户体验已经逐渐超越腾讯文档和金山文档,同样老规矩我们先看他用的啥编辑器。打开控制台,我们会发现ace字样,我们推测其使用的是ace编辑器,同样通过控制台,我们可以看到其数据结构也是个树形。
其中飞书的后台提交和其他几家不同,飞书采用的是cgi的提交方式,这一点和Google很像。
谷歌文档
谷歌文档作为在线文档界的鼻祖,那在行业的积累那是相当炸裂的,体验也是几家最好的。google和腾讯文档一样也采用的canvas渲染,据说这个canvas渲染google内部研究了好几年,作者对比了一下腾讯文档和google文档的体验,毫不客气的说google做的cavnas渲染比腾讯文档好很多倍。
其次,Google文档前端的混淆也是相当的厉害,几乎达到了完全不可读的程度。当然也不是完全没办法。
打开控制台,我们通过如下命令可以获取到Google编辑器的数据结构,
window.KX_kixApp
虽然可读性很差,但是慢慢debug,总体还是可以查看的。当然如果大佬们有时间可以通过映射字符还原文档,比如如下这种。
Google的数据提交和飞书一样是采用轮询CGI的方式进行提交,数据结构如下所示,为了方便理解我在下方做了一定的解释。
[{"commands":[{"ty":"is", // Operation类型"ibi":11, // 插入坐标"s":"1" // 插入内容}],"sid":"151f773a331f21bc","reqId":1}
]
没错is就是GoogleDocs的一个Op,我们可以通过同样的方式继续挖掘google 的op设计,从而完成产品的分析。
对比
产品 | 编辑器渲染类型 | 数据提交方式 | 协同算法 | 采用框架 |
---|---|---|---|---|
腾讯文档 | Canvas | websocket | OT算法 | 自研Canvas引擎 |
金山文档 | DOM | websocket | OT算法 | Prosemirror |
飞书文档 | DOM | CGI轮询 | OT算法 | Ace |
谷歌文档 | Canvas | CGI轮询 | OT算法 | 自研Canvas引擎 |
通过上述表格,我们就可以知道各家的产品都是啥样了。
最后
最后希望大家能在我的文章中有所收获,欢迎交流。
参考链接
https://www.kdocs.cn/latest?from=docs 金山文档
https://ace.c9.io/#nav=embedding Ace编辑器
https://prosemirror.net/ Prosemirror编辑器
https://www.slatejs.org Slate源码
相关文章:
在线文档技术-编辑器篇
这是在线文档技术的第二篇文章,本文将对目前市面上所有的主流编辑器和在线文档进行一次深入的剖析和研究,从而使大家对在线文档技术有更深入的了解,也让更多人能够参与其开发与设计中来。 注意:出于对主流文档产品的尊重…...
top -p pid为什么超过100%
CPU:Cores, and Hyper-Threading 超线程(Hyper-Threading ) 超线程是Intel最早提出一项技术,最早出现在2002年的Pentium4上。单个采用超线程的CPU对于操作系统来说就像有两个逻辑CPU,为此P4处理器需要多加入一个Logic…...
#高光谱图像分类#:分类的方法有哪些?
高光谱图像分类方法可以根据分类粒度的不同分为基于像素的分类和基于对象的分类 高光谱图像分类方法可以根据分类粒度的不同分为基于像素的分类和基于对象的分类。 基于像素的分类:这种分类方法是针对每个像素进行分类,将像素的光谱信息作为输入特征&am…...
观察者模式
观察者模式常常用于以下场景:事件驱动系统:当事件发生时,通知所有对该事件感兴趣的观察者。发布/订阅模型:一个主题(发布者)可以有多个订阅者(观察者),当主题发生改变时&…...
前端组件库自定义主题切换探索-03-webpack-theme-color-replacer webpack 同时替换多个颜色改造
接上一篇《前端组件库自定义主题切换探索-02-webpack-theme-color-replacer webpack 的实现逻辑和原理-02》 这篇我们来开始改造,让这个插件最终能达到我们的目的: 首先修改plugin.config.js。 插件首先要在vue.config.js引用注册,因此先对…...
Redis高级-主从复制相关操作
2.1 主从复制简介 2.1.1 高可用 首先我们要理解互联网应用因为其独有的特性我们演化出的三高架构 高并发 应用要提供某一业务要能支持很多客户端同时访问的能力,我们称为并发,高并发意思就很明确了 高性能 性能带给我们最直观的感受就是:速…...
SPI总线设备驱动模型
SPI总线设备驱动模型 文章目录SPI总线设备驱动模型参考资料:一、平台总线设备驱动模型二、 数据结构2.1 SPI控制器数据结构2.2 SPI设备数据结构2.3 SPI设备驱动三、 SPI驱动框架3.1 SPI控制器驱动程序3.2 SPI设备驱动程序致谢参考资料: 内核头文件&…...
开发同事辞职,接手到垃圾代码怎么办?
小王新加入了一家公司,这家公司有点年头,所以连屎山都是发酵过的,味道很冲。和大多数时运不济的程序员一样,到了这种公司,做的大多数工作,就是修补这些祖传代码,为其添砖加瓦。每当被折腾的筋疲…...
gRPC简介
grpc简介 grpc介绍可以参考官网。无论是rpc还是grpc,可以这样理解,都知道过去使用的单单体架构,而在2011年5月威尼斯的一个软件架构会议上提出了微服务架构,围绕业务功能进行组织(organized around business capability)…...
《MySQL系列-InnoDB引擎25》表-InnoDB逻辑存储结构
InnoDB逻辑存储结构 从InnoDB存储引擎的逻辑存储结构看,所有数据都被逻辑地存放在一个空间中,称之为表空间(tablespace)。表空间又由段(segment)、区(extent)、页(page)组成。页在一些文档中有时也称为块(block),InnoDB存储引擎的逻辑存储结构…...
YOLOv8之C2f模块——与YOLOv5的C3模块对比
一、源码对比 YOLOv8完整工程代码下载:ultralytics/ultralytic C2f模块源码在ultralytics/nn/modules.py下,源码如下: class C2f(nn.Module):# CSP Bottleneck with 2 convolutionsdef __init__(self, c1, c2, n1, shortcutFalse, g1, e…...
动态规划实例——换零钱的方法数(C++详解版)
原写了 Java 版本的如何求解换钱的方法数,近期进行了一些细节上的补充,以及部分错误更正,将语言换为了 C 语言。 基础题目 假设你现在拥有不限量的 1 元、5 元、10 元面值纸币,路人甲希望找你换一些零钱,路人甲拿出的…...
linux c
射频驱动 管理硬件设备、分配系统资源 内核由中断服务程序 调度程序 内存管理程序 网络和进程间进程通信程序 linux支持动态加载内核模块 支持多处理smp机制 内核可以抢占preemptive linux系统拥有多个发行版,可能由一个组织 公司和个人发行 VGA兼容或者更…...
第十三章 系统错误消息 - 一般系统错误消息 S - Z
文章目录第十三章 系统错误消息 - 一般系统错误消息 S - Z第十三章 系统错误消息 - 一般系统错误消息 S - Z 错误代码描述<SUBSCRIPT>下标值不合法或Global引用过长。<SWIZZLE FAIL>打开了一个oref,然后试图在另一个无法引用的相关对象中进行搅拌。这可…...
移动web基础
初始缩小:布局视口大于视觉视口 初始放大:布局视口小于视觉视口 布局视口等于视觉视口(这种动作行为叫做理想视口) <meta name"viewport" content"width375" /> <meta name"viewport"…...
MyBatis和MyBatis_Plus有什么区别【面试常考题】
MyBatis和MyBatis_Plus的区别 MyBatis_Plus MyBatis_Plus 是一个 MyBatis 的增强工具,只是在 MyBatis 的基础上增强了却没有做改变,MyBatis-Plus支持所有MyBatis原生的特性,所有引入MyBatis-Plus不会对现有的MyBatis框架产生任何影响。 MyBa…...
华为OD机试用Python实现 -【统一限载货物数最小值】(2023-Q1 新题)
华为OD机试题 华为OD机试300题大纲统一限载货物数最小值题目描述输入描述输出描述说明示例一输入输出说明示例二输入输出说明Python 代码实现算法逻辑华为OD机试300题大纲 参加华为od机试,一定要注意不要完全背诵代码,需要理解之后模仿写出,通过率才会高。 华为 OD 清单查…...
Vue入门小练习
文章目录Hello VueVue文本指令Vue属性绑定Vue双向绑定Vue事件绑定Vue猜数字Vue简单计算器Vue简单计算器升级版Vue循环遍历Vue员工列表练习Vue小练习Vue显示隐藏相关使用一些简单的小案例来熟悉Vue的基本使用方法 Hello Vue <!DOCTYPE html> <html lang"en"…...
Oracle-09-集合运算符篇
2022年4月13日23:01:25 通过本章学习,您将可以:1、描述 SET 操作符2、将多个查询用 SET 操作符连接组成一个新的查询目录 🏆一、SET OPERATORS ⭐️1.1、UNION /UNION ALL ⭐️1.2、INSTERSECT ⭐️1.3、MINUS dz...
获取浏览器(服务端)请求中特定的Cookie
有必要解释一下HttpServletRequest接口,因为我们需要从它里面获取Cookie。 HttpServletRequest HttpServletRequest是一个Java接口,提供了访问HTTP请求信息的方法,例如HTTP方法、请求URI、头部、参数和会话属性。它是Java Servlet API的一部…...
c++11 标准模板(STL)(std::unordered_set)(九)
定义于头文件 <unordered_set>template< class Key, class Hash std::hash<Key>, class KeyEqual std::equal_to<Key>, class Allocator std::allocator<Key> > class unordered_set;(1)(C11 起)namespace pmr { templat…...
python实战应用讲解-【实战应用篇】文件操作(附python示例代码)
目录 知识储备 使用 python-libarchive-c 模块 创建压缩文件 解压文件 查看信息...
OpenCV-Python系列(二)—— 图像处理(灰度图、二值化、边缘检测、高斯模糊、轮廓检测)
一、【灰度图、二值化】 import cv2 img cv2.imread("lz2.png") gray_img cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) # 灰度图 # 二值化,(127,255)为阈值 retval,bit_img cv2.threshold(gray_img, 127, 255, cv2.THRESH_BINARY) cv2.imshow(photo1,im…...
ccc-台大林轩田机器学习基石-hw1
文章目录Question1-14Question15-PLAQuestion16-PLA平均迭代次数Question17-不同迭代系数的PLAQuestion18-Pocket_PLAQuestion19-PLA的错误率Question20-修改Pocket_PLA迭代次数Question1-14 对于有明确公式和定义的不需要使用到ml 智能系统在与环境的连续互动中学习最优行为策…...
hadoop03-MapReduce【尚硅谷】
大数据学习笔记 MapReduce 一、MapReduce概述 MapReduce是一个分布式运算程序的编程框架,是基于Hadoop的数据分析计算的核心框架。 MapReduce处理过程为两个阶段:Map和Reduce。 Map负责把一个任务分解成多个任务;Reduce负责把分解后多任务处…...
测牛学堂:软件测试python学习之异常处理
python的捕获异常 程序在运行时,如果python解释器遇到一个错误,则会停止程序的执行,并且提示一些错误信息,这就是异常。 程序停止执行并且提示错误信息,称之为抛出异常。 因为程序遇到错误会停止执行,有时…...
图神经网络--图神经网络
图神经网络 图神经网络图神经网络一、PageRank简介1.1互联网的图表示1.2PageRank算法概述1.3求解PageRank二、代码实战2.1引入库2.2加载数据,并构建图2.3计算每个节点PageRank重要度2.4用节点尺寸可视化PageRank值一、PageRank简介 PageRank是Google最早的搜索引擎…...
React useCallback如何使其性能最大化?
前言 React中最让人畅谈的就是其带来的灵活性,可以说写起来非常的舒服。但是也就是它的灵活性太强,往往让我们忽略了很多细节的地方,而就是这些细节的东西能进行优化,减小我们的性能开销。可以说刚学React和工作几年后写React的代…...
长尾关键词使用方法,通过什么方式挖掘长尾关键词?
当你在搜索引擎的搜索栏中输入有关如何使用长尾关键词的查询时,你可能希望有简单快捷的方式出现在搜索结果中,可以帮助你更好地应用seo。 不过,这里要记住一件事:SEO 策略只会为你的网站带来流量;在你的产品良好之前&a…...
【网络编程套接字(一)】
网络编程套接字(一)理解源IP地址和目的IP地址理解源MAC地址和目的MAC地址理解源端口号和目的端口号PORT VS PID认识TCP协议和UDP协议网络字节序socket编程接口socket常见APIsockaddr结构简单的UDP网络程序服务端创建套接字服务端绑定字符串IP VS 整数IP客…...
用dw个人网站怎么建立/yandex搜索入口
1.分页是按物理地址划分的,分段是按逻辑地址划分的。 2.页的大小固定不变,段的长度可变。 3.分页用户不可见,分段用户可见。 4.页内位移溢出时,会传给下一页。段内位移溢出时,就会出现越界中断。...
网站用户权限/在线看seo网站
报错提示 1.网上查资料发现原来css-loader和style-loader是被我全局安装了 2.正确做法cnpm install css-loader style-loader --save 3.打包运行成功 转载于:https://www.cnblogs.com/Cavalary/p/7852817.html...
关于网站备案的44个问题/中国十大广告公司排行榜
伪元素:before 和 :after可以做的东西是相当惊人的。对于页面上的每一个元素,你拥有了两个更灵活的、而且可以完成其它HTML元素都能完成的东西的元素。它们让一大堆有趣的设计成为可能,而且不会对你的语义标签产生负面影响。这里有一大堆关于这些有趣的效…...
wordpress多语言内容添加/重庆seo排名
一、编译安装nginx1、下载[rootns1 ~]# wget http://nginx.org/download/nginx-1.10.0.tar.gz2、解压[rootns1 ~]# tar xf nginx-1.10.0.tar.gz [rootns1 ~]# cd nginx-1.10.03、安装依赖组件[rootns1 nginx-1.10.0]# yum install pcre-devel #url重写需要的组件 [rootns1 n…...
wordpress menu插件/长沙关键词优化公司电话
这是一篇讨论Node.js在无需修改任何代码从单核垂直扩展到多核,再水平扩展到多台集群和消息集成的分布式系统,展示了Node.JS在无缝扩展性方面要强于Java。其主要架构是Node.js微服务 消息Messaging 集群Clustering 。翻译如下: 当使用微服务…...
长沙百度做网站多少钱/推广软件赚钱的平台
手机上有好用的时钟APP,时钟软件哪个好用? 每个人的手机屏幕都有着自己独特的设计,而时钟软件能够为用户的手机再增添一些风采和魅力,更有专为来年人设计的大字体桌面时钟,更加的清晰直观,各种特色的桌面时钟app中都有…...