富文本编辑器的技术演进之路

简介: 如果你的业务也将面向国际市场,面向移动端设备访问,不要犹豫了,Hugo.js 就是你最好的选择!

image.png

作者:UC 国际研发 闻节


原生编辑器

浏览器提供了两个原生特性:
contenteditable:
https://developer.mozilla.org/en-US/docs/Web/Guide/HTML/Editable_content
document.execCommand():
https://developer.mozilla.org/en-US/docs/Web/API/Document/execCommand
contenteditable 特性,可以指定某一容器变成可编辑器区域,即用户可以在容器内直接输入内容,或者删减内容。

execCommand API,可以对选中的某一段结构体,执行一个命令,譬如赋予黑体格式。

基于以上,可以做出最简单的富文本编辑器。

原来富文本编辑器是这么简单?当然不止如此简单!

首先问题集中在 execCommand() 身上:

*第一个是兼容性问题,第二个是能力局限问题。
*

传统编辑器

针对上述痛点,出现了第一代传统编辑器,他们主要的思路是:解决各种浏览器兼容差异,以及规避一些bug;同时对有限的命令集进行扩充。

其中具有代表性的包括:CKEditor(4-)、TinyMCE、UEditor、KindEditor、KissyEditor...

但这些耳熟能详的编辑器,还是会有许多问题:

  • 对浏览器差异的屏蔽,和bug的规避,成本巨大,而且不稳定,时不时发现一些新问题;
  • 对有限的命令集进行扩充,但不是基于execCommand() 进行扩展,而是自行封装实现效果,通过工具栏调用;
  • 只是能力扩充,但并没有提供通用扩展接口,开发者无法自定义一种符合业务需要的格式;

归结下来,最大的问题是:缺乏扩展性。

现代编辑器

虽有不足,但传统编辑器仍然被广泛使用,因为大部分业务都首先要解决从无到有的问题。大约到了2013年,开始出现一批现代编辑器(Modern Editor),他们有一个共同的特点:摒弃 execCommnand(),完全自实现各种格式、撤销、重做等功能,而且都是基于自建的数据模型,提供通用扩展接口。

其中具有代表性的包括:CKEditor 5、Slate.js、Quill.js、Draft.js、ProseMirror...

现代编辑器风风火火发展了几年,的确解决了传统编辑器的老大难问题:扩展性。基于现代编辑器的扩展接口,开发者可以自行定义格式,定义内容等,并且可以实现更复杂的编辑器内交互,使用户体验有所提升。

然而现代编辑也并非银弹,真正接入到业务系统后,会发现各种大小问题,而在深入使用后更会发现一个几乎无解的极大的挑战:不受控输入。

这首先要从现代编辑器的常见设计说起。正如上文所述,他们是基于自建数据模型的,即 model-base,即通过数据模型去描述整个编辑器的内部结构,而非html。如此对视图与数据做了抽象隔离,好处是,对编辑器内容的所有变更,实际上是抽象到对数据模型的调整,在完成调整后,再通过渲染引擎更新DOM,性能更优(更少触碰DOM)也更便于提供扩展接口。譬如,Quill.js 基于 delta 做数据模型;Slate.js 则背靠React,以state做数据模型; CKEditor 使用MVC模式,自建model层,等等。

虽然摒弃了execCommand(),但现代浏览器依然是基于contenteditable 特性,那么用户在容器中执行输入、删除等操作的时候,为了达到 model-drive-render 的效果,首先要通过事件捕获用户的行为,再执行API调整model,最后触发render。譬如,用户输入一个backsapce的时候,编辑器需要捕获住,然后通过API去执行删除行为,原生的backspace实际被截断了。这种强控制,除了是 model-base 驱动考虑外,还有一个显著好处,就是可以在一些特殊边界位置,做更多控制,譬如一些位置可以无效化退格删除等,诸如此类场景,有助提升用户体验。

那么问题来了,事实上存在一些场景,输入是不受控的,或者说是不可识别的,譬如,输入法。

输入法的实现,没有一个标准约束,不同的输入法都存在一些差异(浏览器差异的坑才填完,居然还有输入法这个大坑-_-!!),这些差异最重的一点就是,用户输入的内容,是否可以被识别。一旦出现输入不可识别,就会导致事件无法正确截取,最终流向了原生的行为,dom上的内容发生了变化,但是model的数据没有变化,产生的不一致会直接让编辑器的model-base机制崩坏。举个例子:

原本编辑器里面有“abc”三个字母,用户输入了不可识别的“退格”,直接删除了dom上的“c”,剩下“ab”,此时model里面依然是“abc”,如果通过编辑器model接口获取内容,就会拿到和预期不一的结果;

假设用户又输入了可识别的“回车”,事件被截获,然后通过API 调整 model,变成“abcn”,继而触发渲染,dom上从“ab”变成“abcn”。从用户感知上看,原本删除了的“c”,在回车后,又突然跑出来了...

这种不可识别、不受控的输入,在我们一般日常使用的中文输入法上并不常见,但一些小语种的输入法,或者是基于chrome extension 实现的输入法,以及安卓设备的输入法,都大量存在。而最可怕的是,我们可以检测到用户使用什么浏览器,给出提示,但却无法检测到用户使用什么输入法,完全无法防范,最终用户发现编辑的内容出现各种混乱。

不受控输入引起的混乱,并不是一个简单的bug,并不是修修补补就可以解决的问题,而是一个机制性的问题,是要从底层上重构去解决的根本性问题。

显然,如果编辑器所在业务,是要面向国际化用户,面向移动端用户的话,现代编辑器都不足以支撑。

新一代编辑器

早在2010年,Google Docs 团队由于对 contenteditable 特性不满,提出了一种新型的方案 ,他们连 contenteditable 特性都舍弃了,也不基于 execCommand,就是为了达到完全控制,不受浏览器差异影响。事实上,这种方案的实现复杂度相当高,因为原本浏览器帮你做了80%的事情,现在只剩20%了。为了达到最好的效果而不惜提高了复杂度,估计这也是 Google Docs 一直没有开源编辑器的原因吧。

上面提到的方案地址如下:
https://drive.googleblog.com/2010/05/whats-different-about-new-google-docs.html

可以看到,现代编辑器虽然都在 Google Docs之后产生的,但他们都没有采用 Google Docs 这种方案,他们保留了 contenteditable 就是为了控制复杂度。然而,面对不受控输入的挑战,我们最终发现,Google Docs 的方案才能真正有效解决。

纵观整个编辑器市场,不基于contenteditable 的,除了Google Docs,还有苹果的 iCloud Pages,并且更进一步将渲染层改成 SVG实现;后来网易的有道云笔记也实现了脱离 contenteditable 的编辑器。但这三个编辑器都是云服务的方式提供,并没有一个可复用可集成的开源编辑器。

Hugo.js 是阿里UC国际研发团队,参照 Google Docs 的方案抽象而成,第一个可解决不受控输入的可复用编辑器框架。经过抽象后的实现,我们称之为 shadow-input。为什么称为shadow ?下面一张图就能看明白:

image.png

如图,编辑器的编辑区域不再是一个 contenteditable容器,而是由三个层(layer)层叠而成,从上而下分别是 overlay-layer, render-layer, shadow-layer。

overlay-layer 负责模拟selection,即用户可见的光标、选中区间;

render-layer 负责渲染内容,即文本、图片等;

shadow-layer 负责承接用户输入,即各种输入法输入;

可见,三层layer实质上是将原来contenteditable容器的三种职责拆分了:

原来的光标,都是浏览器自带的,现在通过overlay模拟实现了;

原来的内容,都是直接可以在contenteditable 容器中编辑的,现在则强制通过 model-drive-render 方式更新了;

原来的不受控输入,都是直接落入contenteditable 容器中,现在则是重定向到了一个 shadow buffer中;

这里最重要的一点就是,我们将用户的输入重定向放到一个 shadow buffer 中,我们让用户的输入在一个不可见区域完整生效了之后,再去做内容检测,然后推断出用户的输入,以此来解决不可识别不受控的输入法输入。再举刚刚的例子:


原本编辑器里面有“abc”三个字母,shadow buffer 中也存有“abc”副本;

用户输入了不可识别的“退格”,退格并没有直接删除render-layer的内容,而是重定向落入了shadow buffer中,那么shadow buffer 的内容就变成了 “ab”,我们通过内容检查,可以推断出用户刚刚的输入,是一个退格删除行为,那么我们就可以调用 model.delete() API ,更新model并触发 render;

此时通过 model API 获取编辑器内容到时候,取到的是和dom表现一致的 “ab”;

假设用户又输入了“回车”,同样地通过shadow buffer 的内容检查,可以推断用户输入了回车,然后通过API 调整 model,变成“abn”,继而触发渲染,dom上从“ab”变成“abn”。dom视图和model数据始终保持一致,那么用户也不会见到突如其来的内容消失等混乱。


Hugo.js 通过 UC News 两印媒体人创作平台 Wemedia 落地实践,验证了这种方案在面向国际化用户,面向移动端用户的场景下,能提供更稳定的编辑体验,同时具备极强的通用扩展能力,可以应付业务的各种定制需求。

如果你的业务也将面向国际市场,面向移动端设备访问,不要犹豫了,Hugo.js 就是你最好的选择!(内外开源在路上...)

目录
相关文章
|
7月前
|
运维 前端开发 JavaScript
现代化前端开发工具与框架的演进
随着Web应用的复杂性不断增加,前端开发工具和框架在不断演进,以应对日益复杂的需求。本文将从前端开发工具、主流框架以及未来发展趋势等方面进行探讨,帮助读者了解现代化前端开发技术的最新动态。
|
4月前
|
前端开发 JavaScript C#
C#开发者的新天地:Blazor如何颠覆传统Web开发,打造下一代交互式UI?
【8月更文挑战第28天】Blazor 是 .NET 生态中的革命性框架,允许使用 C# 和 .NET 构建交互式 Web UI,替代传统 JavaScript。本文通过问答形式深入探讨 Blazor 的基本概念、优势及应用场景,并指导如何开始使用 Blazor。Blazor 支持代码共享、强类型检查和丰富的生态系统,简化 Web 开发流程。通过简单的命令即可创建 Blazor 应用,并利用其组件化和数据绑定特性快速搭建界面。无论对于 .NET 还是 Web 开发者,Blazor 都是一个值得尝试的新选择。
163 1
|
4月前
|
Java UED Maven
紧跟技术潮流:手把手教你构建响应式Vaadin应用,让用户体验无缝接轨!
【8月更文挑战第31天】本文从零开始,详细介绍如何使用强大的Java框架Vaadin构建流畅且响应式的Web应用程序。首先,确保安装JDK 1.8+、Maven 3.3.9+及IDE。接着,创建Maven项目并添加Vaadin依赖。然后,通过继承`UI`类创建主界面,并定义自定义主题与样式。利用Vaadin的响应式布局组件,如`HorizontalLayout`和`VerticalLayout`,实现多设备兼容性。
67 0
|
7月前
|
Web App开发 前端开发 JavaScript
构建跨浏览器兼容的前端应用:技术实践与挑战
【5月更文挑战第16天】构建跨浏览器兼容的前端应用是应对浏览器差异和多样性的挑战。使用现代框架(如React、Vue)能自动转换代码,编写可移植的Web标准代码,结合浏览器兼容性测试工具和Polyfill解决旧浏览器支持问题。关注浏览器更新,应对性能、API差异和样式问题,采用渐进增强、条件判断和CSS Reset策略确保应用在各种浏览器上运行良好。
|
7月前
|
JSON 移动开发 数据可视化
Dooring无代码搭建平台技术演进之路
Dooring无代码搭建平台技术演进之路
148 0
|
7月前
|
编解码 前端开发 UED
移动端适配:前端开发的必经之路
【2月更文挑战第1天】移动端适配:前端开发的必经之路
185 0
|
开发框架 前端开发 JavaScript
BootstrapBlazor企业级组件库:前端开发的革新之路
BootstrapBlazor企业级组件库:前端开发的革新之路
196 0
|
设计模式 移动开发 开发框架
|
运维 Cloud Native 前端开发
完美融入云原生的无代码平台 iVX编辑器介绍
完美融入云原生的无代码平台 iVX编辑器介绍
完美融入云原生的无代码平台 iVX编辑器介绍
|
前端开发 Cloud Native JavaScript
【云原生】 iVX 低代码开发 引入腾讯地图并在线预览
【云原生】 iVX 低代码开发 引入腾讯地图并在线预览
332 0
【云原生】 iVX 低代码开发 引入腾讯地图并在线预览