内容中的关键词高亮显示,类似浏览器ctrl+f搜索结果。实现方案是在文本字符串中搜索出关键字,然后使用特殊标签(如字体标签)将关键字包裹起来替换匹配的内容,最后得到一个HTML字符串,渲染字符串并在字体上使用CSStag样式可以达到高亮效果。当时的实现过于简单,不支持接收HTML字符串作为关键字匹配的内容。这两天有同学问到这个问题,我又想了想,发现并没有那么麻烦,于是写了几行代码解决了。一、匹配关键词:HTML字符串与文本字符串的比较1、纯文本字符串的处理对于纯文本字符串,如:“江边谁初见月?明月初照人何如年?”,如果我们要匹配关键字“江月”,则匹配结果可以处理为:谁第一次在江边看月亮?江月年初的她什么时候闪过光?这样,“江月”二字被font标签包裹起来,并在font标签上应用特殊的背景样式,达到关键词高亮的效果。2.HTML字符串的处理对于上面的例子,如果内容字符串是一个HTML文本:谁先看到了月?江月初几照?对于同一个关键字“江月”,如何处理?因为关键词中的字在不同的标签中,只能用字体标签代替:谁先在河边看见了月?江月年初是什么时候?这是一个比较简单的情况。在实际情况中,关键词可能跨越多个层次和标签。2.cross-tagmatchingkeywordscross-tagparsingkeywords,其实就是对于匹配到的关键词,在每个标签中提取对应的子段,然后用字体等标签包裹起来,然后对字体使用高亮样式标签就是这样。对于整个HTML内容,呈现的文本由各种标签中的文本节点组成。因为关键词匹配的内容会跨标签,所以需要有序的提取每个文本节点,拼接节点内容进行匹配。拼接时记录节点文本在拼接字符串中的起止位置,这样当关键字匹配到拼接字符串中的某个位置时,截取文本片段并用字体标签包裹。1.深度优先遍历DOM树提取文本节点深度优先遍历可以采用循环或递归的方式进行。这里使用循环实现来提取一个元素下的所有文本节点(使用nodeType判断文本节点):length){constnode=nodeList.shift()if(node.nodeType===node.TEXT_NODE){textNodes.push(node)}else{nodeList.unshift(...node.childNodes)}}returntextNodes}2.取出所有的文字内容和拼接。得到文本节点列表后,可以取出所有文本内容,记录拼接结果中每个文本段的开始,结束索引:functiongetTextInfoList(textNodes){letlength=0consttextList=textNodes.map(node=>{letstartIdx=length,endIdx=length+node.wholeText.lengthlength=endIdxreturn{text:node.wholeText,startIdx,endIdx}})returntextList},拼接文本:constcontent=textList.map(({text})=>text).join('')3.匹配关键词得到拼接文本,可以使用拼接文本得到所有的拼接结果。这里偷懒,直接用正则表达式匹配。您必须转义正则表达式中使用的一些特殊符号:functiongetMatchList(content,keyword){constcharacters=[...'[]()?.+*^${}:'].reduce((r,c)=>(r[c]=true,r),{})keyword=keyword.split('').map(s=>characters[s]?`\\${s}`:s).join('[\\s\\n]*')constreg=newRegExp(keyword,'gmi')return[...content.matchAll(reg)]//matchAll的结果是迭代器,展开带扩展名获取数组}关键字字符转义后,字符间插入常规空格和换行符(\s\n),匹配时忽略一些不可见字符。上面的代码使用了matchAll函数。匹配结果展开后,结果是一个数组,数组中的每一项包含匹配的文本,匹配索引等。matchAll的一个简单例子:4.关键字被字体标签替换。根据关键词匹配结果索引和每个文本节点的起止索引,可以计算出每个关键词匹配了哪些文本节点。对于start和end中的文本节点,可能只是部分匹配,而中间文本节点的所有内容都匹配。例如,对于HTML文本:WhofirstsawMonth?年初的江月什么时候亮过人?DOM树对应的文本节点有3个:如果关键字是“谁第一次看到月亮?”,那么此时匹配第一个文本节点的后半部分,第二个文本节点完全匹配,第三个文本节点匹配第一个字符。三个节点的匹配部分需要用字体标签替换:Riverside谁初见月?年初的江月什么时候大放异彩?默认情况下,连续的文本会在同一个文本节点中,对于匹配部分内容的文本节点,需要拆分成两部分,可以使用Text.splitText()")API进行拆分文本节点,API接收到一个索引值,从索引位置开始截取文本节点的后半部分,返回一个新的包含后半部分内容的文本节点。上面的例子中,匹配到3个节点,将得到5个文本拆分后得到Node:中间的三个文本节点是需要替换的节点,使用replaceChild直接将文本节点替换为字体标签,对于整个HTML字符串,同一个关键字可能在同时,因此所有匹配结果都按上述方式处理。利用前面步骤得到的textNodes、textList、matchList,代码实现如下:functionreplaceMatchResult(textNodes,textList,matchList){//对于每个匹配结果,可能分散在多个标签中,找出这些标签,并截取匹配段并替换为字体标签for(leti=matchList.length-1;i>=0;i--){constmatch=matchList[i]constmatchStart=match.index,matchEnd=matchStart+match[0].length//拼接字符串中匹配结果的起止索引//遍历文本信息列表,寻找匹配的文本节点for(lettextIdx=0;textIdx=matchEnd)break//匹配的文本节点已经处理完毕lettextNode=textNodes[textIdx]//Partorall本节点内容与关键字匹配,匹配部分截取替换constnodeMatchStartIdx=Math.max(0,matchStart-startIdx)//匹配内容在text节点内容中的起始索引constnodeMatchLength=Math.min(endIdx,matchEnd)-startIdx-nodeMatchStartIdx//文本节点内容匹配关键字的长度if(nodeMatchStartIdx>0)textNode=textNode.splitText(nodeMatchStartIdx)//获取textNode的后半部分if(nodeMatchLengthtext).join('')constmatchList=getMatchList(content,keyword)replaceMatchResult(textNodes,textList,matchList)returndiv.innerHTML}输入一个HTML字符串和关键字,将关键字用字体标签包裹在HTML字符串中并返回。4.总结上面的实现方案省略了一些简单的细节,比如设置font标签的样式,忽略隐藏dom匹配等,font标签样式的设置取决于使用场景。如果是较长的HTML字符串匹配,建议不要直接设置style属性,而是操作样式表来达到目的。您可以在字体标签上设置特殊属性,然后使用属性选择器设置样式。例如,您可以为字体设置highlight="${i}"属性,以对匹配的关键字应用不同的样式。要操作样式表,可以为样式标签设置innerText或者调用CSSStyleSheet.insertRule()")和CSSStyleSheet.deleteRule()")。demo:https://wintc.top/laboratory/#/search-highlightgithub查看源码:https://github.com/Lushenggang/vue-search-highlight码代码五分钟,博客两小时....