<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          HTML實(shí)現(xiàn)關(guān)鍵詞高亮字符串中匹配“跨標(biāo)簽關(guān)鍵詞”

          共 4922字,需瀏覽 10分鐘

           ·

          2020-11-28 03:19

          來源 |?https://wintc.top/article/59

          之前分享過一期《Vue關(guān)鍵詞搜索高亮》,今天,我們?cè)趤矸窒硪黄谠?/span>HTML字符串中匹配“跨標(biāo)簽關(guān)鍵詞”高亮實(shí)現(xiàn)案例,類似瀏覽器ctrl+f搜索結(jié)果。
          實(shí)現(xiàn)方案是,將文本字符串中的關(guān)鍵字搜索出來,然后使用特殊的標(biāo)簽(比如font標(biāo)簽)包裹關(guān)鍵詞替換匹配內(nèi)容,最后得到一個(gè)HTML字符串,渲染該字符串并在font標(biāo)簽上使用CSS樣式即可實(shí)現(xiàn)高亮的效果。

          一、匹配關(guān)鍵字:HTML字符串與文本字符串對(duì)比

          1. 純文本字符串的處理

          對(duì)于純文本字符串,如:“江畔何人初見月?江月何年初照人?”,假如我們想匹配“江月”這個(gè)關(guān)鍵字,則匹配結(jié)果可處理為:
          江畔何人初見月?<font style="background:?#ff9632">江月font>何年初照人?
          這樣“江月”兩個(gè)字被font標(biāo)簽包裹,在font標(biāo)簽上應(yīng)用特殊的背景樣式以達(dá)到關(guān)鍵字高亮的效果。

          2. 對(duì)HTML字符串的處理

          對(duì)于上述例子,如果內(nèi)容字符串是一個(gè)HTML文本:
          江畔何人初見<b>b>?江<b>b>何年初照人?
          對(duì)于同樣的關(guān)鍵詞“江月”,怎樣處理它呢?因?yàn)殛P(guān)鍵詞中的字在不同的標(biāo)簽內(nèi),所以只能分別用font標(biāo)簽進(jìn)行替換:
          江畔何人初見<b>b>?<font style="background: #ff9632">font><b><font style="background: #ff9632">font>b>何年初照人?
          這是比較簡(jiǎn)單的情況,實(shí)際情況下關(guān)鍵字則可能跨多級(jí)、多層標(biāo)簽。

          二、跨標(biāo)簽匹配關(guān)鍵詞

          跨標(biāo)簽解析關(guān)鍵詞,其實(shí)就是對(duì)于匹配到的關(guān)鍵詞,提取出各標(biāo)簽中對(duì)應(yīng)的子片段,然后用font之類的標(biāo)簽包裹,再將高亮樣式用于font標(biāo)簽即可。
          對(duì)于整個(gè)HTML內(nèi)容而言,渲染出來的文本由各類標(biāo)簽內(nèi)的文本節(jié)點(diǎn)組成。
          因?yàn)殛P(guān)鍵詞匹配的內(nèi)容會(huì)跨標(biāo)簽,所以需要將各文本節(jié)點(diǎn)有序取出,并將節(jié)點(diǎn)內(nèi)容拼接起來進(jìn)行匹配。
          拼接時(shí)記下節(jié)點(diǎn)文本在拼接串中的起止位置,以便關(guān)鍵詞匹配到拼接串的某位置時(shí)截取文本片段并使用font標(biāo)簽包裹。

          1. 深度優(yōu)先遍歷DOM樹取出文本節(jié)點(diǎn)

          深度優(yōu)先可以采用循環(huán)或者遞歸的方式遍歷,這里采用循環(huán)實(shí)現(xiàn),按取出某個(gè)元素下所有文本節(jié)點(diǎn)(利用nodeType判斷文本節(jié)點(diǎn)):
          function getTextNodeList (dom) {  const nodeList = [...dom.childNodes]  const textNodes = []  while (nodeList.length) {    const node = nodeList.shift()    if (node.nodeType === node.TEXT_NODE) {      textNodes.push(node)    } else {      nodeList.unshift(...node.childNodes)    }  }  return textNodes}

          2. 取出所有文本內(nèi)容進(jìn)行拼接

          獲取到了文本節(jié)點(diǎn)列表,可以取出所有文本內(nèi)容并記錄每個(gè)文本片段在拼接結(jié)果中的開始、結(jié)束索引:
          getTextInfoList (textNodes) {  let length = 0  const textList = textNodes.map(text => {    let start = length, end = length + text.wholeText.length    length = end    return [text.wholeText, start, end]  })  return textList}

          拼接文本:

          const content = textList.map(([text]) => text).join('')

          3. 匹配關(guān)鍵詞

          獲得了拼接文本,可以利用拼接文本獲取所有的拼接結(jié)果了。這里偷個(gè)懶直接用正則匹配吧,得把正則用到的一些特殊符號(hào)進(jìn)行轉(zhuǎn)義一下:
          getMatchList (content, keyword) {  const characters = [...'\\[]()?.+*^${}:'].reduce((r, c) => (r[c] = true, r), {})  keyword = keyword.split('').map(s => characters[s] ? `\\${s}` : s).join('[\\s\\n]*')  const reg = new RegExp(keyword, 'gmi')  return [...content.matchAll(reg)] // matchAll結(jié)果是個(gè)迭代器,用擴(kuò)展符展開得到數(shù)組}
          關(guān)鍵詞字符轉(zhuǎn)義處理后,字符與字符之間中間插入了正則中的空白符和換行符(\s\n),以在匹配時(shí)忽略一些看不見的字符。上述代碼使用了matchAll函數(shù),匹配結(jié)果展開后得到的結(jié)果是一個(gè)數(shù)組,數(shù)組中的每一項(xiàng)都包含了匹配文本、匹配索引等。matchAll的一個(gè)簡(jiǎn)單例子:

          4. 關(guān)鍵詞使用font標(biāo)簽替換

          根據(jù)關(guān)鍵詞匹配結(jié)果索引,以及每個(gè)文本節(jié)點(diǎn)的起止索引,可以計(jì)算出每個(gè)關(guān)鍵詞匹配了哪幾個(gè)文本節(jié)點(diǎn),其中對(duì)于開始和結(jié)束的文本節(jié)點(diǎn),可能只是部分匹配到,而中間的文本節(jié)點(diǎn)的所有內(nèi)容都是匹配到的。
          比如對(duì)于HTML文本:
          <span>江畔何人初見<b>b>?江月何年初照人?span>
          其DOM樹對(duì)應(yīng)的的文本節(jié)點(diǎn)有3個(gè):
          假如關(guān)鍵字是“何人初見月?”,那此時(shí),對(duì)于第一個(gè)文本節(jié)點(diǎn)匹配了后半部分,第二個(gè)文本節(jié)點(diǎn)完全匹配,第三個(gè)文本節(jié)點(diǎn)匹配了第一個(gè)字符。三個(gè)節(jié)點(diǎn)中匹配的部分需要分別用font標(biāo)簽替換:
          <span>江畔<font>何人初見font><b><font>font>b><font>?font>江月何年初照人?span>
          默認(rèn)情況下,連續(xù)的文字會(huì)在同一個(gè)文本節(jié)點(diǎn)中,而對(duì)于匹配了部分內(nèi)容的文本節(jié)點(diǎn),就需要將它一分為二,可以利用Text.splitText()API來分割文本節(jié)點(diǎn),API接收一個(gè)索引值,從索引位置將文本節(jié)點(diǎn)后半部分切割并返回包含后半部分內(nèi)容的新文本節(jié)點(diǎn)。上述例子中匹配的是3個(gè)節(jié)點(diǎn),拆分后就會(huì)得到5個(gè)文本節(jié)點(diǎn):
          中間三個(gè)文本節(jié)點(diǎn)即是需要被替換的節(jié)點(diǎn),使用replaceChild就可以直接將文本節(jié)點(diǎn)替換為font標(biāo)簽。
          對(duì)于整個(gè)HTML字符串,同一個(gè)關(guān)鍵詞可能同時(shí)有多處匹配結(jié)果,因此要對(duì)所有匹配結(jié)果進(jìn)行上述處理。使用前幾步獲取的textNodes、textList、matchList,代碼實(shí)現(xiàn)如下:
          function replaceMatchResult (textNodes, textList, matchList) {  // 對(duì)于每一個(gè)匹配結(jié)果,可能分散在多個(gè)標(biāo)簽中,找出這些標(biāo)簽,截取匹配片段并用font標(biāo)簽替換出  for (let i = matchList.length - 1; i >= 0; i--) {    const match = matchList[i]    const matchStart = match.index, matchEnd = matchStart + match[0].length // 匹配結(jié)果在拼接字符串中的起止索引    // 遍歷文本信息列表,查找匹配的文本節(jié)點(diǎn)    for (let textIdx = 0; textIdx < textList.length; textIdx++) {      const { text, startIdx, endIdx } = textList[textIdx] // 文本內(nèi)容、文本在拼接串中開始、結(jié)束索引      if (endIdx < matchStart) continue // 匹配的文本節(jié)點(diǎn)還在后面      if (startIdx >= matchEnd) break // 匹配文本節(jié)點(diǎn)已經(jīng)處理完了      let textNode = textNodes[textIdx] // 這個(gè)節(jié)點(diǎn)中的部分或全部?jī)?nèi)容匹配到了關(guān)鍵詞,將匹配部分截取出來進(jìn)行替換      const nodeMatchStartIdx = Math.max(0, matchStart - startIdx) // 匹配內(nèi)容在文本節(jié)點(diǎn)內(nèi)容中的開始索引      const nodeMatchLength = Math.min(endIdx, matchEnd) - startIdx - nodeMatchStartIdx // 文本節(jié)點(diǎn)內(nèi)容匹配關(guān)鍵詞的長(zhǎng)度      if (nodeMatchStartIdx > 0) textNode = textNode.splitText(nodeMatchStartIdx) // textNode取后半部分      if (nodeMatchLength < textNode.wholeText.length) textNode.splitText(nodeMatchLength)      const font = document.createElement('font')      font.innerText = text.substr(nodeMatchStartIdx, nodeMatchLength)      textNode.parentNode.replaceChild(font, textNode)    }  }}

          代碼里對(duì)匹配結(jié)果遍歷時(shí),采用的是倒序遍歷,原因是遍歷過程對(duì)textNodes存在副作用:在遍歷中會(huì)對(duì)textNodes中的文本節(jié)點(diǎn)進(jìn)行切割。假設(shè)同一個(gè)文本節(jié)點(diǎn)中有多處匹配,會(huì)進(jìn)行多次分割,而textNodes里引用的是原文本節(jié)點(diǎn)即前半部分,因此從后往前遍歷會(huì)確保未處理的匹配文本節(jié)點(diǎn)的完整。

          同時(shí)代碼中省去了font節(jié)點(diǎn)的樣式設(shè)置,這個(gè)可以根據(jù)自己的邏輯來設(shè)置。

          三、完整代碼調(diào)用

          上述步驟描述了HTML字符串跨標(biāo)簽匹配關(guān)鍵詞的所有流程實(shí)現(xiàn),下面是完整的代碼調(diào)用示例:

          function replaceKeywords (htmlString, keyword) {  if (!keyword) return htmlString  const div = document.createElement('div')  div.innerHTML = htmlString  const textNodes = getTextNodeList(div)  const textList = getTextInfoList(textNodes)  const content = textList.map(({ text }) => text).join('')  const matchList = getMatchList(content, keyword)  replaceMatchResult(textNodes, textList, matchList)  return div.innerHTML}
          輸入一個(gè)HTML字符串和關(guān)鍵詞,將HTML串中的關(guān)鍵詞用font標(biāo)簽包裹后返回。

          四、總結(jié)

          上述實(shí)現(xiàn)方案中有一些簡(jiǎn)單的細(xì)節(jié)省去了,比如設(shè)置font標(biāo)簽的樣式、隱藏的dom匹配時(shí)忽略等。
          font標(biāo)簽樣式設(shè)置看使用場(chǎng)景吧,如果是長(zhǎng)HTML字符串匹配建議是不要直接設(shè)置style屬性,而是操作樣式表來達(dá)到目的??梢越ofont標(biāo)簽設(shè)置特殊的屬性,然后使用屬性選擇器來設(shè)置樣式。比如可以給font設(shè)置highlight="${i}"屬性,來針對(duì)匹配的關(guān)鍵詞應(yīng)用不同的樣式。操作樣式表可以給style標(biāo)簽設(shè)置innerText或者調(diào)用CSSStyleSheet.insertRule()CSSStyleSheet.deleteRule()
          demo:?https://wintc.top/laboratory/#/search-highlight
          github查看源碼:https://github.com/Lushenggang/vue-search-highlight


          瀏覽 50
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  久久久久久久久久国产 | 菠萝 成人网站视频 | 这里只有精品视频国产 | 人高清无码在线播放 | 天天日日日 |