欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

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

 更新時(shí)間:2019年04月23日 10:27:18   作者:AlienZHOU  
這篇文章主要介紹了實(shí)現(xiàn)一個(gè)通用的“劃詞高亮”在線筆記文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧

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

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

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

筆者前段時(shí)間為線上業(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è)“劃詞高亮”的在線筆記功能需要解決的核心問(wèn)題有兩個(gè):

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

一般來(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ù)訪問(wèn)時(shí)還原高亮狀態(tài),而不用去關(guān)心內(nèi)容的組織結(jié)構(gòu)。

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

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

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

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

  1. 獲取選中的文本節(jié)點(diǎn):通過(guò)用戶選擇的區(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)于用戶選區(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>,用戶選取的部分是“一段文本”這四個(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)題需要解決。由于用戶選區(qū)(selection)可能只包含一個(gè)文本節(jié)點(diǎn)的一部分(即 offset 不為 0),所以我們最后得到的用戶選區(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 不是線性結(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)選出了所有被用戶選中的 文本節(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)題:

想象一下,用戶辛辛苦苦劃了很多重點(diǎn)(高亮),開(kāi)心地退出頁(yè)面后,下次訪問(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ò)程在本文中被稱為 DOM 標(biāo)識(shí) 的“序列化”。而下次用戶訪問(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è)線上業(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. 下次訪問(wèn)時(shí),程序必須按上次用戶高亮的順序還原。
  2. 用戶不能隨意取消(刪除)高亮區(qū)域,只能按添加順序從后往前刪。

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

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

對(duì)于上面這段 HTML,用戶分別按順序高亮了兩個(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)已被修改,直觀來(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ū)的高亮被用戶取消,那么下次再訪問(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)??梢灶A(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),這種非耦合的文本高亮功能很依賴于頁(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)題(如何高亮用戶選區(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ù)于線上產(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è)通用的“劃詞高亮”在線筆記功能?詳解整合,希望對(duì)大家有所幫助,如果大家有任何疑問(wèn)請(qǐng)給我留言,小編會(huì)及時(shí)回復(fù)大家的。在此也非常感謝大家對(duì)腳本之家網(wǎng)站的支持!

相關(guān)文章

  • 小程序?qū)崿F(xiàn)計(jì)算器功能

    小程序?qū)崿F(xiàn)計(jì)算器功能

    這篇文章主要為大家詳細(xì)介紹了小程序?qū)崿F(xiàn)計(jì)算器功能,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下
    2021-07-07
  • dwz 如何去掉ajaxloading具體代碼

    dwz 如何去掉ajaxloading具體代碼

    最近使用dwz來(lái)做項(xiàng)目,有時(shí)候在ajax的時(shí)候并不想使用dwz的loading,不知道有什么方法可以去掉嗎,下面為大家詳細(xì)介紹下具體的去掉方法
    2013-05-05
  • 如何更好的編寫(xiě)js async函數(shù)

    如何更好的編寫(xiě)js async函數(shù)

    本文給大家談一下如何優(yōu)化async代碼,更充分的利用異步事件流杜絕濫用async,感興趣的朋友跟隨腳本之家小編一起學(xué)習(xí)吧
    2018-05-05
  • js改變style樣式和css樣式的簡(jiǎn)單實(shí)例

    js改變style樣式和css樣式的簡(jiǎn)單實(shí)例

    下面小編就為大家?guī)?lái)一篇js改變style樣式和css樣式的簡(jiǎn)單實(shí)例。小編覺(jué)得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧
    2016-06-06
  • JS調(diào)用打印機(jī)功能簡(jiǎn)單示例

    JS調(diào)用打印機(jī)功能簡(jiǎn)單示例

    這篇文章主要介紹了JS調(diào)用打印機(jī)功能的方法,結(jié)合完整實(shí)例形式分析了javascript打印機(jī)功能的相關(guān)設(shè)置與使用技巧,需要的朋友可以參考下
    2016-11-11
  • JavaScript中定義類(lèi)的方式詳解

    JavaScript中定義類(lèi)的方式詳解

    這篇文章主要介紹了JavaScript中定義類(lèi)的方式,結(jié)合實(shí)例形式分析了JavaScript實(shí)現(xiàn)面向?qū)ο箢?lèi)的定義及使用相關(guān)技巧,并附帶了四種JavaScript類(lèi)的定義方式,需要的朋友可以參考下
    2016-01-01
  • JavaScript ECMA-262-3 深入解析(一):執(zhí)行上下文實(shí)例分析

    JavaScript ECMA-262-3 深入解析(一):執(zhí)行上下文實(shí)例分析

    這篇文章主要介紹了JavaScript ECMA-262-3 執(zhí)行上下文,結(jié)合實(shí)例形式詳細(xì)分析JavaScript ECMA執(zhí)行上下文相關(guān)概念、原理與操作注意事項(xiàng),需要的朋友可以參考下
    2020-04-04
  • 詳解JS中的對(duì)象字面量

    詳解JS中的對(duì)象字面量

    這篇文章主要介紹了JS中的對(duì)象字面量,對(duì)ES6感興趣的同學(xué),可以參考下
    2021-05-05
  • 詳解JavaScript數(shù)組的常用方法

    詳解JavaScript數(shù)組的常用方法

    這篇文章主要為大家介紹了JavaScript數(shù)組的常用方法,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下,希望能夠給你帶來(lái)幫助
    2021-11-11
  • 你知道該如何捕獲js報(bào)錯(cuò)前的用戶行為嗎

    你知道該如何捕獲js報(bào)錯(cuò)前的用戶行為嗎

    這篇文章主要給大家介紹了該如何捕獲js報(bào)錯(cuò)前的用戶行為的相關(guān)資料,文中通過(guò)實(shí)例代碼介紹的非常詳細(xì),對(duì)大家學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下
    2023-06-06

最新評(píng)論