上期文章,我们从整体上介绍了富文本编辑器的背景,并分享了有道云笔记新版编辑器技术选型中的模型和渲染部分。
[有道云笔记新版编辑器架构设计(上)]
本期文章,我们将继续分享技术选型中的编辑和指令部分内容,并详细解读有道云笔记编辑器的分层架构设计。
由于 contentEditable 会产生不受控事件,导致很多 bug,例如,一开始数据是 abc,对应渲染出的视图是一个 span,内容是 abc。由于需要提供可编辑,span abc 是一个 contentEditable 的元素。
正常情况下,当编辑 span abc 时,例如输入了 d,我们拦截 keyup 事件,在处理函数中将事件 preventDefault,这一步是不让 contentEditable 元素自己修改 span abc为abcd,然后我们在处理函数里调用自定义的 insertText 指令,修改数据 abc 变为 abcd,再用新的数据进行渲染,修改 span abc 为 span abcd。
但是,一旦出现 span abc 上的事件没有被拦截或拦截了但没有正常处理,就会出现 bug。
例如我们旧版的编辑器就没有拦截 ctrl + delete 的事件,如果在 abc 这一行按 ctrl + delete 就没有对应的事件处理函数修改数据模型,数据模型还是 abc,但是由于 span abc 是 contentEditable 的,ctrl + delete 事件会直接修改 span abc 将 abc 整行删除,这样数据模型和视图上就出现了不一致。后续如果再输入 d,则会将数据模型修改成 abcd,这时候视图会根据新数据渲染为 span abcd,表现为已经删除的 abc 再次出现,对用户的使用造成困扰。
针对 contentEditable 的问题,我们决定将完全抛弃它,由此带来两个问题:
触发输入事件:
我们采用了在用户光标位置后画一个隐藏的 Input 组件,Input 组件中有一个 textarea 来接受用户的输入,触发输入相关的事件,如下图所示:
自绘光标/选区:
由于不能使用浏览器默认的光标,我们只能自绘光标。
我们参考浏览器的 Selection 的结构,设计了类似的Selection模型,并用Selection组件渲染 Selection 模型,在屏幕上用绝对定位画出用户的光标,同时用户拖蓝时产生的选区,也可以用这种办法绘制,由于光标和选区本质上一样,我们就放弃了浏览器的选区绘制,也改为自己绘制选区。具体流程如下:
我们用 anchor 表示用户开始托选的位置,focus 表示用户结束托选的位置。anchor 和focus 都包含了 nodeId 和 offset 两个属性,nodeId 表示位置所在的文本节点,offset 表示位置相对于文本节点开始的偏移量,以字符为单位。
当用户点击鼠标开始拖选时,找到鼠标所在的 dom 节点对应模型上的文本节点,我们拿到 id 存储在 anchor 的 nodeId 中,再计算鼠标在 dom 节点上的位置,转换为数据模型上相对文本节点开始处的偏移量,存储在 anchor 的 offset 中。
当用户移动鼠标或者抬起鼠标时,我们用类似的办法更新 focus 数据,将 anchor 和 focus 数据组合成为一个区域(Range)放入 Selection 模型中。这样我们就可以根据用户的点击/拖蓝操作构造出用户的光标/选区对应的Selection模型了。
然后,我们开发 Selection 组件渲染 Selection 模型。当 anchor 和 focus 在同一个位置时,Selection 组件将 Selection 模型渲染为一个闪烁的短线,表示用户的光标,当 anchor 和 focus 不在同一个位置时,Selection 组件将 Selection 模型渲染为一个从 anchor 位置到 focus 位置的一个或者多个矩形区域,表示用户的选区。
总结这一节,我们用 Input 组件触发用户输入事件,构造 Selection 模型和 Selection 组件用于自己绘制光标和选区,最终我们的模型层和视图层如下图所示:
新版编辑器实现了丰富的自定义的富文本编辑指令,自己实现了 execCommand 方法来执行指令。
下面以输入文字的指令作为例子说明指令是如何生成的。
输入文字:
输入文字的指令名称是 'insertText',它需要传入以下四个参数:
在下面的例子中,我们想在 'This is a text' 的位置10处插入一个红色的 'rich' 变为 'This is a rich text',需要生成的指令如图所示:
生成指令上述的指令,需要我们将光标定位到 ‘This is a ’ 之后,然后点击工具栏的颜色按钮设置颜色为红色,再输入字符串 'rich ',根据用户操作生成指令的过程如下图所示:
在用户点击将光标定位到 ‘This is a ’ 之后时,我们更新了 Selection 模型,它的 anchor 和 focus 中的 nodeId 变为当前文本的 id,而 offset 变为 10。
然后,用户点击了红色按钮,这时候我们在Selection模型上记录用户当前设置的行内样式,将在下一次输入时生效(如果点击其他地方,这个行内样式将会重置为新光标前面一个字符的行内样式)。
最后,在用户按下按键输入文字时,我们拦截用户的keydown事件,从event.data中拿到需要插入的文本r,再根据当前的Selection模型,拿到anchor节点的nodeId和offset,以及存储在Selection模型上的行内样式,根据这几个参数就可以生成insertText指令了。
指令的组合:
指令直接会有一些公用的逻辑,为了指令逻辑的复用,我们将一些公用逻辑也封装成指令。简单的指令(Operation)可以组合成复杂的命令(Command)。例如选中一块区域并输入文字,理想的表现是删除区域内的所有文字,再插入输入的文字,如下图所示:
这个复杂的命令我们将它定义为 insertTextAtRange,它实际上是由三步组成:
第一步,先删除选区中的所有文字,这里我们用 deleteByRange 命令实现。而要删除选区中所有的内容,因为选区跨了三个段落,我们需要首先将第一个段落中的 ‘world’ 删除,用到了 deleteText 指令;然后将第二个段落节点 ‘hello javascript’ 整个删除,这里用的是 deleteNode 指令;最后我们还需要将最后一段中的 hello 删除,这里用的也是 deleteText 指令。所以一个 deleteByRange 命令又由多个 deleteText 和 deleteNode 指令组成。
第二步,删除完选区所有文字之后,我们需要插入 editor 到第一段的 hello 之后,用到了上面提到的 insertText 指令。
第三步,我们发现 hello editor 和2020都是文本段落,按照需求我们要将他们合并到一起变为'hello editor 2020',就用到了mergeNode 的指令。
由此可见一个复杂操作对于的命令是有多个指令和命令共同组成的,这种方式能充分解耦和复用的指令,让每个指令只关注于实现一类对数据模型的修改。
撤销重做:
将对数据模型的修改抽象成指令之后,撤销重做就变得比较好实现。
我们规定指令都是成对出现的,每个指令都有对应的逆指令,例如 insertText 的指令它的逆指令是 deleteText,文档模型 Document 在 insertText 指令的修改下变为了Document',那么根据 insertText 指令构造出的逆指令 deleteText 就可以修改Document‘ 让它恢复成 Document,这就是实现撤销重做的基础。
对于复杂的命令,我们会在他执行的时候收集执行的所有简单指令。在撤销时,根据指令的执行顺序,反向的执行所有收集到的指令的逆指令。在重做时,则只需要正向的执行所有收集到的指令。
本章从模型、渲染、编辑、指令四个角度中的前两个说明了新编辑器的技术选型。
总结起来,新编辑器采用典型的 MVC 模式,结合了 React 等前端框架的数据驱动的思想,通过修改数据模型来解决更新视图,由于放弃了 execCommand 和 contentEditable 这两个浏览器的 API,所以自己实现了指令系统、事件拦截和光标绘制。
整个富文本编辑的模块如下图所示:
但是由于富文本编辑器除了需要支持富文本的编辑功能,还需要支持图片、附件、表格、代码块等其他复杂功能,在上述框架内如何扩展支持这些功能,**如何实现功能的解耦和可配置,**这就是下一节我们讨论的问题。
首先我们用图片功能为例,说明如何在现有框架下实现。
我们先只考虑占据一行的图片,这类图片可以单独当做一个段落,所以是可以放入我们的三层文档模型的第二层,如下图所示:
对应的我们需要开发 Image 组件渲染图片,它与 Paragraph 组件一样,也是 Document 组件的子组件,如下图所示:
点击工具栏按钮,我们需要在文档光标处插入对应的图片,这就需要我们生成 insertImage 命令,用它修改文档模型,生成 insertImage 命令的过程如下:
由上述添加图片功能的做法可以看出,新添加一个功能,我们需要设计实现对应的模型、组件和命令,每个功能都涉及到这三处功能的修改,随着功能越来越多,不同功能之间的代码会互相耦合。
并且,在不同应用场景下,需要不同的功能,例如编辑器 A 只需要图片附件和表格的功能,编辑器 B 需要图片、代办、列表的功能,这种编辑器定制是比较难实现的,之前只能通过屏蔽入口实现,js 包里有很多无用代码。
如何解决这些问题呢?
为了解决编辑器核心功能和业务功能的解耦,我们将云笔记新版编辑器的架构分为了核心层和业务层:
核心层:
核心层的主要能力是通过第二节的 MVC 框架提供富文本编辑能力。它暴露了以下接口:
业务层:
在核心层提供的富文本编辑器的基础上实现云笔记编辑器的众多复杂的业务功能。大致包含以下几个需要开发模块:
所以用如下图这样的分层结构,我们就可以解决**编辑器功能耦合和定制化的问题。**
编辑器和核心富文本编辑功能和扩展功能之间以及不同的扩展功能之间都是单独开发的,耦合的可能性大大降低。同时针对不同的编辑器定制需求,可以组合不同的编辑器特性进行打包,这样就可以实现按需打包出定制版的编辑器。
用核心层提供的扩展机制,我们重新实现图片的功能。
首先,我们将三层文档模型的第二层由段落泛化为块(Block),块上提供 name 字段表示块的类型,默认类型为表示段落的 paragraph,针对图片类型,name 可以标志位 image。
图片模型中我们需要记录图片的地址,我们在块的模型中添加 data 字段用于存储不同类型块的自定数据,对于图片就可以在 data 中存储 url 字段。 其次,我们在渲染时,针对块用 Block 组件进行渲染。同时在 editor 暴露 registerComponent 接口,针对不同 name 的块,将对应的渲染组件注册进编辑器。Block 组件就可以在渲染数据时,根据 name 选择对应的注册组件进行渲染。
例如,针对 name 是 paragraph 的段落数据用 Paragraph 组件进行渲染,针对 name 是 image 的图片组件,则用 Image 组件进行渲染。
最后,我们需要实现 insertImage 的自定义命令,通过 editor 的 registerCommand 注册命令,就可以在点击工具栏插入图片时调用 insertImage 的命令修改数据模型。
在实现 insertImage 自定义命令的过程中,我们可能会用到 editor 上保留的编辑器内置命令和指令。
做完这三步,我们就利用编辑器的扩展机制实现了图片的功能。可以看出这样实现的图片功能,有扩展性强、耦合低、可插拔等优点。
综上所述,云笔记新版编辑器采用了核心层和业务层的两层架构,如下图所示:
在核心层,舍弃了存在较多问题的 contentEditable 和 execCommand 接口,自定义了数据格式,攻克了光标绘制、事件拦截、命令系统等技术难题,实现了富文本编辑的核心功能。同时还暴露了丰富的扩展机制。
在业务层,通过核心层暴露的扩展机制,我们可以开发各种不同编辑器特性,通过注册机制将它们注册回编辑器丰富编辑器的功能。
在开发有道云笔记的新版编辑器的过程中,我们遇到很多实际问题,愈发感觉到这是一个非常有深度的前端技术领域,所以我们将新版编辑器的技术选型、架构和部分实现细节拿出来分享给大家,希望对大家开发富文本编辑器、做复杂系统的架构设计有一定参考意义。
本文由哈喽比特于3年以前收录,如有侵权请联系我们。
文章来源:https://mp.weixin.qq.com/s/JyMrIqQXLG3MLkC0yrmzYg
京东创始人刘强东和其妻子章泽天最近成为了互联网舆论关注的焦点。有关他们“移民美国”和在美国购买豪宅的传言在互联网上广泛传播。然而,京东官方通过微博发言人发布的消息澄清了这些传言,称这些言论纯属虚假信息和蓄意捏造。
日前,据博主“@超能数码君老周”爆料,国内三大运营商中国移动、中国电信和中国联通预计将集体采购百万台规模的华为Mate60系列手机。
据报道,荷兰半导体设备公司ASML正看到美国对华遏制政策的负面影响。阿斯麦(ASML)CEO彼得·温宁克在一档电视节目中分享了他对中国大陆问题以及该公司面临的出口管制和保护主义的看法。彼得曾在多个场合表达了他对出口管制以及中荷经济关系的担忧。
今年早些时候,抖音悄然上线了一款名为“青桃”的 App,Slogan 为“看见你的热爱”,根据应用介绍可知,“青桃”是一个属于年轻人的兴趣知识视频平台,由抖音官方出品的中长视频关联版本,整体风格有些类似B站。
日前,威马汽车首席数据官梅松林转发了一份“世界各国地区拥车率排行榜”,同时,他发文表示:中国汽车普及率低于非洲国家尼日利亚,每百户家庭仅17户有车。意大利世界排名第一,每十户中九户有车。
近日,一项新的研究发现,维生素 C 和 E 等抗氧化剂会激活一种机制,刺激癌症肿瘤中新血管的生长,帮助它们生长和扩散。
据媒体援引消息人士报道,苹果公司正在测试使用3D打印技术来生产其智能手表的钢质底盘。消息传出后,3D系统一度大涨超10%,不过截至周三收盘,该股涨幅回落至2%以内。
9月2日,坐拥千万粉丝的网红主播“秀才”账号被封禁,在社交媒体平台上引发热议。平台相关负责人表示,“秀才”账号违反平台相关规定,已封禁。据知情人士透露,秀才近期被举报存在违法行为,这可能是他被封禁的部分原因。据悉,“秀才”年龄39岁,是安徽省亳州市蒙城县人,抖音网红,粉丝数量超1200万。他曾被称为“中老年...
9月3日消息,亚马逊的一些股东,包括持有该公司股票的一家养老基金,日前对亚马逊、其创始人贝索斯和其董事会提起诉讼,指控他们在为 Project Kuiper 卫星星座项目购买发射服务时“违反了信义义务”。
据消息,为推广自家应用,苹果现推出了一个名为“Apps by Apple”的网站,展示了苹果为旗下产品(如 iPhone、iPad、Apple Watch、Mac 和 Apple TV)开发的各种应用程序。
特斯拉本周在美国大幅下调Model S和X售价,引发了该公司一些最坚定支持者的不满。知名特斯拉多头、未来基金(Future Fund)管理合伙人加里·布莱克发帖称,降价是一种“短期麻醉剂”,会让潜在客户等待进一步降价。
据外媒9月2日报道,荷兰半导体设备制造商阿斯麦称,尽管荷兰政府颁布的半导体设备出口管制新规9月正式生效,但该公司已获得在2023年底以前向中国运送受限制芯片制造机器的许可。
近日,根据美国证券交易委员会的文件显示,苹果卫星服务提供商 Globalstar 近期向马斯克旗下的 SpaceX 支付 6400 万美元(约 4.65 亿元人民币)。用于在 2023-2025 年期间,发射卫星,进一步扩展苹果 iPhone 系列的 SOS 卫星服务。
据报道,马斯克旗下社交平台𝕏(推特)日前调整了隐私政策,允许 𝕏 使用用户发布的信息来训练其人工智能(AI)模型。新的隐私政策将于 9 月 29 日生效。新政策规定,𝕏可能会使用所收集到的平台信息和公开可用的信息,来帮助训练 𝕏 的机器学习或人工智能模型。
9月2日,荣耀CEO赵明在采访中谈及华为手机回归时表示,替老同事们高兴,觉得手机行业,由于华为的回归,让竞争充满了更多的可能性和更多的魅力,对行业来说也是件好事。
《自然》30日发表的一篇论文报道了一个名为Swift的人工智能(AI)系统,该系统驾驶无人机的能力可在真实世界中一对一冠军赛里战胜人类对手。
近日,非营利组织纽约真菌学会(NYMS)发出警告,表示亚马逊为代表的电商平台上,充斥着各种AI生成的蘑菇觅食科普书籍,其中存在诸多错误。
社交媒体平台𝕏(原推特)新隐私政策提到:“在您同意的情况下,我们可能出于安全、安保和身份识别目的收集和使用您的生物识别信息。”
2023年德国柏林消费电子展上,各大企业都带来了最新的理念和产品,而高端化、本土化的中国产品正在不断吸引欧洲等国际市场的目光。
罗永浩日前在直播中吐槽苹果即将推出的 iPhone 新品,具体内容为:“以我对我‘子公司’的了解,我认为 iPhone 15 跟 iPhone 14 不会有什么区别的,除了序(列)号变了,这个‘不要脸’的东西,这个‘臭厨子’。