高亮:單關鍵詞、多關鍵詞、多組多關鍵詞,從簡單到複雜實現知足多方面需求的頁面關鍵詞高亮

未經容許,請勿私自轉載javascript

前言

個人前言都是良心話,仍是姑且看一下吧:css

別人一看這個標題,心想,「怎麼又是一個老到掉牙的需求,網上一搜一大堆解決方案啦!」。沒錯!這個需求實在老土得不能再老土了,我真不想寫這樣一種需求的文章,無奈!無奈!html

現實的狀況是我想這麼舊的需求網上資料一大把一大把,雖然我知道網上資料可能有坑,可是我總不信找不到一篇好的全面的資料的。然而現實又是一次啪啪的打臉,我是沒找到,並且不少資料都是一個拷貝一個,質量良莠不齊,想必不少找資料的人也深有體會java

爲了讓別人再也不走個人老路,特此寫了此篇文章和你們分享node

我不能說我寫的文章質量槓槓滴。可是我會在這裏,客觀地指出我方案的缺點,不忽悠別人。正則表達式

寫該文章的目的只有兩個:chrome

  • 讓缺少這方面經驗的人可以信手拈來一個較爲全面的方案,對本身對公司相對負責,別qa提不少bug啦(我也是這麼過來,純粹想幫助小白)
  • 讓更有能力的人,補充完善個人方案,或者借鑑個人經驗,造出更強更全面的方案,固然,我也但願能讓我學習一下就最好了。

目錄

需求

仍是說一下這究竟是個什麼需求吧。想必你們都試過在一個網頁上,按下「ctrl + F」,而後輸入關鍵詞來找到頁面上匹配的。數組

沒錯,就是這麼一種相似的簡單的需求。可是這麼一個簡單的需求,卻暗藏殺機。這種需求(非就是這種形式)用文字明確描述一下:瀏覽器

頁面上有一個按鈕,或者一個輸入框,進行操做時,針對某些關鍵詞(任意字符串均可以,除換行符),在頁面上進行高亮顯示,注意此頁面內容是有任何可能的網頁bash

描述很抽象?那我就乾脆定一個明確的需求:

實現一個插件,在任何別人的網頁上高亮想要的關鍵詞。

這裏不說實現插件的自己,只描述高亮的方案。

接下來我將按部就班地從一個個簡單的需求到複雜的需求,告訴你這裏邊到底須要考慮什麼。

一個最簡單的方案

第一反應,想必你們都以爲用字符串來處理了吧,在字符串裏找到匹配的文字,而後用一個html元素包圍着,加上類名,css高亮!對吧,一切都感受如此天然順利~
我先不說這方案的雞肋之處,光說落實到實際處理的時候,須要作些什麼。

超簡單處理

// js
var keyword = '關鍵詞1';    // 假設這裏的關鍵詞爲「關鍵詞1」
var bodyContent = document.body.innerHTMl;  // 獲取頁面內容
var contentArray = bodyContent.split(keyword);
document.body.innerHTMl = contentArray.join('<span>' + keyword + '</span>');
複製代碼
// css
.highlight {
    background: yellow;
    color: red;
}
複製代碼

簡單處理二

這裏相對上面還沒那麼簡單,至於爲啥我說這個方案的緣由是,在後面講的複雜方案裏,須要用到這些知識。

關鍵詞的處理

上面說需求的時候講過,是針對任意關鍵詞(除換行符)進行的高亮,若是更簡單點,說只針對英文或中文,那麼能夠直接匹配了,如str.match('keyword');。可是咱們是要作一個通用的功能的話,仍是要特別針對一些轉義字符作處理的,否則如關鍵詞爲?keyword',用'?keyword'.match('?keyword');,會報錯。

我找了各類特殊字符進行了測試,最終造成了如下方法針對各類特殊字符進行了處理。

// string爲本來要進行匹配的關鍵詞
// 結果transformString爲進行處理後的要用來進行匹配的關鍵詞
var transformString = string.replace(/[.[*?+^$|()/]|\]|\\/g, '\\$&');
複製代碼

看不懂?想深究,能夠看一下這邊文章: 這是一篇男女老幼入門精通咸宜的正則筆記
反正這裏的意思就是把各類轉義字符變成普通字符,以即可以匹配出來。

匹配高亮

// js部分
var bodyContent = document.body.innerHTMl;  // 獲取頁面內容
var pattern = new RegExp(transformString, 'g'); // 生成正則表達式
// 匹配關鍵詞並替換
document.body.innerHTMl = bodyContent.replace(pattern, '<span class="highlight">$&</span>');
複製代碼
// css
.highlight {
    background: yellow;
    color: red;
}
複製代碼

缺點

把頁面的內容當成一個字符串來處理,存在不少預想不到的狀況。

  • script標籤內有匹配文本,添加高亮html元素後,致使腳本報錯。
  • 標籤屬性(特別是自定義屬性,如dats-*)存在匹配文本,添加高亮後,破壞原有功能
  • 恰好匹配文本跟某內聯樣式文本匹配上,如<div style="width: 300px;"></div>,關鍵詞恰好爲width,這時候就尷尬了,替換結果爲<div style="<span class="highlight">width</span>: 300px;"><div。這樣就破壞了本來的樣式了。
  • 還有一種狀況,如<div>右</div>,關鍵詞爲>右,這時候替換結果爲<div<span class="highlight">>右</span></div>,一樣破壞告終構。
  • 以及還有不少不少狀況,以上僅是我羅列的一些,未知的狀況實在太多了

利用DOM節點高亮(基礎版)

既然字符串的方法太多弊端了,那隻能捨棄掉了,另尋他法。 這節內容就考你們的基礎知識扎不紮實了

頁面的內容有一個DOM樹構成,其中有一種節點叫文本節點,就是咱們頁面上所能看到的文字(大部分,圖片等除外),那麼咱們只要在這些文本節點裏找到是否有咱們匹配的關鍵詞,匹配上的就對該文本節點作改造就行了。

封裝一個函數作上述處理(註釋中一個個解釋), ①內容爲上述講過:

// ①
// string爲本來要進行匹配的關鍵詞
// 結果transformString爲進行處理後的要用來進行匹配的關鍵詞
var transformString = string.replace(/[.[*?+^$|()/]|\]|\\/g, '\\$&');
var pattern = new RegExp(transformString, 'i'); // 這裏不區分大小寫

/** * ② 高亮關鍵字 * @param node - 節點 * @param pattern - 用於匹配的正則表達式,就是把上面的pattern傳進來 */
function highlightKeyword(node, pattern) {
    // nodeType等於3表示是文本節點
    if (node.nodeType === 3) {
        // node.data爲文本節點的文本內容
        var matchResult = node.data.match(pattern);
        // 有匹配上的話
        if (matchResult) {
            // 建立一個span節點,用來包裹住匹配到的關鍵詞內容
            var highlightEl = document.createElement('span');
            // 不用類名來控制高亮,用自定義屬性data-*來標識,
            // 比用類名更減小几率與本來內容重名,避免樣式覆蓋
            highlightEl.dataset.highlight = 'yes';
            // splitText相關知識下面再說,能夠先去理解了再回來這裏看
            // 從匹配到的初始位置開始截斷到本來節點末尾,產生新的文本節點
            var matchNode = node.splitText(matchResult.index);
            // 重新的文本節點中再次截斷,按照匹配到的關鍵詞的長度開始截斷,
            // 此時0-length之間的文本做爲matchNode的文本內容
            matchNode.splitText(matchResult[0].length);
            // 對matchNode這個文本節點的內容(即匹配到的關鍵詞內容)建立出一個新的文本節點出來
            var highlightTextNode = document.createTextNode(matchNode.data);
            // 插入到建立的span節點中
            highlightEl.appendChild(highlightTextNode);
            // 把本來matchNode這個節點替換成用於標記高亮的span節點
            matchNode.parentNode.replaceChild(highlightEl, matchNode);
        }
    } 
    // 若是是元素節點 且 不是script、style元素 且 不是已經標記太高亮的元素
    // 至於要區分什麼元素裏的內容不是你想要高亮的,可本身補充,這裏的script和style是最基礎的了
    // 不是已經標記太高亮的元素做爲條件之一的理由是,避免進入死循環,一直往裏套span標籤
    else if ((node.nodeType === 1)  && !(/script|style/.test(node.tagName.toLowerCase())) && (node.dataset.highlight !== 'yes')) {
        // 遍歷該節點的全部子孫節點,找出文本節點進行高亮標記
        var childNodes = node.childNodes;
        for (var i = 0; i < childNodes.length; i++) {
            highlightKeyword(childNodes[i], pattern);
        }
    }
}
複製代碼

注意這裏的pattern參數,就是上述關鍵詞處理後的正則表達式

/** css高亮樣式設置 **/
[data-highlight=yes] {
    display: inline-block;
    background: #32a1ff;
}
複製代碼

這裏用的是屬性選擇器

splitText

這個方法針對文本節點使用,IE8+都能使用。它的做用是能把文本節點按照指定位置分離出另外一個文本節點,做爲其兄弟節點,即它們是同父同母哦~ 看圖理解更清楚:

雖然這個div本來是隻有一個文本節點,後來變成了兩個,可是對實際頁面效果,看起來仍是同樣的。

語法

/** * @param offset 指定的偏移量,值爲從0開始到字符串長度的整數 * @returns replacementNode - 截出的新文本節點,不含offset處文本 */
replacementNode = textnode.splitText(offset)
複製代碼

例子

<body>
  <p id="p">example</p>

  <script type="text/javascript"> var p = document.getElementById('p'); var textnode = p.firstChild; // 將原文本節點分割成爲內容分別爲exa和mple的兩個文本節點 var replacementNode = textnode.splitText(3); // 建立一個包含了內容爲' new span '的文本節點的span元素 var span = document.createElement('span'); span.appendChild(document.createTextNode(' new span ')); // 將span元素插入到後一個文本節點('bar')的前面 p.insertBefore(span, replacementNode); // 如今的HTML結構成了<p id="p">exa<span>new span</span>mple</p> </script>
</body>
複製代碼

例子中的最後一個插入span節點的做用,就是讓你們看清楚,實際上本來一個文本節點「example」的確變成了兩個「exa」「mple」,否則加入的span節點不會處於兩者中間了。

缺點

一個基礎版的高亮方案已經造成了,解決了上述用字符串方案遇到的問題。然而,這裏也存在還需額外處理或考慮的事情。

  • 這裏的方案一次性高亮是沒問題的,可是須要屢次不一樣關鍵詞高亮呢?
  • 別人的網頁沒法預測,若是網頁上有一些隱藏文本是經過顏色來隱藏的,例如白色的背景,文本顏色也是白色的這種狀況,高亮了可能把隱藏的信息也給弄出來。(這個我也無能爲力了)

屢次高亮(單關鍵詞高亮完成版)

實現多高亮,就是實現第二次高亮的時候,把上一次的高亮痕跡給抹掉,這裏會有兩個思路:

  • 每一次高亮只對原始數據進行處理。
  • 須要一個關閉舊的高亮,而後從新對新關鍵詞高亮

只對原始數據處理

這個想法其實很好,由於感受處理起來會很簡單,每次都用基礎版的高亮方案作一次就行了,也不存在什麼污染DOM的問題(這裏說的是在已經污染DOM的基礎上再處理高亮)。主要處理手段:

// 剛進入別人頁面時就要保存原始DOM信息了
const originalDom = document.querySelector('body').innerHTML;
複製代碼
// 高亮邏輯開始...
let bodyNode = document.querySelector('body');
// 把原始DOM信息從新賦予body
bodyNode.innerHTML = originalDom
// 把原始DOM信息再次轉化爲節點對象
let bodyChildren = bodyNode.childNodes;
// 針對內容進行高亮處理
for (var i = 0; i < bodyChildren.length; i++) {
    // 這裏的pattern就是上述通過處理後的關鍵詞生成的正則,再也不贅述了
    highlightKeyword(bodyChildren[i], pattern);
}
複製代碼

這裏就是作一次高亮的主要邏輯,若是要屢次高亮,重複運行這裏的邏輯,把關鍵詞改變一下就行了。還有這裏須要理解的是,由於高亮的函數是針對節點對象來處理的,因此必定要把保存起來的DOM信息(此時爲字符串)再轉化爲節點對象。

此方案的確很簡單,看似很完美,可是這裏仍是有些問題不得不考慮一下:

  • 我一貫不傾向這種把對象轉爲字符串再轉化爲對象的作法,由於我不得知轉化裏頭會是否徹底把信息給搞過來仍是會丟失一些信息,正如你們經常使用的深拷貝一個方法JSON.parse(JSON.stringify())的弊端同樣。咱們永遠不知作別人的網站是如何生成的,會不會根據一些恰好轉化時丟失的信息來生成,這些咱們都沒法保證。所以我不太建議使用這種方法。在此次我這裏簡單作了個小測試,發現仍是有些信息會丟失,test的信息不見了。
  • 在實際應用上,存在侷限性,例若有一個場景使用該方法不是個好主意:chrome extension是做爲iframe嵌入到別人的網頁的。使用該方法的話,因爲body直接經過innerHTML從新賦值了,頁面的內容會從新刷了一遍(瀏覽器性能很差的話可能還會看到一瞬間的閃爍),而這個插件iframe也不例外,這樣的話,本來插件上的未保存內容或操做內容都會刷新成初始狀況了,反正就是把插件iframe的狀況也改了就很差了。

關閉舊高亮開啓新高亮

除了上述方法,還有這裏的一個方法。你們確定想,關閉不就是設置高亮樣式沒了嘛,對的,是這樣的,可是總的想法歸總的想法,落實到實踐,要考慮的地方卻每每不像想象中那麼easy。整體思路很簡單,找到已經高亮的節點(dataset.highlight = 'yes'),而後去掉這層包裹層就行了。

// 記住這個函數名,下面不贅述,直接調用
function closeHighlight() {
    let highlightNodeList = document.querySelectorAll('[data-highlight=yes]');
    for (let n = 0; n < highlightNodeList.length; n++) {
        let parentNode = highlightNodeList[n].parentNode;
        // 把高亮包裹層裏面的文本生成一個新的文本節點
        let textNode = document.createTextNode(highlightNodeList[n].innerText);
        // 用新的文本節點替換高亮的節點
        parentNode.replaceChild(textNode, highlightNodeList[n]);
        // 把相鄰的文本節點合成一個文本節點
        parentNode.normalize();
    }
}
複製代碼

而後針對新的關鍵詞高亮,再運行上述封裝的高亮函數。

關於normalize的解釋,詳見:

developer.mozilla.org/en-US/docs/…

這裏的意思就是把相鄰的文本節點合成一個文本節點,避免把文本給截斷了,以後高亮其餘關鍵詞無論用了。如:

<div>hello你們好</div>
複製代碼

第一個關鍵詞「hello」,高亮後關閉,本來的div只有只有一個文本子節點,如今變成了兩個了,分別爲「hello」「你們好」。那麼在此匹配「o大」這個關鍵詞時,就匹配不了。由於不在一個節點上了。

小結

至此,一個關於能過屢次使用的單個關鍵詞高亮的方案已經落幕了。有個選擇: 只對原始數據處理關閉舊高亮開啓新高亮 。各有優缺點,你們根據本身實際項目需求取捨,甚至要求更低的,直接採用最上面的各個簡單方案。

多個關鍵詞同時高亮

這裏的及如下的方案,都是基於DOM高亮—關閉舊高亮開啓新高亮方案下處理的。其實有了以上的基礎,接下來的需求都是錦上添花,不會過於複雜。

首先對關鍵詞的處理上:

// 要進行匹配的多個關鍵詞
let keywords = ['Hello', 'pekonChan'];
let wordMatchString = ''; // 用來造成最終多個關鍵詞特殊字符處理後的結果
keywords.forEach(item => {
    // 每一個關鍵詞都要作特殊字符處理
    let transformString = item.replace(/[.[*?+^$|()/]|\]|\\/g, '\\$&');
    // 用'|'來表示或,正則的意義
    wordMatchString += `|(${transformString})`;
});
wordMatchString = wordMatchString.substring(1);
// 造成匹配多個關鍵詞的正則表達式,用於開啓高亮
let pattern = new RegExp(wordMatchString, 'i');
// 造成匹配多個關鍵詞的正則表達式(無包含關係),用於關閉高亮
let wholePattern = new RegExp(`^${wordMatchString}$`, 'i');
複製代碼

以後的操做跟上述的「關閉舊高亮開啓新高亮方案」的流程是同樣的,只是對關鍵詞的處理不一樣而已。

缺點

高亮存在前後順序。什麼意思?舉例子說明,若有一組關鍵詞['證件照', '照換'],在下面的一個元素裏要高亮:

<div>證件照換背景顏色</div>
複製代碼

用上述方法高亮後結果爲:

<div><span data-highlight="yes">證件照<span>換背景顏色</div>
複製代碼

結果看到,只有「證件照」產生了高亮,是由於在生成匹配的正則時,「證件照」在前的。假設換個順序['照換', '證件照'],那麼結果就是:

<div>證件<span data-highlight="yes">照換<span>背景顏色</div>
複製代碼

這種問題,說實在的,我如今也無能爲力解決,若是你們有更好的方案,請告訴我學習一下~

分組狀況下的多個關鍵詞的高亮

這裏的需求我用例子來闡述,如圖


紅框部分是一個chrome擴展,左邊部分爲任意的別人的網頁(高亮的頁面對象),擴展裏有一個表格,

  • 其中每行都會有一組關鍵詞,
  • 視角詞露出次數列上有個眼睛的圖標,點一下就開啓該行下的關鍵詞高亮,再點一下就關閉高亮。
  • 每行之間的高亮操做能夠同時高亮,都是獨立操做的

咱們先看一下咱們已有的方案(在多個關鍵詞同時高亮方案的基礎上)在知足以上需求的不足之處

例如第一組關鍵詞高亮了,設置爲yes,第二組關鍵詞須要高亮的文本偏偏在第一組高亮文本內,是被包含關係。因爲第一組關鍵詞高亮文本已經設爲yes了,因此第二組關鍵詞開啓高亮模式的時候不會對第一組的已經高亮的節點繼續遍歷下去。不幸的是,這就形成了當第一組關鍵詞關閉高亮模式後,第二組雖然開始顯示爲開啓高亮模式,可是因爲剛剛沒有遍歷,因此本來應該在第一組高亮詞內高亮的文本,卻沒有高亮

文字很差理解?看例子,第一組關鍵詞(假設都爲單個)爲「可口可樂」,第二組爲「可樂」

表格第一行開啓高亮模式,結果:

<div>
    <span data-highlight="yes" data-highlightMatch="Hello">可口可樂</span>
</div>
複製代碼

接着,第二行也開啓高亮模式,執行highlightKeyword函數的else if這裏,因爲可口可樂外層的span已經設爲yes了,因此再也不往下遍歷了。

function highlightKeyword(node, pattern) {
    if (node.nodeType === 3) {
        ...
    } else if ((node.nodeType === 1)  && !(/script|style/.test(node.tagName.toLowerCase())) && (node.dataset.highlight !== 'yes')) {
        ...
    }
}
複製代碼

此時結果仍爲:

<div>
    <span data-highlight="yes" data-highlightMatch="Hello">可口可樂</span>
</div>
複製代碼

然而,當關閉第一行的高亮模式時,此時結果爲:

<div>可口可樂</div>
複製代碼

可是我只關了第一行的高亮,第二行仍是顯示這高亮模式,然而第二行的「可樂」關鍵詞卻沒有高亮。這就是弊端了!

設置分組

要解決上述問題,須要也爲高亮的節點設置分組。highlightKeyword函數須要作點小改造,加個index參數,並綁定在dataset裏,else if的判斷條件也須要做出一些改變,都見註釋部分:

/** * 高亮關鍵字 * @param node 節點 * @param pattern 匹配的正則表達式 * @param index - 表示第幾組關鍵詞 */
function highlightKeyword(node, pattern, index) {
    if (node.nodeType === 3) {
        let matchResult = node.data.match(pattern);
        if (matchResult) {
            let highlightEl = document.createElement('span');
            highlightEl.dataset.highlight = 'yes';
            // 把匹配結果的文本存儲在dataset裏,用於關閉高亮,詳解見下面
            highlightEl.dataset.highlightMatch = matchResult[0];
            // 記錄第幾組關鍵詞
            highlightEl.dataset.highlightIndex = index; 
            let matchNode = node.splitText(matchResult.index);
            ...
        }
    } else if ((node.nodeType === 1)  && !(/script|style/.test(node.tagName.toLowerCase()))) {
        // 若是該節點爲插件的iframe,不作高亮處理
        if (node.className === 'extension-iframe') {
            return;
        }
        // 若是該節點標記爲yes的同時,又是該組關鍵詞的,那麼就不作處理
        if (node.dataset.highlight === 'yes') {
            if (node.dataset.highlightIndex === index.toString()) {
                return;
            }
        }
        let childNodes = node.childNodes;
        for (let i = 0; i < childNodes.length; i++) {
            highlightKeyword(childNodes[i], pattern, index);
        }
    }
}
複製代碼

這樣的話,包含在第一組關鍵詞裏的別組關鍵詞也能夠繼續標爲高亮了。

有沒有留意到,上述添加了這麼一句代碼highlightEl.dataset.highlightMatch = matchResult[0];

這句用意是,用於下面說的分組關閉高亮的。根據這個信息來區分,我要關閉哪些符合內容的高亮節點,不能統一依據highlignth=yes來處理。例如,這個高亮節點匹配的是「Hello」,那麼highlightEl.dataset.highlightMatch就是「Hello」,要關閉這個由於「Hello」產生的高亮節點,就要判斷highlightEl.dataset.highlightMatch == 'Hello'

爲何我這裏會選擇用dataset的形式存關鍵詞內容,可能你們會以爲直接判斷元素裏面的innerText或者firstChid文本節點不就行了嗎,實際上,這種狀況就很差使了:

<div>
    <span data-highlight="yes" data-highlight-index="1">Hel<span data-highlight="yes" data-highlight-index="2">lo</span></span>
    , I'm pekonChan
</div>
複製代碼

當裏面的hello被拆成了幾個節點後,用innerText或者firstChid都很差使。

關閉高亮也要分組關閉

改造本來的關閉高亮函數closeHighlight,不能像以前那樣統一關閉了,在分組前,先對以前改造匹配關鍵詞的地方,再作一些補充:

// string爲本來要進行匹配的關鍵詞
let transformString = string.replace(/[.[*?+^$|()/]|\]|\\/g, '\\$&');
// 這裏有區分,變成頭尾都要匹配,用於分組關閉高亮
let wholePattern = new RegExp(`^${transformString}$`, 'i');
// 用於高亮匹配
let pattern = new RegExp(transformString, 'i');
複製代碼

爲何pattern跟以前的會有區分,由於要徹底符合(不能是包含關係)關鍵詞的時候才能設置節點高亮關閉。如要關閉關鍵詞爲「Hello」的高亮,在下面元素裏是不該該關閉的,要徹底符合「Hello」才行

<div data-highlight="no" data-highlightMatch="showHello"></div>
複製代碼

接下來是改造本來的關閉高亮函數:

function closeHighlight() {
    let highlightNodeList = document.querySelectorAll('[data-highlight=yes]');
    for (let n = 0; n < highlightNodeList.length; n++) {
        // 這裏的wholePattern就是上述的徹底匹配關鍵詞正則表達式
        if (wholePattern.test(highlightNodeList[n].dataset.highlightMatch)) {
            // 記錄要關閉的高亮節點的父節點
            let parentNode = highlightNodeList[n].parentNode;
            // 記錄要關閉的高亮節點的子節點
            let childNodes = highlightNodeList[n].childNodes;
            let childNodesLen = childNodes.length;
            // 記錄要關閉的高亮節點的下個兄弟節點
            let nextSibling = highlightNodeList[n].nextSibling;
            // 把高亮節點的子節點移動到兄弟節點前面
            for (let k = 0; k < childNodesLen; k++) {
                parentNode.insertBefore(childNodes[0], nextSibling);
            }
            // 建立空白文本節點並替換本來的高亮節點
            let flagNode = document.createTextNode('');
            parentNode.replaceChild(flagNode, highlightNodeList[n]);
            // 合併鄰近文本節點
            parentNode.normalize();
        }
    }
}
複製代碼

你們明顯看到,以前的只有innerText實現替換高亮節點的方式已經沒了,由於無論用了,由於有可能出現這種狀況:

<h1>
    <span data-highlight="yes" data-highlightIndex="1" data-highlight-match="證件照">
        證件照
        <span data-highlight="yes" data-highlightIndex="2">lo</span>
    </span>
    , I'm pekonChan
</h1>
複製代碼

若是仍是用本來的方式那麼裏面那層第二組的高亮也沒了:

<h1>
    證件照, I'm pekonChan
</h1>
複製代碼

由於要把高亮節點的全部子節點,都要保留下來,咱們只是移除個包裹層而已。

注意裏面的一個for循環,因爲每移動一次,childNodes就會變化一次,由於insertBefore方法是若是本來沒有要插入的節點,就新增插入,若是已經存在,就會剪切移動插入,移動後舊節點就會沒了。所以childNodes會變化,因此咱們只利用childNodes一開始的長度,每次插入childNodes的第一個節點(由於本來的第一個節點被移走了,第二個就會變成第一個)

缺點

其實這裏的缺點,跟上節的多個關鍵詞高亮是同樣的 傳送門

能返回匹配個數的高亮方案

看到上面的那個需求,表格視角詞露出次數列眼睛圖標旁邊還有個數字,這個其實就是能高亮的關鍵詞個數。那麼這裏也是作點小改造就能順帶計算出個數了(改動在註釋部分):

/** * 高亮關鍵字 * @param node 節點 * @param pattern 匹配的正則表達式 * @param index - 表示第幾組關鍵詞 * @returns exposeCount - 露出次數 */
function highlightKeyword(node, pattern, index) {
    let exposeCount = 0;    // 露出次數變量
    if (node.nodeType === 3) {
        let matchResult = node.data.match(pattern);
        if (matchResult) {
            let highlightEl = document.createElement('span');
            highlightEl.dataset.highlight = 'yes';
            highlightEl.dataset.highlightMatch = matchResult[0];
            highlightEl.dataset.highlightIndex = index;
            let matchNode = node.splitText(matchResult.index);
            matchNode.splitText(matchResult[0].length);
            let highlightTextNode = document.createTextNode(matchNode.data);
            highlightEl.appendChild(highlightTextNode);
            matchNode.parentNode.replaceChild(highlightEl, matchNode);
            exposeCount++;  // 每高亮一次,露出次數加一次
        }
    } else if ((node.nodeType === 1)  && !(/script|style/.test(node.tagName.toLowerCase()))) {
        if (node.className === 'eod-extension-iframe') {
            return;
        }
        if (node.dataset.highlight === 'yes') {
            if (node.dataset.highlightIndex === index.toString()) {
                return;
            }
        }
        let childNodes = node.childNodes;
        for (let i = 0; i < childNodes.length; i++) {
            highlightKeyword(childNodes[i], pattern, index);
        }
    }
    return exposeCount; // 返回露出次數
}
複製代碼

缺點

由於統計露出次數是跟着實際高亮一塊兒統計的,而正如前面所說的,這種高亮方案存在 高亮存在前後順序 的問題,所以統計的個數也會不會準確。

若是你不在意高亮個數和統計個數必定要一致的話,想要很精準的統計個數的話,我能夠提供兩個思路,但因爲篇幅問題,我就不寫出來了,看了這篇文章的都對我提的思路不會以爲很難,就是繁瑣而已:

  1. 運用上述的 只對原始數據處理 方案,針對每一個關鍵詞,都「假」作一遍高亮處理,個數跟着高亮次數而計算,可是要注意,這裏只爲了統計個數,不要真的對頁面進行高亮(若是你不要這種高亮處理的話),就能夠統計準確了。
  2. 不使用「只對原始數據處理」方案,在本來這個方案裏,能夠在data-highlight="yes"又是同組關鍵詞下,判斷被包含的視角詞是否存在,存在就露出次數加1,可是目前我還不知道該怎麼實現。

總結

感受寫了不少不少,我以爲我應該講得比較清楚吧,哪一種方案由哪一種弊端。但我要明確的是,這裏沒有說哪一種方案更好!只有剛好合適的知足需求的方案纔是好方案,若是你只是用來削蘋果的,不拿水果刀,卻拿了把殺豬刀,是能夠削啊,還能削不少東西呢。可是你以爲,這樣好嗎?

這裏也正是這個意思,我爲何不直接寫個最全面的方案出來,你們直接複製粘貼拿走不送就行了,還要囉囉嗦嗦那麼多,爲的就是讓你們自個兒根據自身需求找到更合適本身的方式就行了!

本文最後提供一個暫且最全面的方案,以方便真的着急作項目而沒空詳細閱讀我文章或不想考慮那麼多的人兒。

若本文對您有幫助,請點個贊,未經容許,請勿轉載,寫文章不易吶,都是花寶貴時間寫的~

於發文後,修改了一次,修改於2018/12/28 12:16

暫且最全方案

高亮函數

/** * 高亮關鍵字 * @param node 節點 * @param pattern 匹配的正則表達式 * @param index - 可選。本項目中特定的需求,表示第幾組關鍵詞 * @returns exposeCount - 露出次數 */
function highlightKeyword(node, pattern, index) {
    var exposeCount = 0;
    if (node.nodeType === 3) {
        var matchResult = node.data.match(pattern);
        if (matchResult) {
            var highlightEl = document.createElement('span');
            highlightEl.dataset.highlight = 'yes';
            highlightEl.dataset.highlightMatch = matchResult[0];
            (index == null) || highlightEl.dataset.highlightIndex = index;
            var matchNode = node.splitText(matchResult.index);
            matchNode.splitText(matchResult[0].length);
            var highlightTextNode = document.createTextNode(matchNode.data);
            highlightEl.appendChild(highlightTextNode);
            matchNode.parentNode.replaceChild(highlightEl, matchNode);
            exposeCount++;
        }
    }
    // 具體條件本身加,這裏是基礎條件
    else if ((node.nodeType === 1)  && !(/script|style/.test(node.tagName.toLowerCase()))) {
        if (node.dataset.highlight === 'yes') {
            if (index == null) {
                return;
            }
            if (node.dataset.highlightIndex === index.toString()) {
                return;
            }
        }
        let childNodes = node.childNodes;
        for (var i = 0; i < childNodes.length; i++) {
            highlightKeyword(childNodes[i], pattern, index);
        }
    }
    return exposeCount;
}
複製代碼

對關鍵詞進行處理(特殊字符轉義),造成匹配的正則表達式

/** * @param {String | Array} keywords - 要高亮的關鍵詞或關鍵詞數組 * @returns {Array} */
function hanldeKeyword(keywords) {
    var wordMatchString = '';
    var words = [].concat(keywords);
    words.forEach(item => {
        let transformString = item.replace(/[.[*?+^$|()/]|\]|\\/g, '\\$&');
        wordMatchString += `|(${transformString})`;
    });
    wordMatchString = wordMatchString.substring(1);
    // 用於再次高亮與關閉的關鍵字做爲一個總體的匹配正則
    var wholePattern = new RegExp(`^${wordMatchString}$`, 'i');
    // 用於第一次高亮的關鍵字匹配正則
    var pattern = new RegExp(wordMatchString, 'i');
    return [pattern, wholePattern];
}
複製代碼

關閉高亮函數

/** * @param pattern 匹配的正則表達式 */
function closeHighlight(pattern) {
    var highlightNodeList = document.querySelectorAll('[data-highlight=yes]');
    for (var n = 0; n < highlightNodeList.length; n++) {
        if (pattern.test(highlightNodeList[n].dataset.highlightMatch)) {
            var parentNode = highlightNodeList[n].parentNode;
            var childNodes = highlightNodeList[n].childNodes;
            var childNodesLen = childNodes.length;
            var nextSibling = highlightNodeList[n].nextSibling;
            for (var k = 0; k < childNodesLen; k++) {
                parentNode.insertBefore(childNodes[0], nextSibling);
            }
            var flagNode = document.createTextNode('');
            parentNode.replaceChild(flagNode, highlightNodeList[n]);
            parentNode.normalize();
        }
    }
}
複製代碼

基礎應用

// 只高亮一次
// 要匹配的關鍵詞
var keywords = 'Hello';
var patterns = hanldeKeyword(keywords);
// 針對body內容進行高亮
var bodyChildren = window.document.body.childNodes;
for (var i = 0; i < bodyChildren.length; i++) {
    highlightKeyword(bodyChildren[i], pattern[0]);
}


// 接着高亮其餘關鍵詞
// 可能須要先抹掉不須要以前不須要高亮的
keywords = 'World'; // 新關鍵詞
closeHighlight(patterns[1]);
patterns = hanldeKeyword(keywords);
// 針對新關鍵詞高亮
for (var i = 0; i < bodyChildren.length; i++) {
    highlightKeyword(bodyChildren[i], pattern[0]);
}
複製代碼
// css
.highlight {
    background: yellow;
    color: red;
}
複製代碼

未經容許,請勿私自轉載

相關文章
相關標籤/搜索