ICode9

精准搜索请尝试: 精确搜索
首页 > 其他分享> 文章详细

前端高亮功能实现复盘

2021-06-02 16:32:22  阅读:211  来源: 互联网

标签:const keyword 前端 关键词 中华人民共和国 高亮 文本 复盘


“高亮”功能,个人觉得没必要再解释什么了。作为一名程序猿,天天都会接触高亮:写代码时的语法高亮;使用搜索引擎时的搜索结果高亮。作为一名前端,如果你做过与搜索相关的功能,那么你很有可能就实现过高亮,本文也主要从前端的角度复盘一下“高亮”功能实现的关键知识点。

 

 

 

高亮实现思路

 

 

 

对用户的输入进行分词得到关键词,根据关键词搜索得到搜索结果。再次使用关键词从搜索结果中找到匹配,对匹配加上高亮样式,即完成高亮。细分这个过程,会有以下细节点:

  • 对用户输入分词得到关键词

  • 根据关键词得到搜索结果

  • 关键词匹配

  • 对匹配使用高亮样式

前两步一般在后台完成,后两步才是我们前端的工作,下面通过具体例子来实际演练一下。

普通文本高亮

比如我们有这样的文本:“我是中国人,我爱中华人民共和国,中华人民共和国万岁!”,这时我们的关键词是“我”,即要高亮文本中所有的我。代码实现完整如下:

 

<!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操作来实现一个更精确的富文本高亮

  • 前端也可以自己实现分词,只不过会产生大量无意义词组而已

 

原文链接:https://segmentfault.com/a/1190000009956571?utm_source=sf-similar-article

标签:const,keyword,前端,关键词,中华人民共和国,高亮,文本,复盘
来源: https://www.cnblogs.com/momo798/p/14841787.html

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有