国产探花免费观看_亚洲丰满少妇自慰呻吟_97日韩有码在线_资源在线日韩欧美_一区二区精品毛片,辰东完美世界有声小说,欢乐颂第一季,yy玄幻小说排行榜完本

首頁(yè) > 編程 > JavaScript > 正文

詳解實(shí)現(xiàn)一個(gè)通用的“劃詞高亮”在線(xiàn)筆記功能

2019-11-19 11:43:45
字體:
來(lái)源:轉(zhuǎn)載
供稿:網(wǎng)友

1. 什么是“劃詞高亮”?

有些同學(xué)可能不太清楚“劃詞高亮”是指什么,下面就是一個(gè)典型的“劃詞高亮”:

上圖的示例網(wǎng)站可以點(diǎn)擊這里訪(fǎng)問(wèn)。用戶(hù)選擇一段文本(即劃詞),即會(huì)自動(dòng)將這段選取的文本添加高亮背景,用戶(hù)可以很方便地為網(wǎng)頁(yè)添加在線(xiàn)筆記。

筆者前段時(shí)間為線(xiàn)上業(yè)務(wù)實(shí)現(xiàn)了一個(gè)與內(nèi)容結(jié)構(gòu)非耦合的文本高亮筆記功能。非耦合是指不需要為高亮功能建立特殊的頁(yè)面 DOM 結(jié)構(gòu),而高亮功能對(duì)業(yè)務(wù)近乎透明。該功能核心部分具有較強(qiáng)的通用性與移植性,故拿出來(lái)和大家分享交流一下。

本文具體的核心代碼已封裝成獨(dú)立庫(kù) web-highlighter,閱讀中如有疑問(wèn)可參考其中代碼↓↓。

2. 實(shí)現(xiàn)“劃詞高亮”需要解決哪些問(wèn)題?

實(shí)現(xiàn)一個(gè)“劃詞高亮”的在線(xiàn)筆記功能需要解決的核心問(wèn)題有兩個(gè):

  1. 加高亮背景。即如何根據(jù)用戶(hù)在網(wǎng)頁(yè)上的選取,為相應(yīng)的文本添加高亮背景;
  2. 高亮區(qū)域的持久化與還原。即如何保存用戶(hù)高亮信息,并在下次瀏覽時(shí)準(zhǔn)確還原,否則下次打開(kāi)頁(yè)面用戶(hù)高亮的信息就丟失了。

一般來(lái)說(shuō),劃詞高亮的業(yè)務(wù)需求方主要是針對(duì)自己產(chǎn)出的內(nèi)容,你可以比較容易對(duì)內(nèi)容在網(wǎng)頁(yè)上的排版、HTML 標(biāo)簽等方面進(jìn)行控制。這種情況下,處理高亮需求會(huì)更方便一些,畢竟自己可以根據(jù)高亮需求調(diào)整現(xiàn)有內(nèi)容的 HTML。

而筆者面對(duì)的情況是,頁(yè)面 HTML 排版結(jié)構(gòu)復(fù)雜,且無(wú)法根據(jù)高亮需求來(lái)推動(dòng)業(yè)務(wù)改動(dòng) HTML。這也催生出了對(duì)解決方案更通用化的要求,目標(biāo)就是:針對(duì)任意內(nèi)容均可“劃詞高亮”并支持后續(xù)訪(fǎng)問(wèn)時(shí)還原高亮狀態(tài),而不用去關(guān)心內(nèi)容的組織結(jié)構(gòu)。

下面就來(lái)具體說(shuō)說(shuō),如何解決上面的兩個(gè)核心問(wèn)題。

3. 如何“加高亮背景”?

根據(jù)動(dòng)圖演示我們可以知道,用戶(hù)選擇某一段文本(下文稱(chēng)為“用戶(hù)選區(qū)”)后,我們會(huì)給這段文本加一個(gè)高亮背景。

例如用戶(hù)選擇了上圖中的文本(即藍(lán)色部分)。為其加高亮的基本思路如下:

  1. 獲取選中的文本節(jié)點(diǎn):通過(guò)用戶(hù)選擇的區(qū)域信息,獲取所有被選中的所有文本節(jié)點(diǎn);
  2. 為文本節(jié)點(diǎn)添加背景色:給這些文本節(jié)點(diǎn)包裹一層新的元素,該元素具有指定的背景顏色。

3.1. 如何獲取選中的文本節(jié)點(diǎn)?

1)Selection API

需要基于瀏覽器為我們提供的 Selection API 。它的兼容性還不錯(cuò)。如果要支持更低版本的瀏覽器則需要用 polyfill。

 

Selection API 可以返回一系列關(guān)于用戶(hù)選區(qū)的信息。那么是不是可以通過(guò)它直接獲取選取中的所有 DOM 元素呢?

很遺憾并不能。但好在它可以返回選區(qū)的首尾節(jié)點(diǎn)信息:

const range = window.getSelection().getRangeAt(0);const start = { node: range.startContainer, offset: range.startOffset};const end = { node: range.endContainer, offset: range.endOffset};

 Range 對(duì)象包含了選區(qū)的開(kāi)始與結(jié)束信息,其中包括節(jié)點(diǎn)(node)與文本偏移量(offset)。節(jié)點(diǎn)信息不用多說(shuō),這里解釋一下 offset 是指什么:例如,標(biāo)簽<p>這是一段文本的示例</p>,用戶(hù)選取的部分是“一段文本”這四個(gè)字,這時(shí)首尾的 node 均為 p 元素內(nèi)的文本節(jié)點(diǎn)(Text Node),而 startOffset 和 endOffset 分別為 2 和 6。

2)首尾文本節(jié)點(diǎn)拆分

理解了 offset 的概念后,自然就發(fā)現(xiàn)有個(gè)問(wèn)題需要解決。由于用戶(hù)選區(qū)(selection)可能只包含一個(gè)文本節(jié)點(diǎn)的一部分(即 offset 不為 0),所以我們最后得到的用戶(hù)選區(qū)所包含的節(jié)點(diǎn)里,也只希望有首尾文本節(jié)點(diǎn)的這“一部分”。對(duì)此,我們可以使用 .splitText() 拆分文本節(jié)點(diǎn):

// 首節(jié)點(diǎn)if (curNode === $startNode) { if (curNode.nodeType === 3) { curNode.splitText(startOffset); const node = curNode.nextSibling; selectedNodes.push(node); }}// 尾節(jié)點(diǎn)if (curNode === $endNode) { if (curNode.nodeType === 3) { const node = curNode; node.splitText(endOffset); selectedNodes.push(node); }}

以上代碼會(huì)依據(jù) offset 對(duì)文本節(jié)點(diǎn)進(jìn)行拆分。對(duì)于開(kāi)始節(jié)點(diǎn),只需要收集它的后半部分;而對(duì)于結(jié)束節(jié)點(diǎn)則是前半部分。

3)遍歷 DOM 樹(shù)

到目前為止,我們準(zhǔn)確找到了首尾節(jié)點(diǎn),所以下一步就是找出“中間”所有的文本節(jié)點(diǎn)。這就需要遍歷 DOM 樹(shù)。

“中間”加上引號(hào)是因?yàn)椋谝曈X(jué)上這些節(jié)點(diǎn)是位于首尾之間的,但由于 DOM 不是線(xiàn)性結(jié)構(gòu)而是樹(shù)形結(jié)構(gòu),所以這個(gè)“中間”換成程序語(yǔ)言,就是指深度優(yōu)先遍歷時(shí),位于首尾兩節(jié)點(diǎn)之間的所有文本節(jié)點(diǎn)。DFS 的方法有很多,可以遞歸,也可以用棧+循環(huán),這里就不贅述了。

需要提一下的是,由于我們是要為文本節(jié)點(diǎn)添加高亮背景,因此在遍歷時(shí)只會(huì)收集文本節(jié)點(diǎn)。

if (curNode.nodeType === 3) { selectedNodes.push(curNode);}

3.2. 如何為文本節(jié)點(diǎn)添加背景色?

這一步本身并不困難。在上一步的基礎(chǔ)上,我們已經(jīng)選出了所有被用戶(hù)選中的 文本節(jié)點(diǎn)(包括拆分后的首尾節(jié)點(diǎn))。對(duì)此,一個(gè)最直接的方法就是為其“包裹上”一個(gè)帶背景樣式的元素。

具體的,我們可以給每個(gè)文本節(jié)點(diǎn)外加上一個(gè) class 為 highlight 的 <span> 元素;而背景樣式則通過(guò) CSS .highlight 選擇器設(shè)置。

// 使用上一步中封裝的方法獲取選區(qū)內(nèi)的文本節(jié)點(diǎn)const nodes = getSelectedNodes(start, end);nodes.forEach(node => { const wrap = document.createElement('span'); wrap.setAttribute('class', 'highlight'); wrap.appendChild(node.cloneNode(false)); node.parentNode.replaceChild(wrap);});
.highlight { background: #ff9;}

這樣就可以給被選中的文字添加一個(gè)“永久”的高亮背景了。

p.s. 選區(qū)的重合問(wèn)題

然而,文本高亮里還有一個(gè)比較棘手的需求 ―― 高亮區(qū)域的重合。舉個(gè)例子,最開(kāi)始的演示圖(下圖)里,第一個(gè)高亮區(qū)域和第二個(gè)高亮區(qū)域之間存在重疊部分,即“本區(qū)域高”四個(gè)字。

這個(gè)問(wèn)題目前來(lái)看似乎還不是問(wèn)題,但在結(jié)合下面要提到的一些功能與需求時(shí),就會(huì)變成非常麻煩,甚至無(wú)法正常運(yùn)行(一些開(kāi)源庫(kù)這塊處理也不盡如人意,這也是沒(méi)有選擇它們的一個(gè)原因)。這里簡(jiǎn)單提一下,具體的情況我會(huì)放到后續(xù)對(duì)應(yīng)的地方再詳細(xì)說(shuō)。

4. 如何實(shí)現(xiàn)高亮選區(qū)的持久化與還原?

到目前我們已經(jīng)可以給選中的文本添加高亮背景了。但還有一個(gè)大問(wèn)題:

想象一下,用戶(hù)辛辛苦苦劃了很多重點(diǎn)(高亮),開(kāi)心地退出頁(yè)面后,下次訪(fǎng)問(wèn)時(shí)發(fā)現(xiàn)這些都不能保存時(shí),該有多么得沮喪。因此,如果只是在頁(yè)面上做“一次性”的文本高亮,那它的使用價(jià)值會(huì)大大降低。這也就促使我們的“劃詞高亮”功能要能夠保存(持久化)這些高亮選區(qū)并正確還原。

持久化高亮選區(qū)的核心是找到一種合適的 DOM 節(jié)點(diǎn)序列化方法。

通過(guò)第三部分可以知道,當(dāng)確定了首尾節(jié)點(diǎn)與文本偏移(offset)信息后,即可為其間文本節(jié)點(diǎn)添加背景色。其中,offset 是數(shù)值類(lèi)型,要在服務(wù)器保存它自然沒(méi)有問(wèn)題;但是 DOM 節(jié)點(diǎn)不同,在瀏覽器中保存它只需要賦值給一個(gè)變量,但想在后端保存所謂的 DOM 則不那么直接了。

4.1 序列化 DOM 節(jié)點(diǎn)標(biāo)識(shí)

所以這里的核心點(diǎn)就是找到一種方法,能夠定位 DOM 節(jié)點(diǎn),同時(shí)可以被保存成普通的 JSON Object,用以傳給后端保存,這個(gè)過(guò)程在本文中被稱(chēng)為 DOM 標(biāo)識(shí) 的“序列化”。而下次用戶(hù)訪(fǎng)問(wèn)時(shí),又可以從后端取回,然后“反序列化”為對(duì)應(yīng)的 DOM 節(jié)點(diǎn)。
有幾種常見(jiàn)的方式來(lái)標(biāo)識(shí) DOM 節(jié)點(diǎn):

  1. 使用 xPath
  2. 使用 CSS Selector 語(yǔ)法
  3. 使用 tagName + index

這里選擇了使用第三種方式來(lái)快速實(shí)現(xiàn)。需要注意一點(diǎn),我們通過(guò) Selection API 取到的首尾節(jié)點(diǎn)一般是文本節(jié)點(diǎn),而這里要記錄的 tagName 和 index 都是該文本節(jié)點(diǎn)的父元素節(jié)點(diǎn)(Element Node)的,而 childIndex 表示該文本節(jié)點(diǎn)是其父親的第幾個(gè)兒子:

function serialize(textNode, root = document) { const node = textNode.parentElement; let childIndex = -1; for (let i = 0; i < node.childNodes.length; i++) {  if (textNode === node.childNodes[i]) {   childIndex = i;   break;  } }  const tagName = node.tagName; const list = root.getElementsByTagName(tagName); for (let index = 0; index < list.length; index++) {  if (node === list[index]) {   return {tagName, index, childIndex};  } } return {tagName, index: -1, childIndex};}

通過(guò)該方法返回的信息,再加上 offset 信息,即定位選取的起始位置,同時(shí)也完全可發(fā)送給后端進(jìn)行保存了。

4.2 反序列化 DOM 節(jié)點(diǎn)

基于上一節(jié)的序列化方法,從后端獲取到數(shù)據(jù)后,可以很容易反序列化為 DOM 節(jié)點(diǎn):

function deSerialize(meta, root = document) { const {tagName, index, childIndex} = meta; const parent = root.getElementsByTagName(tagName)[index]; return parent.childNodes[childIndex];}

至此,我們大體已經(jīng)解決了兩個(gè)核心問(wèn)題,這似乎已經(jīng)是一個(gè)可用版本了。但其實(shí)不然,根據(jù)實(shí)踐經(jīng)驗(yàn),如果僅僅是上面這些處理,往往是無(wú)法應(yīng)對(duì)實(shí)際需求的,存在一些“致命問(wèn)題”。

但不用灰心,下面會(huì)具體來(lái)說(shuō)說(shuō)所謂的“致命問(wèn)題”是什么,而又是如何解決并實(shí)現(xiàn)一個(gè)線(xiàn)上業(yè)務(wù)可用的通用“劃詞高亮”功能的。

5. 如何實(shí)現(xiàn)一個(gè)生產(chǎn)環(huán)境可用的“劃詞高亮”?

1)上面的方案有什么問(wèn)題?
首先來(lái)看看上面的方案會(huì)有什么問(wèn)題。
當(dāng)我們需要高亮文本時(shí),會(huì)為文本節(jié)點(diǎn)包裹span元素,這就改動(dòng)了頁(yè)面的 DOM 結(jié)構(gòu)。它可能會(huì)導(dǎo)致后續(xù)高亮的首尾節(jié)點(diǎn)與其 offset 信息其實(shí)是基于被改動(dòng)后的 DOM 結(jié)構(gòu)的。帶來(lái)的結(jié)果有兩個(gè):

  1. 下次訪(fǎng)問(wèn)時(shí),程序必須按上次用戶(hù)高亮的順序還原。
  2. 用戶(hù)不能隨意取消(刪除)高亮區(qū)域,只能按添加順序從后往前刪。

否則,就會(huì)有部分的高亮選區(qū)在還原時(shí)無(wú)法定位到正確的元素。
文字可能不好理解,下面我舉個(gè)例子來(lái)直觀(guān)解釋下這個(gè)問(wèn)題。

<p> 非常高興今天能夠在這里和大家分享一下文本高亮的實(shí)現(xiàn)方式。</p>

對(duì)于上面這段 HTML,用戶(hù)分別按順序高亮了兩個(gè)部分:“高興”和“文本高亮”。那么按照上面的實(shí)現(xiàn)方式,這段 HTML 變成了下面這樣:

<p> 非常 <span class="highlight">高興</span> 今天能夠在這里和大家分享一下 <span class="highlight">文本高亮</span> 的實(shí)現(xiàn)方式。</p>

對(duì)應(yīng)的兩個(gè)序列化數(shù)據(jù)分別為:

// “高興”兩個(gè)字被高亮?xí)r獲取的序列化信息{ start: {  tagName: 'p',  index: 0,  childIndex: 0,  offset: 2 }, end: {  tagName: 'p',  index: 0,  childIndex: 0,  offset: 4 }}
// “文本高亮”四個(gè)字被高亮?xí)r獲取的序列化信息。// 這時(shí)候由于p下面已經(jīng)存在了一個(gè)高亮信息(即“高興”)。// 所以其內(nèi)部 HTML 結(jié)構(gòu)已被修改,直觀(guān)來(lái)說(shuō)就是 childNodes 改變了。// 進(jìn)而,childIndex屬性由于前一個(gè) span 元素的加入,變?yōu)榱?2。{ start: {  tagName: 'p',  index: 0,  childIndex: 2,  offset: 14 }, end: {  tagName: 'p',  index: 0,  childIndex: 2,  offset: 18 }}

可以看到,“文本高亮”這四個(gè)字的首尾節(jié)點(diǎn)的 childIndex 都被記為 2,這是由于前一個(gè)高亮區(qū)域改變了<p>元素下的DOM結(jié)構(gòu)。如果此時(shí)“高興”選區(qū)的高亮被用戶(hù)取消,那么下次再訪(fǎng)問(wèn)頁(yè)面就無(wú)法還原高亮了 ―― “高興”選區(qū)的高亮被取消了,<p>下自然就不會(huì)出現(xiàn)第三個(gè) childNode,那么 childIndex 為 2 就找不到對(duì)應(yīng)的節(jié)點(diǎn)了。這就導(dǎo)致存儲(chǔ)的數(shù)據(jù)在還原高亮選區(qū)時(shí)出現(xiàn)問(wèn)題。

此外,還記得在第三部分末尾提到的高亮選取重合問(wèn)題么?支持選取重合很容易出現(xiàn)如下的包裹元素嵌套情況:

<p> 非常 <span class="highlight">高興</span> 今天能夠在這里和大家分享一下 <span class="highlight">  文本  <span class="highlight">高涼</span> </span> 的實(shí)現(xiàn)方式。</p>

這也使得某個(gè)文本區(qū)域經(jīng)過(guò)多次高亮、取消高亮后,會(huì)出現(xiàn)與原 HTML 頁(yè)面不同的復(fù)雜嵌套結(jié)構(gòu)。可以預(yù)見(jiàn),當(dāng)我們使用 xpath 或 CSS selector 作為 DOM 標(biāo)識(shí)時(shí),上面提到的問(wèn)題也會(huì)出現(xiàn),同時(shí)也使其他需求的實(shí)現(xiàn)更加復(fù)雜。

到這里可以提一下其他開(kāi)源庫(kù)或產(chǎn)品是如何處理選區(qū)重合問(wèn)題的:

開(kāi)源庫(kù) Rangy 有一個(gè) Highlighter 模塊可以實(shí)現(xiàn)文本高亮,但其對(duì)于選區(qū)重合的情況是將兩個(gè)選區(qū)直接合并了,這是不合符我們業(yè)務(wù)需求的。

付費(fèi)產(chǎn)品 Diigo 直接不允許選區(qū)的重合。

Medium.com 是支持選區(qū)重合的,體驗(yàn)非常不錯(cuò),這也是我們產(chǎn)品的目標(biāo)。但它頁(yè)面的內(nèi)容區(qū)結(jié)構(gòu)相較我面對(duì)的情況會(huì)更簡(jiǎn)單與更可控。

 所以如何解決這些問(wèn)題呢?

2)另一種序列化 / 反序列化方式

我會(huì)對(duì)第四部分提到的序列化方式進(jìn)行改進(jìn)。仍然記錄文本節(jié)點(diǎn)的父節(jié)點(diǎn) tagName 與 index,但不再記錄文本節(jié)點(diǎn)在 childNodes 中的 index 與 offset,而是記錄開(kāi)始(結(jié)束)位置在整個(gè)父元素節(jié)點(diǎn)中的文本偏移量。

例如下面這段 HTML:

<p> 非常 <span class="highlight">高興</span> 今天能夠在這里和大家分享一下 <span class="highlight">文本高亮</span> 的實(shí)現(xiàn)方式。</p>

對(duì)于“文本高亮”這個(gè)高亮選區(qū),之前用于標(biāo)識(shí)文本起始位置的信息為childIndex = 2, offset = 14。而現(xiàn)在變?yōu)閛ffset = 18(從<p>元素下第一個(gè)文本“非”開(kāi)始計(jì)算,經(jīng)過(guò)18個(gè)字符后是“文”)。可以看出,這樣表示的優(yōu)點(diǎn)是,不管<p>內(nèi)部原有的文本節(jié)點(diǎn)被<span>(包裹)節(jié)點(diǎn)如何分割,都不會(huì)影響高亮選區(qū)還原時(shí)的節(jié)點(diǎn)定位。

據(jù)此,在序列化時(shí),我們需要一個(gè)方法來(lái)將文本節(jié)點(diǎn)內(nèi)偏移量“翻譯”為其對(duì)應(yīng)的父節(jié)點(diǎn)內(nèi)部的總體文本偏移量:

function getTextPreOffset(root, text) { const nodeStack = [root]; let curNode = null; let offset = 0; while (curNode = nodeStack.pop()) {  const children = curNode.childNodes;  for (let i = children.length - 1; i >= 0; i--) {   nodeStack.push(children[i]);  }  if (curNode.nodeType === 3 && curNode !== text) {   offset += curNode.textContent.length;  }  else if (curNode.nodeType === 3) {   break;  } } return offset;}

而還原高亮選區(qū)時(shí),需要一個(gè)對(duì)應(yīng)的逆過(guò)程:

function getTextChildByOffset(parent, offset) { const nodeStack = [parent]; let curNode = null; let curOffset = 0; let startOffset = 0; while (curNode = nodeStack.pop()) {  const children = curNode.childNodes;  for (let i = children.length - 1; i >= 0; i--) {   nodeStack.push(children[i]);  }  if (curNode.nodeType === 3) {   startOffset = offset - curOffset;   curOffset += curNode.textContent.length;   if (curOffset >= offset) {    break;   }  } } if (!curNode) {  curNode = parent; } return {node: curNode, offset: startOffset};}

3)支持高亮選區(qū)的重合

重合的高亮選區(qū)帶來(lái)的一個(gè)問(wèn)題就是高亮包裹元素的嵌套,從而使得 DOM 結(jié)構(gòu)會(huì)有較復(fù)雜的變動(dòng),增加了其他功能(交互)實(shí)現(xiàn)與問(wèn)題排查的復(fù)雜度。因此,我在 3.2. 節(jié)提到的包裹高亮元素時(shí),會(huì)再進(jìn)行一些稍復(fù)雜的處理(尤其是重合選區(qū)),以保證盡量復(fù)用已有的包裹元素,避免元素的嵌套。

在處理時(shí),將需要包裹的各個(gè)文本片段(Text Node)分為三類(lèi)情況:

  1. 完全未被包裹,則直接包裹該部分。
  2. 屬于被包裹過(guò)的文本節(jié)點(diǎn)的一部分,則使用.splitText()將其拆分。
  3. 是一段完全被包裹的文本段,不需要對(duì)節(jié)點(diǎn)進(jìn)行處理。

于此同時(shí),為每個(gè)選區(qū)生成唯一 ID,將該段文本幾點(diǎn)多對(duì)應(yīng)的 ID、以及其由于選區(qū)重合所涉及到的其他 ID,都附加包裹元素上。因此像上面的第三種情況,不需要變更 DOM 結(jié)構(gòu),只用更新包裹元素兩類(lèi) ID 所對(duì)應(yīng)的 dataset 屬性即可。

 6. 其他問(wèn)題

解決以上的一些問(wèn)題后,“文本劃詞高亮”就基本可用了。還剩下一些“小修補(bǔ)”,簡(jiǎn)單提一下。

6.1. 高亮選區(qū)的交互事件,例如 click、hover

首先,可以為每個(gè)高亮選區(qū)生成一個(gè)唯一 ID,然后在該選區(qū)內(nèi)所有的包裹元素上記錄該 ID 信息,例如用data-highlight-id屬性。而對(duì)于選取重合的部分可以在data-highlight-extra-id屬性中記錄重合的其他選區(qū)的 ID。
而監(jiān)聽(tīng)到包裹元素的 click、hover 后,則觸發(fā) highlighter 的相應(yīng)事件,并帶上高亮 ID。

6.2. 取消高亮(高亮背景的刪除)

由于在包裹時(shí)支持選區(qū)重合(對(duì)應(yīng)會(huì)有上面提到的三種情況需要處理),因此,在刪除選取高亮?xí)r,也會(huì)有三種情況需要分別處理:

直接刪除包裹元素。即不存在選區(qū)重合。
更新data-highlight-id屬性和data-highlight-extra-id屬性。即刪除的高亮 ID 與 data-highlight-id 相同。
只更新data-highlight-extra-id屬性。即刪除的高亮 ID 只在 data-highlight-extra-id 中。

6.3. 對(duì)于前端生成的動(dòng)態(tài)頁(yè)面怎么辦?

不難發(fā)現(xiàn),這種非耦合的文本高亮功能很依賴(lài)于頁(yè)面的 DOM 結(jié)構(gòu),需要保證做高亮?xí)r的 DOM 結(jié)構(gòu)和還原時(shí)的一致,否則無(wú)法正確還原出選區(qū)的起始節(jié)點(diǎn)位置。據(jù)此,對(duì)“劃詞”高亮最友好的應(yīng)該是純后端渲染的頁(yè)面,在onload監(jiān)聽(tīng)中觸發(fā)高亮選區(qū)還原的方法即可。但目前越來(lái)越多的頁(yè)面(或頁(yè)面的一部分)是前端動(dòng)態(tài)生成的,針對(duì)這個(gè)問(wèn)題該怎么處理呢?
我在實(shí)際工作中也遇到了類(lèi)似問(wèn)題 ―― 頁(yè)面的很多區(qū)域是 ajax 請(qǐng)求后前端渲染的。我的處理方式包括如下:

隔離變化范圍。將上述代碼中的“根節(jié)點(diǎn)”從documentElement換為另一個(gè)更具體的容器元素。例如我面對(duì)的業(yè)務(wù)會(huì)在 id 為 article-container 的<div>內(nèi)加載動(dòng)態(tài)內(nèi)容,那么我就會(huì)指定這個(gè) article-container 為“根節(jié)點(diǎn)”。這樣可以最大程度防止外部的 DOM 變動(dòng)影響到高亮位置的定位,尤其是頁(yè)面改版。

確定高亮選區(qū)的還原時(shí)機(jī)。由于內(nèi)容可能是動(dòng)態(tài)生成,所以需要等到該部分的 DOM 渲染完成后再調(diào)用還原方法。如果有暴露的監(jiān)聽(tīng)事件可以在監(jiān)聽(tīng)內(nèi)處理;或者通過(guò) MutationObserver 監(jiān)聽(tīng)標(biāo)志性元素來(lái)判斷該部分是否加載完成。

記錄業(yè)務(wù)內(nèi)容信息,應(yīng)對(duì)內(nèi)容區(qū)改版。內(nèi)容區(qū)的 DOM 結(jié)構(gòu)更改算是“毀滅性打擊”。如何確實(shí)有該類(lèi)情況,可以嘗試讓業(yè)務(wù)內(nèi)容展示方將段落信息等具體的內(nèi)容信息綁定在 DOM 元素上,而我在高亮?xí)r取出這些信息來(lái)冗余存儲(chǔ),改版后可以通過(guò)這些內(nèi)容信息“刷”一遍存儲(chǔ)的數(shù)據(jù)。

6.4. 其他

篇幅問(wèn)題,還有其他細(xì)節(jié)的問(wèn)題就不在這篇文章里分享了。詳細(xì)內(nèi)容可以參考 web-highlighter 這個(gè)倉(cāng)庫(kù)里的實(shí)現(xiàn)。

7. 總結(jié)

本文先從“劃詞高亮”功能的兩個(gè)核心問(wèn)題(如何高亮用戶(hù)選區(qū)的文本、如何將高亮選區(qū)還原)切入,基于 Selection API、深度優(yōu)先遍歷和 DOM 節(jié)點(diǎn)標(biāo)識(shí)的序列化這些手段實(shí)現(xiàn)了“劃詞高亮”的核心功能。然而,該方案仍然存在一些實(shí)際問(wèn)題,因此在第 5 部分進(jìn)一步給出了相應(yīng)的解決方案。

基于實(shí)際開(kāi)發(fā)的經(jīng)驗(yàn),我發(fā)現(xiàn)解決上述幾個(gè)“劃詞高亮”核心問(wèn)題的代碼具有一定通用性,因此把核心部分的源碼封裝成了獨(dú)立的庫(kù) web-highlighter,托管在 github,也可以通過(guò) npm 安裝。

其已服務(wù)于線(xiàn)上產(chǎn)品業(yè)務(wù),基本的高亮功能一行代碼即可開(kāi)啟:

(new Highlighter()).run();

兼容IE 10/11、Edge、Firefox 52+、Chrome 15+、Safari 5.1+、Opera 15+。

以上所述是小編給大家介紹的如何實(shí)現(xiàn)一個(gè)通用的“劃詞高亮”在線(xiàn)筆記功能?詳解整合,希望對(duì)大家有所幫助,如果大家有任何疑問(wèn)請(qǐng)給我留言,小編會(huì)及時(shí)回復(fù)大家的。在此也非常感謝大家對(duì)武林網(wǎng)網(wǎng)站的支持!

發(fā)表評(píng)論 共有條評(píng)論
用戶(hù)名: 密碼:
驗(yàn)證碼: 匿名發(fā)表
主站蜘蛛池模板: 赣榆县| 青浦区| 蒙城县| 玉龙| 潞城市| 柞水县| 曲麻莱县| 黔江区| 永和县| 三亚市| 吴桥县| 建德市| 沐川县| 上蔡县| 名山县| 常德市| 钟祥市| 胶州市| 乐安县| 和林格尔县| 高平市| 博湖县| 咸阳市| 永州市| 宝清县| 白沙| 河北区| 浦县| 榆社县| 延津县| 牟定县| 东乌| 墨竹工卡县| 香港| 南江县| 会东县| 成都市| 明水县| 昆明市| 玉龙| 乌拉特后旗|