I.粗糙版
首先实现高亮的第一反应是使用html的方式渲染原文本,如 innerHTML、vue的v-html react的dangerouslySetInnerHTML
。搜索关键字时如果有匹配项就替换原文本为span标签,并加上高亮样式。于是一个粗糙版的搜索高亮就实现了。
<script setup>
import { ref } from 'vue'
const text = ref(
'噫吁嚱,危乎高哉!蜀道之难,难于上青天!蚕丛及鱼凫,开国何茫然!尔来四万八千岁,不与秦塞通人烟。西当太白有鸟道,可以横绝峨眉巅。地崩山摧壮士死,然后天梯石栈相钩连。上有六龙回日之高标,下有冲波逆折之回川。黄鹤之飞尚不得过,猿猱欲度愁攀援。青泥何盘盘,百步九折萦岩峦。扪参历井仰胁息,以手抚膺坐长叹。问君西游何时还?畏途巉岩不可攀。但见悲鸟号古木,雄飞雌从绕林间。又闻子规啼夜月,愁空山。蜀道之难,难于上青天,使人听此凋朱颜!连峰去天不盈尺,枯松倒挂倚绝壁。飞湍瀑流争喧豗,砯崖转石万壑雷。其险也如此,嗟尔远道之人胡为乎来哉!剑阁峥嵘而崔嵬,一夫当关,万夫莫开。所守或匪亲,化为狼与豺。朝避猛虎,夕避长蛇;磨牙吮血,杀人如麻。锦城虽云乐,不如早还家。蜀道之难,难于上青天,侧身西望长咨嗟!',
)
const searchText = ref('')
const handleSearch = () => {
text.value = renderHighLights(text.value, searchText.value)
}
const renderHighLights = (text, keyword) => {
const regex = new RegExp(keyword, 'gi')
// 使用正则表达式替换匹配的部分,并添加高亮效果
return text.replace(
regex,
(match) => `<span style="background: yellow;">${match}</span>`,
)
}
</script>
<template>
<div>
<input type="text" v-model="searchText" placeholder="请输入...">
<button @click="handleSearch">搜索并高亮</button>
<div ref="content">
<p v-html="text"></p>
</div>
</div>
</template>
II.优化一下
1.由于我们是直接替换原文本,那么再次搜索之后就可能会发现匹配不到文本了,这是由于原文本被替换为span标签,正则表达式不能正确匹配。这就需要我们添加一个变量存储被替换后的文本,原文本保持不变,每次搜索从原文本匹配,搜索完成后,修改这个变量的值就可以了。
2.因为使用正则进行匹配,当搜索的字符是一些特殊字符时,就会出现问题,比如输入 .*
就会匹配所有字符串。
此时我们需要对搜索的关键字进行转义,这样就能避免这个问题了。
const escapeRegExp = (string) => {
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
}
3.在开发中遇到过一个情况:需要搜索个高亮的文本带有换行符,没法实现跨行搜索。
在正则表达式中 \s
能匹配所有的空白符,其中也包括换行符,这里将搜索关键词中的每个字符之间插入 \s* 以匹配任何数量的空白字符以实现跨行搜索,当然中间间隔其他空白符我们也能匹配到,这个是否需要看需求。
const regex = new RegExp(keyword.split('').join('\\s*'), 'gi')
至此,一个优化版的搜索高亮就完成了。
<script setup>
import { ref, onMounted, watch } from 'vue'
const text = ref(
'噫吁嚱,危乎高哉!蜀道 之难,难于上青天!蚕丛及鱼凫,开国何茫然!尔来四万八千岁,不与秦塞通人烟。西当太白有鸟道,可以横绝峨眉巅。地崩山摧壮士死,然后天梯石栈相钩连。上有六龙回日之高标,下有冲波逆折之回川。黄鹤之飞尚不得过,猿猱欲度愁攀援。青泥何盘盘,百步九折萦岩峦。扪参历井仰胁息,以手抚膺坐长叹。问君西游何时还?畏途巉岩不可攀。但见悲鸟号古木,雄飞雌从绕林间。又闻子规啼夜月,愁空山。蜀道之难,难于上青天,使人听此凋朱颜!连峰去天不盈尺,枯松倒挂倚绝壁。飞湍瀑流争喧豗,砯崖转石万壑雷。其险也如此,嗟尔远道之人胡为乎来哉!剑阁峥嵘而崔嵬,一夫当关,万夫莫开。所守或匪亲,化为狼与豺。朝避猛虎,夕避长蛇;磨牙吮血,杀人如麻。锦城虽云乐,不如早还家。蜀道之难,难于上青天,侧身西望长咨嗟!',
)
const searchText = ref('')
const displayText = ref('')
onMounted(() => {
handleSearch()
})
const handleSearch = () => {
displayText.value = renderHighLights(text.value, searchText.value)
}
/**
* @description: 此方法用于转义用户输入的特殊字符 如.*等等 避免影响正则表达式的生成导致匹配错误
* @param {string} string
* @return {string} 转义后的字符串
*/
const escapeRegExp = (string) => {
// 转义正则表达式中的特殊字符
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') // $& 表示整个匹配的子字符串
}
/**
* @description: 关键词匹配高亮 此方法使用v-html片段渲染 需注意xss攻击
* @param {string} text 原始文本
* @param {string} keyword 关键词
* @return {string} 替换后的html片段
*/
const renderHighLights = (text, keyword) => {
if (keyword === '' || !text) {
// 如果关键词为空或文本为空,直接返回原文本
return text
}
// 转义关键词中的特殊字符 防止输入恶意字符
const escapedKeyword = escapeRegExp(keyword)
// 构建跨行匹配的正则表达式
// 将关键词中的每个字符之间插入 \s* 以匹配任何数量的空白字符(包括换行符)
const regex = new RegExp(escapedKeyword.split('').join('\\s*'), 'gi')
// 使用正则表达式替换匹配的部分,并添加高亮效果
return text.replace(
regex,
(match) => `<span style="background: yellow;">${match}</span>`,
)
}
watch(searchText, () => {
handleSearch()
})
</script>
<template>
<div>
<input type="text" v-model="searchText" placeholder="请输入...">
<button @click="handleSearch">搜索并高亮</button>
<div class="text" ref="content">
<pre v-html="displayText"></pre>
</div>
</div>
</template>
<style scoped>
.text {
width: 400px;
height: 400px;
padding: 0 10px;
border: 1px solid #ddd;
overflow: auto;
pre {
white-space: pre-wrap;
}
}
</style>