「高亮」功能,我的以爲不必再解釋什麼了。做爲一名程序猿,每天都會接觸高亮:寫代碼時的語法高亮;使用搜索引擎時的搜索結果高亮。做爲一名前端,若是你作過與搜索相關的功能,那麼你頗有可能就實現太高亮,本文也主要從前端的角度覆盤一下「高亮」功能實現的關鍵知識點。html
對用戶的輸入進行分詞獲得關鍵詞,根據關鍵詞搜索獲得搜索結果。再次使用關鍵詞從搜索結果中找到匹配,對匹配加上高亮樣式,即完成高亮。細分這個過程,會有如下細節點:前端
對用戶輸入分詞獲得關鍵詞node
根據關鍵詞獲得搜索結果數組
關鍵詞匹配瀏覽器
對匹配使用高亮樣式安全
前兩步通常在後臺完成,後兩步纔是咱們前端的工做,下面經過具體例子來實際演練一下。less
好比咱們有這樣的文本:「我是中國人,我愛中華人民共和國,中華人民共和國萬歲!」,這時咱們的關鍵詞是「我」,即要高亮文本中全部的我。代碼實現完整以下:編輯器
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>普通文本高亮</title> <style> .keyword-match { color: red; } </style> </head> <body> <div> 原文本:我是中國人,我愛中華人民共和國,中華人民共和國萬歲!<br /> 關鍵詞: 我 <br /> 結果以下:<br /><br /> </div> <div id="content"></div> <script> const content = document.getElementById('content'); const text = '我是中國人,我愛中華人民共和國,中華人民共和國萬歲!'; const keyword = '我'; // 根據關鍵詞,對匹配加上高亮樣式 let hightlightText = text.replace(new RegExp(`(${keyword})`, 'g'), `<span class="keyword-match">$1</span>`); // 經過innerHTML寫入匹配後的內容 content.innerHTML = hightlightText; </script> </body> </html>
上面的demo運行效果以下函數
上面的例子雖然很簡單,但已經完整實現了高亮功能,說明了高亮的實現原理。在實際應用中,咱們的關鍵詞多半不會是一個簡簡單單的「我」,而是一個List,下面咱們將關鍵詞改爲:我,中華。只需稍加修改,就能夠實現同時對「我」,「中華」兩個關鍵詞高亮搜索引擎
<body> <div> 原文本:我是中國人,我愛中華人民共和國,中華人民共和國萬歲!<br /> 關鍵詞: 我,中華 <br /> 結果以下:<br /><br /> </div> <div id="content"></div> <script> const content = document.getElementById('content'); const text = '我是中國人,我愛中華人民共和國,中華人民共和國萬歲!'; const keyword = ['我', '中華']; // 關鍵詞是一個數組 // new RegExp(`(${keyword})` 改爲 new RegExp(`(${keyword.join('|')})` let hightlightText = text.replace(new RegExp(`(${keyword.join('|')})`, 'g'), `<span class="keyword-match">$1</span>`); // 經過innerHTML寫入匹配後的內容 content.innerHTML = hightlightText; </script> </body>
運行效果:
上面的代碼彷佛已經完美了。但若是咱們將關鍵詞改爲:我,中華,中華人民共和國。再次運行看結果,會發現「中華人民共和國」這個關鍵詞沒有被高亮,而「中華」高亮了,但這並非咱們想要的結果。
注意:若是一個關鍵詞包含另外一個關鍵詞,要優先高亮長度較長的詞纔對。要修復這個Bug也很簡單,只須要將字數多的關鍵詞放到關鍵詞數組的最前面便可
<body> <div> 原文本:我是中國人,我愛中華人民共和國,中華人民共和國萬歲!<br /> 關鍵詞: 我,中華,中華人民共和國 <br /> 結果以下:<br /><br /> </div> <div id="content"></div> <div id="content2"></div> <script> const content = document.getElementById('content'); const text = '我是中國人,我愛中華人民共和國,中華人民共和國萬歲!'; const keyword = ['我', '中華', '中華人民共和國']; // 關鍵詞是一個數組 let hightlightText = text.replace(new RegExp(`(${keyword.join('|')})`, 'g'), `<span class="keyword-match">$1</span>`); // 經過innerHTML寫入匹配後的內容 content.innerHTML = hightlightText; </script> <script> const content2 = document.getElementById('content2'); const text2 = '我是中國人,我愛中華人民共和國,中華人民共和國萬歲!'; const keyword2 = ['中華人民共和國', '中華', '我']; // 將字數多的關鍵詞放到前面,保證字數多的關鍵詞優先匹配 let hightlightText2 = text.replace(new RegExp(`(${keyword2.join('|')})`, 'g'), `<span class="keyword-match">$1</span>`); content2.innerHTML = hightlightText2; </script> </body>
效果以下:
說完了普通文本的高亮,咱們來講說富文本的高亮。所謂富文本,即待高亮的字符串再也不是單獨的文本,而是html代碼。若是你須要富文本編輯器,能夠考慮百度的UEditor,富文本編輯器生成的代碼就是能夠直接插入到頁面的html串。
// 普通文本 我是中國人,我愛中華人民共和國,中華人民共和國萬歲! // 富文本,即html串 <div>我是<span style="font-size: 20px; font-weight: bold; background-color: cyan;">中國人</span>,我愛中華人民共和國,中華人民共和國萬歲!</div>
<body> 富文本高亮 <br /> <br /> <h3>未高亮</h3> <div id="text"> <div style="font-style: italic;">我是<span style="font-size: 20px; font-weight: bold; background-color: cyan;">中國人</span>,我愛中華人民共和國,中華人民共和國萬歲!</div> </div> <br /> <h3>高亮效果</h3> <div id="content"></div> <script> const content = document.getElementById('content'); const text = document.getElementById('text').innerHTML; // 富文本串 const keyword = ['中華人民共和國', '中華', '我']; let hightlightText = text.replace(new RegExp(`(${keyword.join('|')})`, 'g'), `<span class="keyword-match">$1</span>`); content.innerHTML = hightlightText; </script> </body>
這裏的高亮實現直接使用了前面普通文本高亮,彷佛也能正常工做。但前提是這個富文本串太簡單了。咱們如今考慮這樣一種狀況,若是富文本串中的元素屬性具備徹底匹配關鍵詞的內容,會發生什麼呢?
// 待高亮富文本串 <div style="font-style: italic;" data-attr="中華人民共和國">我是<span style="font-size: 20px; font-weight: bold; background-color: cyan;">中國人</span>,我愛中華人民共和國,中華人民共和國萬歲!</div>
對於上面的字符串,直接使用前面的高亮邏輯,獲得的字符串是:
<div style="font-style: italic;" data-attr="<span class="keyword-match">中華人民共和國</span>"><span class="keyword-match">我</span>是<span style="font-size: 20px; font-weight: bold; background-color: cyan;">中國人</span>,<span class="keyword-match">我</span>愛<span class="keyword-match">中華人民共和國</span>,<span class="keyword-match">中華人民共和國</span>萬歲!</div>
顯然,第一個div屬性中的「中華人民共和國」不該該被匹配到。若是直接匹配會破壞原來的富文本串,使之再也不是一個有效的html串。這是富文本高亮的最大難題,那這個難題怎麼解決呢?
固然,第一反應可能就是改正則。但根據我的經驗,對於一個模式內包含本身時,正則幾乎無能爲力。好比下面咱們常見的字符串結構:
// 這是常見的less語法。如今若是要求使用正則將最未的選擇器名稱加上「$」,你們能夠試一下,可否作到 // 原串 @media (max-width: 600px) { .header { .hd { width: 100px; } .bd { background-color: #fff; } } } // 要求結果 @media (max-width: 600px) { .header { .hd$ { // 加上$ width: 100px; } .bd$ { background-color: #fff; } } }
對於富文本串,它也多是模式自包含的字符串類型,好比
// 這是一個標準得不能再標準的html串了 <div data-html="<div>hello world!</div>">hello world!</div>
若是要使用正則去匹配上面字符串的hello內容,這個正則應該怎麼寫?先提醒一下,真正的富文本串要比這複雜太多太多,真實的用戶輸入也比這複雜太多太多。我當時作富文本高亮時,遇到這個問題也是一時找不到辦法。某天,忽然靈光一閃,不要硬碰硬啊,曲線救國嘛(這其實應該早就想到,只是走入正則的死衚衕了):若是可以先把富文本串中的html標籤去掉,剩下的不就是普通文本了嗎?匹配普通文本簡直是不要太簡單了哦。匹配完成後,再把去掉的html還原,就完成高亮匹配了。不過這裏有幾個難題:
如何去掉html標籤。別笑,這真心難,不信你試下
佔位符必定要夠特殊,不能在關鍵詞匹配時被破壞
如何還原html串
根據上面的思路,完成了新一版的高亮邏輯,代碼以下:
富文本高亮,關鍵詞:['中華人民共和國', '中華', '我']; <br /> <h3>未高亮</h3> <div id="text"> <div style="font-style: italic;" data-attr="中華人民共和國">我是<span style="font-size: 20px; font-weight: bold; background-color: cyan;">中國人</span>,我愛中華人民共和國,中華人民共和國萬歲!</div> </div> <h3>高亮效果</h3> <div id="content"></div> <script> const content = document.getElementById('content'); const text = document.getElementById('text').innerHTML; // 富文本串 const keyword = ['中華人民共和國', '中華', '我']; let hightlightText = hightlightKeyword(text, keyword.join('|')); content.innerHTML = hightlightText; /** * 高亮 * @param input - 待高亮的富文本串 * @param keyword - 由關鍵詞生成的匹配串,格式 'xxxx|xxx|x' * @returns {string} */ function hightlightKeyword(input, keyword) { let store = { length: 0 }; try { return input .replace(/^\s+/, ' ') // 去掉多餘的空白 // 去掉Html標籤,並使用特殊佔位符佔位,方便後面還原 .replace(/(<\w+[^>]*?>)|(<\/\w+[^>]*?>)/g, function(match) { var key = '\t' + store.length++; // 注意這裏使用了\t store[key] = match; return key; }) // 關鍵詞高亮 .replace(new RegExp('(' + keyword + ')', 'gi'), '<span class="keyword-match">' + '$1' + '</span>') // html標籤還原 .replace(/\t\d+/g, function(match) { return store[match] || ''; }); } catch (e) { return input; } } </script>
上面的hightlightKeyword函數已經可以知足大多數狀況下富文本高亮,博主曾經負責的一個項目,使用這個高亮邏輯安全運行1年多,也沒出現大問題。但其實,上面的高亮邏輯仍是有bug,對於一些十分特殊的富文本串仍是存在問題,好比下面的富文本串
<div> <div data-html="<div>hello world!</div>"> hello world! <div data-html="<div>hello world!</div>">hello world!</div> </div> </div>
通過屢次嘗試,碰壁,最後決定借用瀏覽器來將html轉成DOM,經過DOM操做來完成高亮。下面是高亮終極版本
<body> 富文本高亮,關鍵詞:['中華人民共和國', '中華', '我']; <br /> <h3>未高亮</h3> <div id="text"> <div data-html="<div>hello world!</div>">hello world!</div> <div> <div data-html="<div>hello world!</div>"> hello world! <div data-html="<div>hello world!</div>">hello world!</div> </div> </div> <div style="font-style: italic;" data-attr="中華人民共和國"> 我是 <span style="font-size: 20px; font-weight: bold; background-color: cyan;">中國人</span> ,我愛中華人民共和國,中華人民共和國萬歲! </div> </div> <h3>高亮效果</h3> <div id="content"></div> <script> const content = document.getElementById('content'); const text = document.getElementById('text').innerHTML; // 富文本串 const keyword = ['中華人民共和國', 'hello', '中華', '我']; let hightlightText = hightlightKeyword(text, keyword.join('|')); content.innerHTML = hightlightText; /** * 藉助瀏覽器完成高亮。 * 深度優先遍歷全部的節點,對文本節點進行高亮 * * @param input - 待高亮的富文本串 * @param keyword - 由關鍵詞生成的匹配串,格式 'xxxx|xxx|x' * @returns {string} */ function hightlightKeyword(html, keyword) { // 複製一個節點去進行遍歷操做 let wrap = document.createElement('div'); wrap.innerHTML = html; return DFSTraverseAndHightlight(wrap); function DFSTraverseAndHightlight (node) { const rootNodes = node.childNodes; const childNodes = Array.from(rootNodes); for(let i = 0, len = childNodes.length; i < len; i++) { const node = childNodes[i]; // 文本節點,要進行高亮 if (node.nodeType === 3) { let span = document.createElement('span'); let a = span.innerHTML = node.nodeValue.replace(new RegExp(`(${keyword})`, 'g'), `<span class="keyword-match">$1</span>`); console.log(node.nodeValue); node.parentNode.insertBefore(span, node); node.parentNode.removeChild(node); } //文本節點不會有childNodes屬性,若是有子節點,繼續遍歷 if (node.childNodes.length) { DFSTraverseAndHightlight(node); } } return node.innerHTML; } } </script> </body>
文章最開始說了,分詞邏輯通常是後臺經過專門的庫來完成的。但其實,前端也能夠本身實現一個簡單的分詞,只不過會產生許多無心義的詞而已,思路你們一看就明白了
// 簡單,粗暴分詞 function splitWord(word) { var len = word.length, splitWordList = word.split(''); // 一字分組 // 分詞 for(var i = 2; i <= len; i++) { for (var j = 0; j + i <= len; j++) { splitWordList.push(word.slice(j, j+i)); } } // 必須把長度最長的放到最前面,不然會形成匹配不全的狀況 return splitWordList.reverse(); }
運行效果
本文主要從前端的角度,介紹瞭如何實現高亮功能,包括普通文本高亮和富文本高亮。關鍵的知識點是:
利用new RegExp((${keyword})
, 'g')方式動態建立正則
利用str.replace(regexp, <span class="keyword-match">$1</span>
)爲匹配加上高亮樣式
最長的關鍵詞必定要優先匹配,不然會形成匹配不全的狀況
富文本匹配,用正則很難作到100%精確。但若是有瀏覽器環境,能夠藉助瀏覽器先將富文本串轉換成DOM,經過DOM操做來實現一個更精確的富文本高亮
前端也能夠本身實現分詞,只不過會產生大量無心義詞組而已