富文本编辑器是允许用户在输入和编辑文本内容时,可以应用不同的格式、样式等功能,例如图文混排等,具有所见即所得的能力。与简单的纯文本编辑组件<input>
等不同,富文本编辑器提供了更多的功能和灵活性,让用户可以创建更丰富和结构化的内容。现代的富文本编辑器也已经不仅限于文字和图片,还包括视频、表格、代码块、附件、公式等等比较复杂的模块。
那么为什么要从零设计实现新的富文本编辑器,编辑器是公认的天坑,且当前已经有很多优秀的编辑器实现。例如极具表现力的数据结构设计Quill
、结合React
视图层的Draft
、纯粹的编辑器引擎Slate
、高度模块化的ProseMirror
、开箱即用的TinyMCE/TipTap
、集成协同解决方案的EtherPad
等等。
我也算是比较关注于各类富文本编辑器的实现,包括在各个站点上的编辑器实现文章我也会看。但是我发现这其中极少有讲富文本编辑器的底层设计,绝大多数都是讲的应用层,例如如何使用编辑器引擎实现某某功能等。虽然这些应用层的实现本身也会有一定复杂性,但是底层的设计却是更值得探讨的问题。
此外,我觉得富文本编辑器很类似于低代码的设计,准确来说是No Code
的一种实现。本质上低代码和富文本都是基于DSL
的描述来操作DOM
结构,只不过富文本主要是通过键盘输入来操作DOM
,而无代码则是通过拖拽等方式来操作DOM
,我想这里应该是有些共通的设计思路。
而我恰好前段时间都在专注于编辑器的应用层实现,在具体实现的过程中也遇到了很多问题,并且记录了相关文章。然而在应用层实现的过程中,遇到了很多我个人觉得可以优化的地方,特别是在数据结构层面上,希望能够将我的一些想法应用出来。而具体来说,主要有下面的几个原因:
纸上得来终觉浅,绝知此事要躬行。
我的博客是从20
年开始写的,记录的内容很多,基本上是想到什么就写什么,毕竟是作为平时学习的记录。然后在24
年写了比较多的富文本编辑器的文章,主要是整理了平时遇到的问题以及解决方案,集中在应用层的设计上,例如:
此外,前段时间还研究了slate
富文本编辑器相关的实现,并且也给slate
的仓库提过一些PR
。还写了一些slate
相关的文章,并且还基于slate
实现了一个文档编辑器,同样也是比较关注于应用层的实现,例如:
在实现了诸多的应用层的功能之后,发现整个编辑器有很多可以深入研究的地方。特别是有些实现看似很理所当然,但是仔细研究起来会发现这其中有很多细节可以探究,例如在DOM
结构后常见的零宽字符、Mention
节点的渲染等等,这些内容都可以单独拿出来记录文章,这其实就是我想从零实现编辑器的最重要原因。
24
年开始写了很多业务上的东西,到了25
年就略感题穷,而目前我也没有别的擅长的方面,由此写编辑器相关的内容是比较好的选择,这样对于文章的选题也会简单些。不过,虽然想的是深入写编辑器相关的内容,但是在平时遇到问题的时候,还是会记录下来,例如最近有个基于immer
配合OT-JSON
实现的状态管理的想法可以实现。
而对于编辑器的具体实现,我目前的目标是实现可用的编辑器,而不是兼容性非常好且功能完备的编辑器。主要是现在已经有非常多优秀的编辑器实现,且有很多生态插件可以支持,能够满足大部分的需求。目前我想实现的编辑器主要是兼容Chrome
浏览器即可,移动端的问题暂时不会考虑。不过,如果能够将编辑器做得比较好的话,自然可以去做兼容性适配。
不过目前还是试探性地来设计并实现编辑器,期间必然会遇到很多问题,这些问题也将会成为专栏的主体内容。最开始的时候,我是准备将编辑器完善后再开始撰写文章,后来发现设计过程中的历史方案同样很有价值,因此决定将设计过程也一并记录下来。如果将来真的能够将编辑器适用于生产环境,那么这些文章就能够溯源到模块为什么这么设计,想必也是极好的。整体来说,我们不能一口吃成胖子,但是一口一口吃却是可以的。
这部分是让我想起来一句话:我们富文本编辑器是这样的,你不写你不懂。
编辑器是个非常注重细节的工程,很多时候都需要深入研究浏览器的API
,例如document
上的caretPositionFromPoint
方法,用以获取当前某个点所在的选区位置,通常用于拖拽文本后的落点定位。除此之外,还有很多选区相关的API
,例如Selection
、Range
等等,这些都是编辑器实现的基础。
那么深入编辑器底层就是很有意义的事情,很多时候我们都需要跟浏览器打交道,即使是对我们平时的业务开发也会有价值。在这里我想聊一下编辑器中的零宽字符,以此例学习编辑器的细节设计,这是一个非常有意思的话题,类似这种内容就是不研究则不会关注到的有趣事情。
零宽字符顾名思义是没有宽度的字符,因此就很容易推断出这些字符在视觉上是不显示的。因此这些字符就可以作为不可见的占位内容,实现特殊的效果。例如可以实现信息隐藏,以此来实现水印的功能,以及加密的信息分享等等,某些小说站点会通过这种方式以及字形替换来追溯盗版。
而在富文本编辑器中,如果我们在开发者工具检查元素时,可能会发现一些类似于​
即U+200B
类似的字符,这就是常见的零宽字符。例如在飞书文档的编辑器中,我们通过$("[data-enter]")
就可以检查到其中存在的零宽字符。
那么从名字上来看,这个零宽字符在视觉上是不显示的,因为其是零宽度。但是在编辑器中,这个字符却是很重要的。简单来说,我们需要这个字符来放置光标,以及做额外的显示效果。需要注意的是我们在这里指的是ContentEditable
实现的编辑器,如果是自绘选区的编辑器则不一定需要这部分设计。
我们先来聊一下额外的显示效果,举个例子,我们在选择飞书文档文本内容,如果选中到文本末尾时,会发现末尾会额外多出形似xxx|
的效果。在平时不关注的话可能会觉得这是编辑器默认行为,但是实际上这个效果无论是slate
还是quill
中都是不存在的。
实际上这个效果就是使用零宽字符来实现的,在行内容的末尾后面插入零宽字符,就可以做到末尾的文本选中效果。实际上这个效果在word
中更常见,也就是额外渲染的回车符号。
那么在这个零宽字符如果只是渲染效果的话,那么可能实际上起的作用并不很必要。但是在交互上这个效果却很有用,例如此时我们有3
行文本,如果此时从第1
行末尾选到第2
行时,并且按下Tab
键,那么此时这两行的内容就会缩进。
那么如果没有这个显示效果,此时进行缩进操作,用户可能认为仅仅是选中了第2
行,但是实际上是选中了1/2
两行文本。这样的话用户可能会以为是BUG
,而我们也实际接受过这个交互效果的反馈。
也对各个在线文档实现进行了简单调研: 基于contenteditable
实现的编辑器中,飞书文档、早期EtherPad
存在这个交互实现;自绘选区的编辑器中,钉钉文档存在这个实现;Canvas
引擎实现的编辑器中,腾讯文档、Google Doc
存在这个实现。
在渲染效果部分,零宽字符还有一个重要的作用是撑起行内容。当我们的行内容为空时,此时这个行DOM
结构的内容就是空,这就导致此行的高度塌陷为0
,且无法放置光标。为了解决这个问题,我们可以选择在行内容中插入零宽字符,这样就可以撑起行内容且可以放置光标。当然使用<br>
来撑起行高也是可以的,使用这两种方案会各有优劣,且兼容性方面也有所不同。
在类似于Notion
这种块结构的编辑器中,还有个比较重要的交互效果。即块级结构独立选择,例如我们可以直接将整个代码块独立选出来,而不是仅仅能选择其中的文本。这种效果在目前的开源编辑器很少有实现,都是需要自行以块结构重新组织设计选区。
通常来说,这个交互同样可以使用零宽字符来实现。因为我们的选区通常是需要放置在文本节点上的,因此我们很容易可以想到,可以在块结构所在行的末尾放置零宽字符,当选区在零宽字符上时就将整个块选中。这里用零宽字符而不是<br>
的好处是,零宽字符本身就是零宽,不会引起额外的换行。
在结构上,零宽字符还有个非常重要的实现。在编辑器内的contenteditable=false
节点会存在特殊的表现,在类似于inline-block
节点中,例如Mention
节点中,当节点前后没有任何内容时,我们就需要在其前后增加零宽字符,用以放置光标。
在下面的例子中,line-1
是无法将光标放置在@xxx
内容后的,虽然我们能够将光标放置之前,但此时光标位置是在line node
上,是不符合我们预期的文本节点的。那么我们就必须要在其后加入零宽字符,在line-2/3
中我们就可以看到正确的光标放置效果。这里的0.1px
也是个为了兼容光标的放置的magic
,没有这个hack
的话,非同级节点光标同样无法放置在inline-block
节点后。
除此之外,编辑器自然是需要跟字符打交道的,那么在js
表现出来的Unicode
编码实现中,emoji
就是最常见且容易出问题的表达。除了其单个长度为2
这种情况外,组合的emoji
也是使用独特的零宽连字符\u200d
来表示的。
编辑器数据结构的设计是影响面非常广的事情,无论是在维护编辑器的文本内容、块结构嵌套、序列化反序列化等,还是平台应用层面上的diff
算法、查找替换、协同算法等,以及后端服务的数据转换、导出md/word/pdf
、数据存储等,都会涉及到编辑器的数据结构设计。
通常来说,基于JSON
嵌套的数据结构来表达编辑器Model
是很常见的,例如Slate
、ProseMirror
、Lexical
等等。以slate
编辑器为例,无论是数据结构还是选区的设计,都尽可能倾向于HTML
的设计,因此可以存在诸多层级节点的嵌套。
通过线性的扁平结构来表达文档内容也是常见的实现方案,例如Quill
、EtherPad
、Google Doc
等等。以quill
编辑器为例,其内容上的数据结构表达不会存在嵌套,当然本质上还是JSON
结构,而选区则采用了更精简的表达。
当然还有很多特别的数据结构设计,例如vscode/monaco
的piece table
数据结构。代码编辑器又何尝不是一种富文本编辑器,毕竟其是可以支持代码高亮的功能的,只不过类似piece table
的结构我还没有太深入研究。
在这里我希望能够以线性的数据结构来表达整个富文本结构,虽然嵌套的结构能够更加直观地表达文档内容,但是对于内容的操作起来会更加复杂,特别是存在嵌套的内容时。以slate
为例,在0.50
之前的版本API
设计非常复杂,需要比较大的理解成本,虽然之后将其简化了不少:
从这里可以看出来,slate
对于文档内容的完整操作是需要9
种类型的Op
。而如果是基于线性结构的话,我们就只需要三种类型的操作,即可表达整个文档的操作。当然对于一些类似Move
的操作,则需要额外的选区Range
计算处理,相当于将计算成本移交到了应用层。
此外,嵌套结构的normalize
会变得很复杂,且变更造成的时间复杂度也会变高,特别是脏路径标记算法,以及标记后的数据处理也需要由上述Op
处理。还有用户操作导致的嵌套层级无法非常好地控制,就要normalize
过程时规范数据,否则下面例如粘贴HTML
时就可能会出现大量的数据嵌套。
再举个更加实用的例子,如果我们此时存在格式的嵌套内容。例如quote
与list
两种格式嵌套,如果此时我们文档的数据结构是嵌套结构,那么操作内容就会存在ul > quote
或者quote > ul
的两种情况,正常情况下我们必须要设计规则来做normalize
;而扁平结构下,属性全部写在attrs
内,不同操作造成的数据格式变更是完全幂等的。
扁平的数据结构在数据处理方面会存在优势,而在视图层面上,扁平的数据结构表达结构化的数据会是比较困难的,例如表达代码块、表格等嵌套结构。但是这件事并非是不可行的,例如Google Doc
的复杂表格嵌套就是完全的线性结构,这其中是存在很巧妙的设计在里边的,在这里先不展开了。
此外,如果我们需要实现在线文档的编辑器的话,在整个管理流程中可能会需要diff
,即取得两边数据结构的增删改。这种情况下扁平的数据结构能够更好地处理文本内容,而JSON
嵌套结构的数据则会麻烦很多。还有一些其他关于数据处理方面的周边应用,整体复杂度都要提升不少。
最后还是有协同相关的实现,协同算法是富文本编辑器的可选模块。无论是基于OT
的协同算法,还是Op-Based CRDT
的协同算法,都是需要传输上述的op
类型与数据的,那么很显然9
种操作的op
类型会比3
种操作的op
类型更加复杂。
因此,我希望能够以线性的数据结构来实现整个编辑器结构,这样quill
的delta
就是非常好的选择。但是quill
是自行实现的视图层结构,并非是可以组合react
等视图层的形式,组合这些视图层的优势就是可以直接使用组件库样式来实现编辑器,而避免了每个组件都需要自行实现。那么这里我准备基于quill
的数据结构,来从零实现富文本编辑器核心层,并且像slate
一样以此组合基本的视图层。
其实这里有个有趣的问题,为什么用不到1mb
的代码量就可以实现部分类似office word
编辑器的能力,是因为浏览器已经帮我们做了很多事情,并通过API
提供给开发者,包括输入法处理、字体解析、排版引擎、视图渲染等等。
因此我们是需要设计出如何跟浏览器交互的方案,毕竟我们实际上是需要跟浏览器交互的。而对于富文本编辑器最经典的描述则是分为了三级:
L0
: 基于浏览器提供的ContentEditable
实现富文本编辑,使用浏览器的document.execCommand
执行命令操作。 是作为早期轻量编辑器,可以较短时间内快速完成开发,但可定制的空间非常有限。L1
: 同样基于浏览器提供的ContentEditable
实现富文本编辑,但数据驱动可以自定义数据模型与命令的执行。常见的实现有语雀、飞书文档等等,可以满足绝大部分使用场景,但无法突破浏览器自身的排版效果。 |L2
: 基于Canvas
自主实现排版引擎,只依赖少量的浏览器API
。常见的实现有Google Docs
、腾讯文档等等,具体实现需要完全由自己控制排版,相当于使用画板而不是DOM
来绘制富文本,技术难度相当高。实际上在目前的开源产品中,这三种类型的编辑器都有涉及到,特别是绝大多数开源的都是L1
类型的实现。而这其中还分化了不依赖ContentEditable
却也不是完全自绘引擎,而是依赖DOM
呈现内容外加自绘选区的实现,实际上倒是可以算作L1.5
的级别。
本着学习的目的,自然要选择开源产品多的实现,这样遇到问题可以更好地借鉴和分析相关内容。因此我同样打算选择基于ContentEditable
,实现数据驱动的标准MVC
模型的富文本编辑器,基于这种方式来与浏览器交互,实现基本的富文本编辑能力。在此之前,我们还是先了解一下基本的编辑器实现:
如果我们仅仅需要最基本的行内样式,例如加粗、斜体、下划线等,这可能在一些基本输入框中是足够的,那么我们自然可以选择使用execCommand
来实现。甚至直接基于execCommand
的好处就是,其体积会非常小,例如 pell 的实现,仅仅需要3.54KB
的代码体积,此外还有 react-contenteditable 等实现。
我们也可以实现可以加粗的最小DEMO
,execCommand
命令可以在contenteditable
元素中选区内的元素执行,document.execCommand
方法接受三个参数,分别是命令名称、显示用户界面、命令参数。显示用户界面一般都是false
,Mozilla
没有实现,而命令参数则是可选的,例如超链接命令则需要传递具体链接地址。
当然,这个示例过于简单,我们还可以在选区变换的时候,来判断加粗按钮的加粗状态,以此来显示当前选区状态。不过我们需要对齐execCommand
的命令行为,前边也提到了可控性非常差,因此我们需要通过document.createTreeWalker
迭代所有的选区节点,以此来判断当前选区的状态。
其实这里还需要注意的是,execCommand
命令的行为在各个浏览器的表现是不一致的,这也是之前我们提到的浏览器兼容行为的一种,然而这些行为我们也没有任何办法去控制,这都是其默认的行为:
contenteditable
编辑器的情况下,直接按下回车键,在Chrome
中的表现是会插入<div><br></div>
,而在FireFox(<60)
中的表现是会插入<br>
,IE
中的表现是会插入<p><br></p>
。123|123
,在Chrome
中的表现内容是123<div>123</div>
,而在FireFox
中的表现则是会将内容格式化为<div>123</div><div>123</div>
。123|123->123123
,在Chrome
中的表现内容会恢复原本的123123
,而在FireFox
中的表现则是会变为<div>123123</div>
。("formatBlock", false, "P")
命令,在Chrome
中的表现是会将两行内容包裹在同个<p>
中,而在FireFox
中的表现则是会将两行内容分别包裹<p>
标签。此外还有类似于实现加粗的功能,我们无法控制是使用<b></b>
来实现加粗还是<strong></strong>
来实现加粗。还有浏览器的兼容性问题,例如在IE
浏览器中是使用<strong></strong>
来实现加粗,在Chrome
中是使用<b></b>
来实现加粗,IE
和Safari
不支持通过heading
命令来实现标题命令等等。且对于一些比较复杂的功能,例如图片、代码块等等,是无法很好实现的。
当然,默认的行为并不是完全没有用的,在某些情况下,我们可能要实现纯HTML
的编辑器。毕竟如果在基于MVC
模式的编辑器实现中,会处理掉对Model
来说无效的数据内容,这样就导致原本的HTML
内容丢失,因此在这种需求背景下依赖浏览器的默认行为可能是最有效的,这种情况下我们可能主要关注的就是XSS
的处理了。
ContentEditable
是HTML5
中的一个属性,可以让元素变得可编辑,再配合上内置的execCommand
就是我们上边聊的最基本DEMO
。那么如果要实现最基本的文本编辑器,就只需要在地址栏中输入下面的内容:
那么通过document.execCommand
来执行命令修改HTML
的方案虽然简单,我们也聊过了其可控性很差。除了上述的execCommand
命令执行兼容性问题之外,还有很多DOM
上的需要兼容处理的行为,例如下面存在简单加粗格式的句子:
有许多方式可以表达这样的内容,编辑器可以认为显示效果是等价的,此时可能也需要对此类DOM
结构等同处理:
但是这里仅仅是视觉上相等,将其完整对应到Model
上时,自然会是件麻烦的事。除此之外,选区的表达同样也是复杂的问题,以下面的DOM
结构为例:
如果我们要表达选区折叠在4
这个字符左侧时,同样会出现多种表达可以实现这个位置,这实际上就会很依赖浏览器的默认行为:
因此为了更强的扩展以及可控性,也解决数据与视图无法对应的问题,L1
的富文本编辑器使用了自定义数据模型的概念。即在DOM
树的基础上抽离出来的数据结构,相同的数据结构可以保证渲染的HTML
也是相同的,配合自定义的命令直接控制数据模型,最终保证渲染的HTML
文档的一致性。对于选区的表达,则需要根据DOM
选区来不断normalize
选区Model
。
其实这也就是我们常见的MVC
模型,当执行命令时会修改当前的模型,进而表现到视图的渲染上。简单来说就是构建一个描述文档结构与内容的数据模型,并且使用自定义的execCommand
对数据描述模型进行修改。在这个阶段的富文本编辑器,通过抽离数据模型,解决了富文本中脏数据、复杂功能难以实现的问题。我们也可以大概描述流程:
而类似这种方案,无论是 quill 还是 slate 都是这样的调度。而类似于slate
的实现,通过适配器来连接React
之后,就需要更复杂的兼容处理。在React
节点中加入ContentEditable
后,会出现类似下面的warning
:
这个warning
的意思是,React
无法保证ContentEditable
中的children
不会被意外修改或复制,这可能是不被意料到的。也就是说除了React
本身是会需要执行DOM
操作的,使用了ContentEditable
之后,这个行为就变的不受控了,自然这个问题同样会出现在我们的编辑器中。
此外还有一些其他的行为,例如下面的例子中,我们无法从123
字符选中到456
上。也就是这里存在跨越ContentEditable
节点了,就不能够正常使用浏览器的默认行为来处理,虽然这个处理是很合理的,但毕竟也会对我们实现blocks
编辑器造成一些困扰。
那么其实我们是可以避免使用ContentEditable
的,设想一下即使我们没有实现编辑器,同样是可以选择页面上的文本内容的,就是我们普通的选区实现。那么如果借助原生的选区实现,然后在此基础上实现控制器层,就可以实现完全受控的编辑器。
但是这里存在一个很大的问题,就是内容的输入,因为不启用ContentEditable
的话是无法出现光标的,自然也无法输入内容。而如果我们想唤醒内容输入,特别是需要唤醒IME
输入法的话,浏览器给予的常规API
就是借助<input>
来完成,因此我们就必须要实现隐藏的<input>
来实现输入,实际上很多代码编辑器例如 CodeMirror 就是类似实现。
但是使用隐藏的<input>
就会出现其他问题,因为焦点在input
上时,浏览器的文本就无法选中了。因为在同个页面中,焦点只会存在一个位置,因此在这种情况下,我们就必须要自绘选区的实现了。例如钉钉文档、有道云笔记就是自绘选区,开源的 Monaco Editor 同样是自绘选区,TextBus 则绘制了光标,选区则是借助了浏览器实现。
在这里可以总结一下,使用ContentEditable
需要处理很多DOM
的特异行为,但是明显我们是不需要太过于处理唤醒输入这个行为。而如果不使用ContentEditable
,却使用DOM
来呈现富文本内容,则必须要借助额外的隐藏input
节点来实现输入,由于焦点问题在这种情况下就不能使用浏览器的选区行为,因此就需要自绘选区的实现。
基于Canvas
绘制我们需要的内容,颇有些文艺复兴的感觉,这种实现方式是完全不依赖DOM
的,因此可以完全控制排版引擎。那么文艺复兴指的是,基于DOM
兼容实现的任何生态都会失效,例如无障碍、SEO
、开发工具支持等等。
那么为什么要抛弃现有的DOM
生态,转而用Canvas
来绘制富文本内容。特别是富文本会是非常复杂的内容,因为除了文本外,还有图片的内容,以及很多结构话格式的内容,例如表格等。这些内容都需要自行实现,那么在Canvas
中实现这些内容其实相当于重新实现了部分skia
。
基于Canvas
绘制的编辑器,当前主要有腾讯文档、Google Doc
等,而开源的编辑器实现有 Canvas Editor。而除了文档编辑器之外,在线表格的实现基本都是Canvas
实现,例如腾讯文档Sheet
、飞书多维表格等,开源的实现有 LuckySheet。
在Google Doc
发布的Blog
中,对于使用Canvas
绘制文档主要选了两个原因:
Chrome
中双击某段文本的内容,选区会自动选择为整个单词,而在早期FireFox
中则是会自动选择一句话。类似这种行为的不一致会导致用户体验的不一致,而使用Canvas
绘制文档可以自行实现保证这种一致性。Canvas
绘制文档,可以更好地控制绘制时机,而不需要等待重绘回流,更不需要考虑DOM
本身复杂的兼容性考量,以此造成的性能损失。此外,Canvas
绘图替代繁重的DOM
操作,通过逐帧渲染和硬件加速可以提升渲染性能,从而提高用户操作的响应速度和整体使用体验。此外,排版引擎还可以控制文档的排版效果,做富文本的各种需求,我们就可能面临产品为什么不能支持像office word
那样的效果。例如如果我们编写的文字正好排满了一行,假如在这里再加一个句号,那么前边的字就会挤一挤,从而可以使这个句号是不需要换行。而如果我们再敲一个字的话,这个字是会换行的。在浏览器的排版中是不会出现这个状态的,所以假如需要突破浏览器的排版限制,就需要自己实现排版能力。
也就是说,在word
中通常是不会出现句号在段落起始的,而在浏览器中是会存在这种情况的,特别是在纯ASCII
字符的情况下。如果想规避这种排版状态的差异,就必须要自行实现排版引擎,以此来控制文档的排版效果。
此外,还有一些其他的功能,例如受控的RTL
布局、分页、页码、页眉、脚注、字体字形控制等等。特别是分页的能力,在某些需要打印的情况下,这个效果是很必要的,但是DOM
的实现在绘制前是无法得知其高度的,因此也就无法很好地实现分页的效果。除此之外,还有大表格的分页渲染效果等等,都变得难以控制。
因此,这些如果希望对齐word
的实现,就必须要用Canvas
从头造一遍。除了这些额外的功能,还有原本的浏览器基于DOM
实现的基本功能,例如输入法的支持、复制粘贴的支持、拖拽的支持等等。而基本的Canvas
是无法支持这些功能的,特别是输入法IME
的支持,以及文本选区的实现,都需要很复杂的交互实现,这样的成本自然不会是很容易接受的。
在本文我们聊了很多关于富文本编辑器基础能力的实现,特别是在DOM
结构表现和数据结构之间的设计。并且在浏览器交互的方案上,我们也聊了ExecCommand
、ContentEditable
、Canvas
实现方案的特点,简单总结了当前成熟产品以及开源编辑器,并且描述了相关实现的优缺点。
在后边我们会基于ContentEditable
实现基本的富文本编辑器,首先对于整体的架构设计,以及数据结构的操作做概述。然后开始分别实现具体的模块,例如输入模块、剪贴板模块、选区模块等等。实现编辑器从来都不是一件简单的事情,除了在核心层面的基础框架设计,应用层上也会有很多问题需要兼容处理,因此这将会是一份大工程,需要慢慢积累。