技术人日常避坑手册:高效工作,少踩坑
邪男沸檬那么为什么要从零设计实现新的富文本编辑器编辑器是公认的天坑且当前已经有很多优秀的编辑器实现。例如极具表现力的数据结构设计Quill、结合React视图层的Draft、纯粹的编辑器引擎Slate、高度模块化的ProseMirror、开箱即用的TinyMCE/TipTap、集成协同解决方案的EtherPad等等。我也算是比较关注于各类富文本编辑器的实现包括在各个站点上的编辑器实现文章我也会看。但是我发现这其中极少有讲富文本编辑器的底层设计绝大多数都是讲的应用层例如如何使用编辑器引擎实现某某功能等。虽然这些应用层的实现本身也会有一定复杂性但是底层的设计却是更值得探讨的问题。此外我觉得富文本编辑器很类似于低代码的设计准确来说是No Code的一种实现。本质上低代码和富文本都是基于DSL的描述来操作DOM结构只不过富文本主要是通过键盘输入来操作DOM而无代码则是通过拖拽等方式来操作DOM我想这里应该是有些共通的设计思路。而我恰好前段时间都在专注于编辑器的应用层实现在具体实现的过程中也遇到了很多问题并且记录了相关文章。然而在应用层实现的过程中遇到了很多我个人觉得可以优化的地方特别是在数据结构层面上希望能够将我的一些想法应用出来。而具体来说主要有下面的几个原因:编辑器专栏#纸上得来终觉浅绝知此事要躬行。我的博客是从20年开始写的记录的内容很多基本上是想到什么就写什么毕竟是作为平时学习的记录。然后在24年写了比较多的富文本编辑器的文章主要是整理了平时遇到的问题以及解决方案集中在应用层的设计上例如:初探富文本之文档虚拟滚动初探富文本之OT协同算法...此外前段时间还研究了slate富文本编辑器相关的实现并且也给slate的仓库提过一些PR。还写了一些slate相关的文章并且还基于slate实现了一个文档编辑器同样也是比较关注于应用层的实现例如:WrapNode数据结构与操作变换Node节点与Path路径映射...在实现了诸多的应用层的功能之后发现整个编辑器有很多可以深入研究的地方。特别是有些实现看似很理所当然但是仔细研究起来会发现这其中有很多细节可以探究例如在DOM结构后常见的零宽字符、Mention节点的渲染等等这些内容都可以单独拿出来记录文章这其实就是我想从零实现编辑器的最重要原因。24年开始写了很多业务上的东西到了25年就略感题穷而目前我也没有别的擅长的方面由此写编辑器相关的内容是比较好的选择这样对于文章的选题也会简单些。不过虽然想的是深入写编辑器相关的内容但是在平时遇到问题的时候还是会记录下来例如最近有个基于immer配合OT-JSON实现的状态管理的想法可以实现。而对于编辑器的具体实现我目前的目标是实现可用的编辑器而不是兼容性非常好且功能完备的编辑器。主要是现在已经有非常多优秀的编辑器实现且有很多生态插件可以支持能够满足大部分的需求。目前我想实现的编辑器主要是兼容Chrome浏览器即可移动端的问题暂时不会考虑。不过如果能够将编辑器做得比较好的话自然可以去做兼容性适配。不过目前还是试探性地来设计并实现编辑器期间必然会遇到很多问题这些问题也将会成为专栏的主体内容。最开始的时候我是准备将编辑器完善后再开始撰写文章后来发现设计过程中的历史方案同样很有价值因此决定将设计过程也一并记录下来。如果将来真的能够将编辑器适用于生产环境那么这些文章就能够溯源到模块为什么这么设计想必也是极好的。整体来说我们不能一口吃成胖子但是一口一口吃却是可以的。深入编辑器#这部分是让我想起来一句话我们富文本编辑器是这样的你不写你不懂。编辑器是个非常注重细节的工程很多时候都需要深入研究浏览器的API例如document上的caretPositionFromPoint方法用以获取当前某个点所在的选区位置通常用于拖拽文本后的落点定位。除此之外还有很多选区相关的API例如Selection、Range等等这些都是编辑器实现的基础。那么深入编辑器底层就是很有意义的事情很多时候我们都需要跟浏览器打交道即使是对我们平时的业务开发也会有价值。在这里我想聊一下编辑器中的零宽字符以此例学习编辑器的细节设计这是一个非常有意思的话题类似这种内容就是不研究则不会关注到的有趣事情。零宽字符顾名思义是没有宽度的字符因此就很容易推断出这些字符在视觉上是不显示的。因此这些字符就可以作为不可见的占位内容实现特殊的效果。例如可以实现信息隐藏以此来实现水印的功能以及加密的信息分享等等某些小说站点会通过这种方式以及字形替换来追溯盗版。而在富文本编辑器中如果我们在开发者工具检查元素时可能会发现一些类似于​即U200B类似的字符这就是常见的零宽字符。例如在飞书文档的编辑器中我们通过([data-enter])就可以检查到其中存在的零宽字符。Copy\u200B​那么从名字上来看这个零宽字符在视觉上是不显示的因为其是零宽度。但是在编辑器中这个字符却是很重要的。简单来说我们需要这个字符来放置光标以及做额外的显示效果。需要注意的是我们在这里指的是ContentEditable实现的编辑器如果是自绘选区的编辑器则不一定需要这部分设计。我们先来聊一下额外的显示效果举个例子我们在选择飞书文档文本内容如果选中到文本末尾时会发现末尾会额外多出形似xxx|的效果。在平时不关注的话可能会觉得这是编辑器默认行为但是实际上这个效果无论是slate还是quill中都是不存在的。实际上这个效果就是使用零宽字符来实现的在行内容的末尾后面插入零宽字符就可以做到末尾的文本选中效果。实际上这个效果在word中更常见也就是额外渲染的回车符号。Copy末尾零宽字符 Line 1​末尾零宽字符 Line 2​末尾纯文本 Line 1末尾纯文本 Line 2那么在这个零宽字符如果只是渲染效果的话那么可能实际上起的作用并不很必要。但是在交互上这个效果却很有用例如此时我们有3行文本如果此时从第1行末尾选到第2行时并且按下Tab键那么此时这两行的内容就会缩进。那么如果没有这个显示效果此时进行缩进操作用户可能认为仅仅是选中了第2行但是实际上是选中了1/2两行文本。这样的话用户可能会以为是BUG而我们也实际接受过这个交互效果的反馈。Copy123|4|x56也对各个在线文档实现进行了简单调研: 基于contenteditable实现的编辑器中飞书文档、早期EtherPad存在这个交互实现自绘选区的编辑器中钉钉文档存在这个实现Canvas引擎实现的编辑器中腾讯文档、Google Doc存在这个实现。在渲染效果部分零宽字符还有一个重要的作用是撑起行内容。当我们的行内容为空时此时这个行DOM结构的内容就是空这就导致此行的高度塌陷为0且无法放置光标。为了解决这个问题我们可以选择在行内容中插入零宽字符这样就可以撑起行内容且可以放置光标。当然使用来撑起行高也是可以的使用这两种方案会各有优劣且兼容性方面也有所不同。Copy​在类似于Notion这种块结构的编辑器中还有个比较重要的交互效果。即块级结构独立选择例如我们可以直接将整个代码块独立选出来而不是仅仅能选择其中的文本。这种效果在目前的开源编辑器很少有实现都是需要自行以块结构重新组织设计选区。通常来说这个交互同样可以使用零宽字符来实现。因为我们的选区通常是需要放置在文本节点上的因此我们很容易可以想到可以在块结构所在行的末尾放置零宽字符当选区在零宽字符上时就将整个块选中。这里用零宽字符而不是的好处是零宽字符本身就是零宽不会引起额外的换行。Copyxxx​在结构上零宽字符还有个非常重要的实现。在编辑器内的contenteditablefalse节点会存在特殊的表现在类似于inline-block节点中例如Mention节点中当节点前后没有任何内容时我们就需要在其前后增加零宽字符用以放置光标。在下面的例子中line-1是无法将光标放置在xxx内容后的虽然我们能够将光标放置之前但此时光标位置是在line node上是不符合我们预期的文本节点的。那么我们就必须要在其后加入零宽字符在line-2/3中我们就可以看到正确的光标放置效果。这里的0.1px也是个为了兼容光标的放置的magic没有这个hack的话非同级节点光标同样无法放置在inline-block节点后。Copyxxx​xxx​​xxx​除此之外编辑器自然是需要跟字符打交道的那么在js表现出来的Unicode编码实现中emoji就是最常见且容易出问题的表达。除了其单个长度为2这种情况外组合的emoji也是使用独特的零宽连字符\u200d来表示的。Copy??.length// 2?? \u200d ??// ?????数据结构设计#编辑器数据结构的设计是影响面非常广的事情无论是在维护编辑器的文本内容、块结构嵌套、序列化反序列化等还是平台应用层面上的diff算法、查找替换、协同算法等以及后端服务的数据转换、导出md/word/pdf、数据存储等都会涉及到编辑器的数据结构设计。通常来说基于JSON嵌套的数据结构来表达编辑器Model是很常见的例如Slate、ProseMirror、Lexical等等。以slate编辑器为例无论是数据结构还是选区的设计都尽可能倾向于HTML的设计因此可以存在诸多层级节点的嵌套。Copy[{type: paragraph,children: [{ text: editable }],},{type: ul,children: [{type: li,children: [{ text: list }],},],},];通过线性的扁平结构来表达文档内容也是常见的实现方案例如Quill、EtherPad、Google Doc等等。以quill编辑器为例其内容上的数据结构表达不会存在嵌套当然本质上还是JSON结构而选区则采用了更精简的表达。Copy[{ insert: editable\n },{ insert: list\n, attributes: { list: bullet } },];当然还有很多特别的数据结构设计例如vscode/monaco的piece table数据结构。代码编辑器又何尝不是一种富文本编辑器毕竟其是可以支持代码高亮的功能的只不过类似piece table的结构我还没有太深入研究。在这里我希望能够以线性的数据结构来表达整个富文本结构虽然嵌套的结构能够更加直观地表达文档内容但是对于内容的操作起来会更加复杂特别是存在嵌套的内容时。以slate为例在0.50之前的版本API设计非常复杂需要比较大的理解成本虽然之后将其简化了不少:Copy// https://github.com/ianstormtaylor/slate/blob/6aace0/packages/slate/src/interfaces/operation.tsexport type NodeOperation | InsertNodeOperation| MergeNodeOperation| MoveNodeOperation| RemoveNodeOperation| SetNodeOperation| SplitNodeOperation;export type TextOperation InsertTextOperation | RemoveTextOperation;从这里可以看出来slate对于文档内容的完整操作是需要9种类型的Op。而如果是基于线性结构的话我们就只需要三种类型的操作即可表达整个文档的操作。当然对于一些类似Move的操作则需要额外的选区Range计算处理相当于将计算成本移交到了应用层。Copy// https://github.com/WindRunnerMax/BlockKit/blob/c24b9e/packages/delta/src/delta/interface.tsexport interface Op {// Only one property out of {insert, delete, retain} will be presentinsert?: string;delete?: number;retain?: number;attributes?: AttributeMap;}此外嵌套结构的normalize会变得很复杂且变更造成的时间复杂度也会变高特别是脏路径标记算法以及标记后的数据处理也需要由上述Op处理。还有用户操作导致的嵌套层级无法非常好地控制就要normalize过程时规范数据否则下面例如粘贴HTML时就可能会出现大量的数据嵌套。Copy[{children: [{children: [{children: [{children: [{// ...text: content}]}]}]}]}]再举个更加实用的例子如果我们此时存在格式的嵌套内容。例如quote与list两种格式嵌套如果此时我们文档的数据结构是嵌套结构那么操作内容就会存在ul quote或者quote ul的两种情况正常情况下我们必须要设计规则来做normalize而扁平结构下属性全部写在attrs内不同操作造成的数据格式变更是完全幂等的。Copy// slate[{type: quote,children: [{type: ul,children: [{ text: text }]}],}, {type: ul,children: [{type: quote,children: [{ text: text }]}],}]// quill[{insert: text,attributes: { blockquote: true, list: bullet }}]扁平的数据结构在数据处理方面会存在优势而在视图层面上扁平的数据结构表达结构化的数据会是比较困难的例如表达代码块、表格等嵌套结构。但是这件事并非是不可行的例如Google Doc的复杂表格嵌套就是完全的线性结构这其中是存在很巧妙的设计在里边的在这里先不展开了。此外如果我们需要实现在线文档的编辑器的话在整个管理流程中可能会需要diff即取得两边数据结构的增删改。这种情况下扁平的数据结构能够更好地处理文本内容而JSON嵌套结构的数据则会麻烦很多。还有一些其他关于数据处理方面的周边应用整体复杂度都要提升不少。最后还是有协同相关的实现协同算法是富文本编辑器的可选模块。无论是基于OT的协同算法还是Op-Based CRDT的协同算法都是需要传输上述的op类型与数据的那么很显然9种操作的op类型会比3种操作的op类型更加复杂。OT.js: Text 数据类型ShareDB Rich-Text: Delta OT 数据类型ShareDB JSON0: JSON OT 数据类型ShareDB Slate: Slate OT 数据结构适配器YJS YText: Delta 数据类型实现YJS YMap/YArray: JSON 数据类型实现YJS Slate: Slate 数据结构适配器因此我希望能够以线性的数据结构来实现整个编辑器结构这样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来实现。甚至直接基于execCommand的好处就是其体积会非常小例如 pell 的实现仅仅需要3.54KB的代码体积此外还有 react-contenteditable 等实现。我们也可以实现可以加粗的最小DEMOexecCommand命令可以在contenteditable元素中选区内的元素执行document.execCommand方法接受三个参数分别是命令名称、显示用户界面、命令参数。显示用户界面一般都是falseMozilla没有实现而命令参数则是可选的例如超链接命令则需要传递具体链接地址。Copy加粗123123当然这个示例过于简单我们还可以在选区变换的时候来判断加粗按钮的加粗状态以此来显示当前选区状态。不过我们需要对齐execCommand的命令行为前边也提到了可控性非常差因此我们需要通过document.createTreeWalker迭代所有的选区节点以此来判断当前选区的状态。其实这里还需要注意的是execCommand命令的行为在各个浏览器的表现是不一致的这也是之前我们提到的浏览器兼容行为的一种然而这些行为我们也没有任何办法去控制这都是其默认的行为:在空contenteditable编辑器的情况下直接按下回车键在Chrome中的表现是会插入而在FireFox(60)中的表现是会插入IE中的表现是会插入。在有文本的编辑器中如果在文本中间插入回车例如123|123在Chrome中的表现内容是123123而在FireFox中的表现则是会将内容格式化为123123。同样在有文本的编辑器中如果在文本中间插入回车后再删除回车例如123|123-123123在Chrome中的表现内容会恢复原本的123123而在FireFox中的表现则是会变为123123。在同时存在两行文本的时候如果同时选中两行内容再执行(formatBlock, false, P)命令在Chrome中的表现是会将两行内容包裹在同个中而在FireFox中的表现则是会将两行内容分别包裹标签。...此外还有类似于实现加粗的功能我们无法控制是使用来实现加粗还是来实现加粗。还有浏览器的兼容性问题例如在IE浏览器中是使用来实现加粗在Chrome中是使用来实现加粗IE和Safari不支持通过heading命令来实现标题命令等等。且对于一些比较复杂的功能例如图片、代码块等等是无法很好实现的。当然默认的行为并不是完全没有用的在某些情况下我们可能要实现纯HTML的编辑器。毕竟如果在基于MVC模式的编辑器实现中会处理掉对Model来说无效的数据内容这样就导致原本的HTML内容丢失因此在这种需求背景下依赖浏览器的默认行为可能是最有效的这种情况下我们可能主要关注的就是XSS的处理了。ContentEditable#ContentEditable是HTML5中的一个属性可以让元素变得可编辑再配合上内置的execCommand就是我们上边聊的最基本DEMO。那么如果要实现最基本的文本编辑器就只需要在地址栏中输入下面的内容:Copydata:text/html,那么通过document.execCommand来执行命令修改HTML的方案虽然简单我们也聊过了其可控性很差。除了上述的execCommand命令执行兼容性问题之外还有很多DOM上的需要兼容处理的行为例如下面存在简单加粗格式的句子:Copy123**456**789有许多方式可以表达这样的内容编辑器可以认为显示效果是等价的此时可能也需要对此类DOM结构等同处理:Copy123456789123456789123456789但是这里仅仅是视觉上相等将其完整对应到Model上时自然会是件麻烦的事。除此之外选区的表达同样也是复杂的问题以下面的DOM结构为例:Copy123456789如果我们要表达选区折叠在4这个字符左侧时同样会出现多种表达可以实现这个位置这实际上就会很依赖浏览器的默认行为:Copy{ node: 123, offset: 3 }{ node: , offset: 0 }{ node: , offset: 0 }因此为了更强的扩展以及可控性也解决数据与视图无法对应的问题L1的富文本编辑器使用了自定义数据模型的概念。即在DOM树的基础上抽离出来的数据结构相同的数据结构可以保证渲染的HTML也是相同的配合自定义的命令直接控制数据模型最终保证渲染的HTML文档的一致性。对于选区的表达则需要根据DOM选区来不断normalize选区Model。其实这也就是我们常见的MVC模型当执行命令时会修改当前的模型进而表现到视图的渲染上。简单来说就是构建一个描述文档结构与内容的数据模型并且使用自定义的execCommand对数据描述模型进行修改。在这个阶段的富文本编辑器通过抽离数据模型解决了富文本中脏数据、复杂功能难以实现的问题。我们也可以大概描述流程:Copy而类似这种方案无论是 quill 还是 slate 都是这样的调度。而类似于slate的实现通过适配器来连接React之后就需要更复杂的兼容处理。在React节点中加入ContentEditable后会出现类似下面的warning:CopycontentEditablesuppressContentEditableWarning// A component is contentEditable and contains children managed by React. It is now your responsibility to guarantee that none of those nodes are unexpectedly modified or duplicated. This is probably not intentional.这个warning的意思是React无法保证ContentEditable中的children不会被意外修改或复制这可能是不被意料到的。也就是说除了React本身是会需要执行DOM操作的使用了ContentEditable之后这个行为就变的不受控了自然这个问题同样会出现在我们的编辑器中。此外还有一些其他的行为例如下面的例子中我们无法从123字符选中到456上。也就是这里存在跨越ContentEditable节点了就不能够正常使用浏览器的默认行为来处理虽然这个处理是很合理的但毕竟也会对我们实现blocks编辑器造成一些困扰。Copy123456那么其实我们是可以避免使用ContentEditable的设想一下即使我们没有实现编辑器同样是可以选择页面上的文本内容的就是我们普通的选区实现。那么如果借助原生的选区实现然后在此基础上实现控制器层就可以实现完全受控的编辑器。但是这里存在一个很大的问题就是内容的输入因为不启用ContentEditable的话是无法出现光标的自然也无法输入内容。而如果我们想唤醒内容输入特别是需要唤醒IME输入法的话浏览器给予的常规API就是借助来完成因此我们就必须要实现隐藏的来实现输入实际上很多代码编辑器例如 CodeMirror 就是类似实现。但是使用隐藏的就会出现其他问题因为焦点在input上时浏览器的文本就无法选中了。因为在同个页面中焦点只会存在一个位置因此在这种情况下我们就必须要自绘选区的实现了。例如钉钉文档、有道云笔记就是自绘选区开源的 Monaco Editor 同样是自绘选区TextBus 则绘制了光标选区则是借助了浏览器实现。在这里可以总结一下使用ContentEditable需要处理很多DOM的特异行为但是明显我们是不需要太过于处理唤醒输入这个行为。而如果不使用ContentEditable却使用DOM来呈现富文本内容则必须要借助额外的隐藏input节点来实现输入由于焦点问题在这种情况下就不能使用浏览器的选区行为因此就需要自绘选区的实现。Canvas#基于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那样的效果。例如如果我们编写的文字正好排满了一行假如在这里再加一个句号那么前边的字就会挤一挤从而可以使这个句号是不需要换行。而如果我们再敲一个字的话这个字是会换行的。在浏览器的排版中是不会出现这个状态的所以假如需要突破浏览器的排版限制就需要自己实现排版能力。Copy文本文本文本文本文本文本文本文本文本文本文本文本文本文本。文本文本文本文本文本文本文本文本文本文本文本文本文本文本。也就是说在word中通常是不会出现句号在段落起始的而在浏览器中是会存在这种情况的特别是在纯ASCII字符的情况下。如果想规避这种排版状态的差异就必须要自行实现排版引擎以此来控制文档的排版效果。此外还有一些其他的功能例如受控的RTL布局、分页、页码、页眉、脚注、字体字形控制等等。特别是分页的能力在某些需要打印的情况下这个效果是很必要的但是DOM的实现在绘制前是无法得知其高度的因此也就无法很好地实现分页的效果。除此之外还有大表格的分页渲染效果等等都变得难以控制。因此这些如果希望对齐word的实现就必须要用Canvas从头造一遍。除了这些额外的功能还有原本的浏览器基于DOM实现的基本功能例如输入法的支持、复制粘贴的支持、拖拽的支持等等。而基本的Canvas是无法支持这些功能的特别是输入法IME的支持以及文本选区的实现都需要很复杂的交互实现这样的成本自然不会是很容易接受的。总结#在本文我们聊了很多关于富文本编辑器基础能力的实现特别是在DOM结构表现和数据结构之间的设计。并且在浏览器交互的方案上我们也聊了ExecCommand、ContentEditable、Canvas实现方案的特点简单总结了当前成熟产品以及开源编辑器并且描述了相关实现的优缺点。

相关新闻

C# 异常处理机制详解

C# 异常处理机制详解

C# 异常处理机制详解 引言 在软件开发过程中,异常处理是保证程序健壮性和稳定性的一个重要环节。C# 提供了一种结构化的异常处理方式,利用 try、catch、finally 和 throw 关键字,帮助开发者应对程序在运行时可能发生的错误。理解和掌握 C# 异…

2026/7/6 13:27:53 阅读更多 →
不踩雷! 9个降AIGC软件测评:专科生降AI率必看攻略

不踩雷! 9个降AIGC软件测评:专科生降AI率必看攻略

在如今的学术写作中,AI 生成内容(AIGC)已经成为了许多学生和研究者必须面对的问题。尤其是在论文写作过程中,如何有效降低 AIGC 率、去除 AI 痕迹,同时又不破坏原文的逻辑和语义,是每一个专科生都需要掌握的…

2026/7/6 10:55:58 阅读更多 →
从此告别拖延 10个AI论文工具测评:专科生毕业论文写作必备神器

从此告别拖延 10个AI论文工具测评:专科生毕业论文写作必备神器

在当前学术写作日益智能化的背景下,论文写作已成为专科生毕业过程中不可回避的重要环节。然而,面对繁重的写作任务、复杂的格式要求以及不断升级的查重系统,许多学生常常陷入拖延与焦虑之中。为帮助广大专科生高效完成毕业论文,笔…

2026/7/3 5:45:22 阅读更多 →

最新新闻

开源一个 Android/Rockchip 代码审查 Skill:android-rockchip-reviewer-skill

开源一个 Android/Rockchip 代码审查 Skill:android-rockchip-reviewer-skill

摘要最近整理并开源了一个面向 Android/Rockchip 源码改动审查的 Claude Skill:android-rockchip-reviewer-skill。它不是一个通用的代码风格检查工具,而是专门用于 Android 11/12/13、Rockchip RK3566/RK3568/RK3562 等源码项目中的 Delta Review。重点…

2026/7/8 13:51:39 阅读更多 →
昆山企业仅靠短视频流量不够?试试豆顶顶主流AI获客资讯推广模式

昆山企业仅靠短视频流量不够?试试豆顶顶主流AI获客资讯推广模式

在昆山做企业服务、制造配套、本地实体生意的老板,多半都踩过短视频流量的坑:花了3个月拍了上百条视频,本地账号粉丝涨了大几千,后台播放量破百万,最后一算有效留资的客户不到20个,一半咨询的都是问“你们家…

2026/7/8 13:51:39 阅读更多 →
NAS / 服务器断电数据丢失?UPS 配套 USB关机保护板,市电断电池低自动提示服务器/电脑关机

NAS / 服务器断电数据丢失?UPS 配套 USB关机保护板,市电断电池低自动提示服务器/电脑关机

20年UPS监控产品研发,专注于UPS机房,电池巡检,环境监测等领域,提供专业,稳定可靠的应用方案和成熟的系列产品。 20年UPS监控产品研发,专注于UPS机房,电池巡检,环境监测等领域&#x…

2026/7/8 13:47:35 阅读更多 →
TLP241A光耦与GD32VF103VBT6在工业隔离设计中的应用

TLP241A光耦与GD32VF103VBT6在工业隔离设计中的应用

1. 项目背景与核心需求在工业控制和电力电子系统中,电气隔离是一个至关重要的设计考量。TLP241A光耦与GD32VF103VBT6微控制器的组合,为解决高噪声环境下的信号传输问题提供了可靠方案。这个设计主要应对三个核心挑战:安全隔离:防止…

2026/7/8 13:45:35 阅读更多 →
5分钟极速方案:用OneDragon重构你的《绝区零》游戏体验

5分钟极速方案:用OneDragon重构你的《绝区零》游戏体验

5分钟极速方案:用OneDragon重构你的《绝区零》游戏体验 【免费下载链接】ZenlessZoneZero-OneDragon 绝区零 一条龙 | 全自动 | 自动闪避 | 自动每日 | 自动空洞 | 支持手柄 项目地址: https://gitcode.com/gh_mirrors/ze/ZenlessZoneZero-OneDragon 当你打开…

2026/7/8 13:45:35 阅读更多 →
6.LVM动态扩容

6.LVM动态扩容

1.如图,提前在虚拟机中添加了三块NVMe硬盘2.使用以下命令创建vg并为创建的卷组添加pv,并查看效果vgcreate [vg名称] [块设备文件]:创建vg并为创建的卷组添加pvpvscan:查看pv信息vgdisplay [vg]:查看指定vg信息3.为vg01…

2026/7/8 13:45:34 阅读更多 →

日新闻

3大核心能力重塑《明日方舟》游戏体验:MAA自动化助手的革命性突破

3大核心能力重塑《明日方舟》游戏体验:MAA自动化助手的革命性突破

3大核心能力重塑《明日方舟》游戏体验:MAA自动化助手的革命性突破 【免费下载链接】MaaAssistantArknights 《明日方舟》小助手,全日常一键长草!| A one-click tool for the daily tasks of Arknights, supporting all clients. 项目地址: …

2026/7/8 0:00:48 阅读更多 →
MyBatis 批量操作深度优化——从 N+1 到批处理的全路径

MyBatis 批量操作深度优化——从 N+1 到批处理的全路径

MyBatis 批量操作深度优化——从 N1 到批处理的全路径 一、从"功能正确"到"性能可接受"——MyBatis 批量操作的三段式进化 MyBatis 在日常增删改查场景中几乎是无感的——实体映射直观、SQL 控制灵活。但当数据量从千级上升到十万级、百万级,许…

2026/7/8 0:00:48 阅读更多 →
工业负载控制方案:TPD2015FN与PIC18F45K22应用解析

工业负载控制方案:TPD2015FN与PIC18F45K22应用解析

1. 工业负载控制方案概述在工业自动化、电机驱动和照明控制等高需求场景中,可靠地控制电感和电阻负载是核心挑战之一。TPD2015FN作为东芝的8通道高端智能功率开关IC,配合PIC18F45K22微控制器,能够构建一套稳定、高效的负载控制系统。这套组合…

2026/7/8 0:02:48 阅读更多 →

周新闻

B站视频下载神器BiliTools:5分钟学会轻松保存任何B站内容

B站视频下载神器BiliTools:5分钟学会轻松保存任何B站内容

B站视频下载神器BiliTools:5分钟学会轻松保存任何B站内容 【免费下载链接】BiliTools A cross-platform bilibili toolbox. 跨平台哔哩哔哩工具箱,支持下载视频、番剧等等各类资源 项目地址: https://gitcode.com/GitHub_Trending/bilit/BiliTools …

2026/7/7 14:24:45 阅读更多 →
威胁模型全解析:从新手入门到实战应用,助你构建安全产品!

威胁模型全解析:从新手入门到实战应用,助你构建安全产品!

威胁模型的陌生现状在忙碌疲惫的一天里,参与了关于混合后量子密码学的讨论,应付端点攻击找茬的人,还参与留言板讨论后,发现“威胁模型”对多数人仍是陌生概念,且多被当作时髦用语。有趣的相关画作有一幅由 Embyr 创作的…

2026/7/7 12:34:47 阅读更多 →
渗透测试入门指南:从零基础到实战环境搭建

渗透测试入门指南:从零基础到实战环境搭建

1. 从“看热闹”到“入门”:我理解的渗透测试到底是什么?每次看到新闻里说某个大公司的数据被“黑”了,或者某个网站被攻击导致服务瘫痪,你是不是和我一样,心里会冒出两个念头:一是“这黑客真厉害”&#x…

2026/7/7 15:59:06 阅读更多 →

月新闻