function deSerialize(meta, root = document) { const {tagName, index, childIndex} = meta; const parent = root.getElementsByTagName(tagName)[index]; return parent.childNodes[childIndex]; }
至此,我们大体已经解决了两个核心问题,这似乎已经是一个可用版本了。但其实不然,根据实践经验,如果仅仅是上面这些处理,往往是无法应对实际需求的,存在一些“致命问题”。
但不用灰心,下面会具体来说说所谓的“致命问题”是什么,而又是如何解决并实现一个线上业务可用的通用“划词高亮”功能的。
5. 如何实现一个生产环境可用的“划词高亮”?
1)上面的方案有什么问题?
首先来看看上面的方案会有什么问题。
当我们需要高亮文本时,会为文本节点包裹span元素,这就改动了页面的 DOM 结构。它可能会导致后续高亮的首尾节点与其 offset 信息其实是基于被改动后的 DOM 结构的。带来的结果有两个:
下次访问时,程序必须按上次用户高亮的顺序还原。
用户不能随意取消(删除)高亮区域,只能按添加顺序从后往前删。
否则,就会有部分的高亮选区在还原时无法定位到正确的元素。
文字可能不好理解,下面我举个例子来直观解释下这个问题。
<p> 非常高兴今天能够在这里和大家分享一下文本高亮的实现方式。 </p>
对于上面这段 HTML,用户分别按顺序高亮了两个部分:“高兴”和“文本高亮”。那么按照上面的实现方式,这段 HTML 变成了下面这样:
<p> 非常 <span>高兴</span> 今天能够在这里和大家分享一下 <span>文本高亮</span> 的实现方式。 </p>
对应的两个序列化数据分别为:
// “高兴”两个字被高亮时获取的序列化信息 { start: { tagName: 'p', index: 0, childIndex: 0, offset: 2 }, end: { tagName: 'p', index: 0, childIndex: 0, offset: 4 } }
// “文本高亮”四个字被高亮时获取的序列化信息。 // 这时候由于p下面已经存在了一个高亮信息(即“高兴”)。 // 所以其内部 HTML 结构已被修改,直观来说就是 childNodes 改变了。 // 进而,childIndex属性由于前一个 span 元素的加入,变为了 2。 { start: { tagName: 'p', index: 0, childIndex: 2, offset: 14 }, end: { tagName: 'p', index: 0, childIndex: 2, offset: 18 } }
可以看到,“文本高亮”这四个字的首尾节点的 childIndex 都被记为 2,这是由于前一个高亮区域改变了<p>元素下的DOM结构。如果此时“高兴”选区的高亮被用户取消,那么下次再访问页面就无法还原高亮了 —— “高兴”选区的高亮被取消了,<p>下自然就不会出现第三个 childNode,那么 childIndex 为 2 就找不到对应的节点了。这就导致存储的数据在还原高亮选区时出现问题。
此外,还记得在第三部分末尾提到的高亮选取重合问题么?支持选取重合很容易出现如下的包裹元素嵌套情况:
<p> 非常 <span>高兴</span> 今天能够在这里和大家分享一下 <span> 文本 <span>高凉</span> </span> 的实现方式。 </p>
这也使得某个文本区域经过多次高亮、取消高亮后,会出现与原 HTML 页面不同的复杂嵌套结构。可以预见,当我们使用 xpath 或 CSS selector 作为 DOM 标识时,上面提到的问题也会出现,同时也使其他需求的实现更加复杂。
到这里可以提一下其他开源库或产品是如何处理选区重合问题的:
开源库 Rangy 有一个 Highlighter 模块可以实现文本高亮,但其对于选区重合的情况是将两个选区直接合并了,这是不合符我们业务需求的。
付费产品 Diigo 直接不允许选区的重合。
Medium.com 是支持选区重合的,体验非常不错,这也是我们产品的目标。但它页面的内容区结构相较我面对的情况会更简单与更可控。
所以如何解决这些问题呢?
2)另一种序列化 / 反序列化方式
我会对第四部分提到的序列化方式进行改进。仍然记录文本节点的父节点 tagName 与 index,但不再记录文本节点在 childNodes 中的 index 与 offset,而是记录开始(结束)位置在整个父元素节点中的文本偏移量。
例如下面这段 HTML:
<p> 非常 <span>高兴</span> 今天能够在这里和大家分享一下 <span>文本高亮</span> 的实现方式。 </p>