vue实现搜索关键词高亮(保姆级教程)

6,107 阅读6分钟

补在前面

看到有些小伙伴在评论区说可以用v-html和正则的replace方法替换高亮词,这种做法确实简单容易,但难以应对一些复杂的业务场景,如下

这篇文主要是针对类似这样的业务场景:

假如我是一家医疗企业,然后我现在有这样的对应关系,需要引导用户查看这些关键字对应的内容,内容的获取方式是通过点击从接口获取(完整代码已放在文章最末)

map:{
     '感冒':0,
      '三九':1,
      '小柴胡':2,
      '发烧':3,
      '咳嗽':4
 }

111.gif

--------------------------手动分割线-----------------------------------------

正文

废话少说先上效果(左侧代码,右侧效果)

  • 第一组

image-20220627232935936.png

  • 第二组

image-20220627233049019.png

  • 第三组

image-20220627233146630.png

看到什么规律了吗?

没错!!这份代码就是依靠正则表达式来做出高亮效果,你负责传入str与reg,而函数则负责实现代码的高亮

解析

结构部分如下

<template>
  <div>
    <span
      :class="index%2!==0?'hightLight':''"
      v-for="(item,index) in textData"
      :key="index"
    >{{item}}</span>
  </div>
</template>

基本原理就是将传入的str按照正则的规则进行切割,偶数部分负责高亮,奇数部分负责正常显示

如:

str与reg分别为

let str = "高亮几个词语出来好不好";
let reg = /(高亮|词语)/g;

将该部分切割为如下数组

"高亮几个词语出来好不好" ---》 ['', '高亮', '几个', '词语', '出来好不好'] ----》image-20220627234321878.png

知道了基本原理,我们再来看一下代码

      let str = "高亮几个词语出来好不好";
      let reg = /(高亮|词语)/g;
      let result = {};
      let currentIndex = 0, returnData = [];
      while ((result = reg.exec(str))) {
        returnData.push(str.slice(currentIndex, result.index));
        returnData.push(result[1]);
        currentIndex = result.index + result[0].length;
      }
      if (str.length > currentIndex) {
        returnData.push(str.slice(currentIndex, str.length));
      }
      console.log(returnData);
      this.textData = returnData;

代码关键在于理解reg.exec,了解这个api的可以直接跳到提取关键字并分组

正则api——exec

理解了这个例子,也就基本理解了其他的例子

我们这样规定,将被两个#和一个空格包围的内容进行高亮即## 内容 ##时,内容要被提取出来并高亮,首先要对其进行分组

"1212## 4455 #### 0011 ##44488## 000000 ##"就会被分为

['1212', '4455', '', '0011', '44488', '000000'] 高亮文本与普通文本间隔排序

image-20220628210854640.png

先来看看下面这部分代码

let str = "1212## 4455 #### 0011 ##44488## 000000 ##";
let reg = /##\s+(.*?)\s+##/g;
console.log(reg.exec(str));
console.log(reg.exec(str));
console.log(reg.exec(str));
console.log(reg.exec(str));

执行结果如下

image-20220628211127002.png

reg.exec执行

  • 第一次的时候会从字段中找到第一个匹配的字段(## 4455 ##),

  • 第二次:会在第一次匹配的位置后找到第二个匹配的字段(## 0011 ##),

  • 第三次:会在第二次匹配的位置后找到第三个匹配字段(## 000000 ##)

  • 第四次时,剩余的字符已经没有了匹配所有返回null

    也就是说,在null之后的执行结果,都是循环之前的执行结果,所以咱们在执行结果为null时即可停止

接下来看看执行具体的执行结果

0: "## 4455 ##"
1: "4455"
groups: undefined
index: 4
input: "1212## 4455 #### 0011 ##44488## 000000 ##"
length: 2

返回的是一个数组,并带有几个字段属性

结合一下其正则'0'是拿匹配到正则规则的值,'1'则是匹配到在正则里第一个被括号包起来的值

,这个lenth你可以粗糙的理解为需要提取的(即有几个括号)数量+1(这个指的是能匹配到的整体)

至于为什么是一样的还要用括号括起来,这个后面会说

知道原理之后我们来看看如何提取关键字并分组

提取关键字并分组

提取关键字并分组 首先我们清楚的看到,通过正则,他会给出三个有用的信息

第一个是匹配到的正则规则中的值

第二个是可以拿到我们想要提取出来 即用括号括起来的值

第三个是该值所在的index

我们一步一步来

image-20220628210854640.png

分成这种结构,

也就是每次exec执行不为null时都做一次区分, 将匹配到的值前方位置提取作为普通字符串,再把当前的关键字提取作为高亮字符串,如下:

比如第一次匹配时,拿到的值是这样的

  • 第二次匹配
/*
0: "## 4455 ##"
1: "4455"
groups: undefined
index: 4
input: "1212## 4455 #### 0011 ##44488## 000000 ##"
length: 2
*/

这里index为4,将下标为0-4的字符提取为普通数组,而匹配到的字符串4455作为高亮字符

此时数组为['1212','4455']

  • 第二次匹配
0: "## 0011 ##"
1: "0011"
groups: undefined
index: 14
input: "1212## 4455 #### 0011 ##44488## 000000 ##"
length: 2

普通字符串应下标为为14-14即空字符串,高亮字符为匹配到的0011

此时数组为['','0011']

这里需要明确两个14分别是怎么来的

第一个14是上一次匹配返回值的 index+上次匹配到字符串字符串## 4455 ##的长度

image-20220628214057244.png

第二个14是本次配到的index,

可能这时还有点懵,看这最后一次匹配,你应该就理解了

  • 第三次匹配
0: "## 000000 ##"
1: "000000"
groups: undefined
index: 29
input: "1212## 4455 #### 0011 ##44488## 000000 ##"
length: 2

普通字符串应是下标为24-29即44488,高亮字符为000000

此时数组为['44488','000000']

同样需要明确24,29分别是怎么来的

第一个是上一次匹配返回值的 index+上次匹配到字符串### 0011 ##的长度

image-20220628215516703.png

第二个则是本次匹配到的index 29

至此 我们用一个while循环将其包裹,利用上面的规则把对应的值push到数组中,并用一个值缓存上一次的index就可以得到我们的目标数组

	  let str = "1212## 4455 #### 0011 ##44488## 000000 ##";
      let reg = /##\s+(.*?)\s+##/g;
      let result = {};
      let currentIndex = 0,
        returnData = [];
      while ((result = reg.exec(str))) {
        console.log(result);
        
        returnData.push(str.slice(currentIndex, result.index));
        returnData.push(result[1]);
        currentIndex = result.index + result[0].length;
      }
	  // 最后一次匹配的高亮词后可能还有没push到数组中的值
      if (str.length > currentIndex) {
        returnData.push(str.slice(currentIndex, str.length));
      }
      console.log(returnData);  // ['1212', '4455', '', '0011', '44488', '000000']
      this.textData = returnData;

完整vue代码

<template>
  <div>
    <span
      :class="index%2!==0?'hightLight':''"
      v-for="(item,index) in textData"
      :key="index"
    >{{item}}</span>
  </div>
</template>

<script>
export default {
  data() {
    return {};
  },
  created() {
    this.init();
  },
  methods: {
    init() {
      // let str = "实现搜索词高亮 - 百度文库";
      // let reg = /([高亮词文])/g;
      // let str = "高亮几个词语出来好不好";
      // let reg = /(高亮|词语)/g;
      let str = "1212## 4455 #### 0011 ##44488## 000000 ##";
      let reg = /##\s+(.*?)\s+##/g;
      let result = {};
      let currentIndex = 0,
        returnData = [];
      while ((result = reg.exec(str))) {
        console.log(result);
        
        returnData.push(str.slice(currentIndex, result.index));
        returnData.push(result[1]);
        currentIndex = result.index + result[0].length;
      }
      if (str.length > currentIndex) {
        returnData.push(str.slice(currentIndex, str.length));
      }
      console.log(returnData);
      this.textData = returnData;
    }
  }
};
</script>

<style>
.hightLight {
  color: red;
}
</style>

开头提到效果的对应代码

<template>
  <div class='text_box'>
    <div
      :class="{hightLight:index%2!==0}"
      v-for="(item,index) in textData"
      :key="index"
    >
      <span >{{item}}</span> 
      <img v-if="index%2!==0" @click="go(computedSrc(item),$event)" src="https://img95.699pic.com/element/40029/9841.png_860.png"></img>
    </div>
    <div class="card" v-show="card.show" :style="`left:${card.x}px;top:${card.y}px`" >
      {{card.text}}
    </div>
  </div>
</template>

<script>
export default {
  data() {
    return {
      map:{
        '感冒':0,
        '三九':1,
        '小柴胡':2,
        '发烧':3,
        '咳嗽':4
      },
      card:{
        show:false,
        x:0,
        y:0,
        text:'',
        id:null
      }
    };
  },
  computed:{
    computedSrc(){
      return function(data){
        return this.map[data]
      }
      
    }
  },
  created() {
    this.init();
  },
  methods: {
    go(id,e){
      if(id === this.card.id){
        this.card.show = false
        this.card.id = null
        return
      }
      this.card.show = true
      this.card.x = e.clientX
      this.card.y = e.clientY
      this.card.id = id
      this.card.text = `根据id值${id}请求回来的数据展示`
      console.log(e.clientX)
    },
    init() {
      let  a = []
      for(let key in this.map){
        a.push(key)
      }
      // 根据map动态填写规则
      let str = "今天突然感冒了,请问吃三九好还是小柴胡好";
      let reg = new RegExp(`(${a.join('|')})`,'g')

      let result = {};
      let currentIndex = 0, returnData = [];

      while ((result = reg.exec(str))) {
        returnData.push(str.slice(currentIndex, result.index));
        returnData.push(result[1]);
        currentIndex = result.index + result[0].length;
      }
      if (str.length > currentIndex) {
        returnData.push(str.slice(currentIndex, str.length));
      }
      console.log(returnData);
      this.textData = returnData;
    }
  }
};
</script>

<style>
.text_box{
  display: flex;
  align-items: start;
}
.text_box a{
  color:red;
}
.text_box img{
  width: 20px;
  vertical-align: middle;
  margin-top: -10px;
  /* margin-bottom: 20px; */
}
.text_box .card{
  width: 100px;
  height: 100px;
  background-color: #bfc;
  border-radius: 20px;
  position: absolute;
  padding: 10px;
  font-size: 18px;
  color: #ff0000;
  animation:mymove 0.3s 1;
}
@keyframes mymove
	{
	from { height: 0;font-size: 0;}
	to {height: 100px;font-size: 18px;}
}
.hightLight {
  color: red;
}
</style>