开发中常常遇到类似于搜索引擎一样的关键词着色,突出显示关键词比如标红, 以下只是解决问题的一两种方法, 当然方法有很多, 需要再进一步的思考
设定:
- 搜索文本时高亮
- 匹配替换(只允许在编辑情况下)
const title = '关键词a, 关键词b, 关键词c, 其他剩余文本';// 关键词可以是以逗号分隔的字符串, 也可以是数组const keyWords = '关键词a, 关键词b';
解决方案
方案一
/*** @desc: 主要使用数组循环以及正则匹配* @params: 返回替换后的字符串*/function formateKeyWords() {// 此处判断是否为数组, 即少去一步const arr = keyWords.replace(/\s/g, '').split(',');let formtedTitle = title;arr.forEach(keyword => {if (formtedTitle.includes(keyword)) {formtedTitle = formtedTitle.replace(new RegExp(keyword, 'g'), '<span>'+keyword+'</span>');}});return formtedTitle;}
方案二
/*** @desc: 单纯使用正则匹配* @params: 返回替换后的字符串*/function formateKeyWords() {const trimKeywords = keyWords.replace(/\s+?/g, '');const r = new RegExp(trimKeywords.replace(/,/g, '|'));const r1 = new RegExp(r, 'g');const formatedTitle = title.replace(r1, match => '<span>'+ match +'</span>');return formatedTitle;}
方案三
方案三参考自网络, 有缺陷, 比如关键词是数组或者命中多个关键词时
参考
/*** @name: formatKeyword* @description: 搜索关键字标红* @param: label: 原文本, value 关键词*/import React from 'react';export default function formatKeyword(label, value) {if (!value) {return label;}return (<span>{label.split(value).reduce((prev, current, i) => {if (!i) {return [current];}return prev.concat(<spanstyle={{ color: '#ff186d' }}key={value + current + Math.random() * 100}>{value}</span>,current,);}, [])}</span>);}
React
在React 中使用
import React from 'react'// 无论使用哪种方法都可以, 这里使用第二种function LabelWithColor({ text, keyWords, color = 'red' }) {const trimKeywords = keyWords.replace(/\s+?/g, '');const r = new RegExp(trimKeywords.replace(/,/g, '|'));const r1 = new RegExp(r, 'g');const formatedTitle = text.replace(r1, match => `<span style="color: ${color}">${match}</span>`);return (<span dangerouslySetInnerHTML={{ __html: formatedTitle }} />);}
最终方案
/*** @name: HighLightWord* @desc: 关键词高亮* @author NHF*/import React, { Fragment } from 'react';interface DeepArray<T> extends Array<T | DeepArray<T>> { }type KeywordTypes = DeepArray<string | string[]> | string;type KeywordColor = string | { [key: string]: string };/** 统计记录匹配到字符开始 & 结束位置 */interface MatchRange {/** 开始位置: reg.index */start: number;/** 结束位置: reg.index + value.length */end: number;/** 匹配到的字符 */value: string;}export interface HighlightProps {className?: string;style?: React.CSSProperties;/** 展示字符串 */text: string;/** 配置的颜色 */color?: KeywordColor;/** 需要高亮的关键词 */keywords?: KeywordTypes;}const { toString } = Object.prototype;const DEFAULT_COLOR = '#f47d31';const isType = (val: any, type: string) => toString.call(val) === `[object ${type}]`;const isObject = (val: any) => isType(val, 'Object');const isString = (val: any) => isType(val, 'String');/** 去除空格 */const trimReg: RegExp = /[-[\]{}()*+?.,\\^$|#\s]/g;function trim(val: any) {return isString(val) ? val.replace(trimReg, '') : val;}function getTrimKeywords(val: KeywordTypes) {if (typeof val === 'string' && val.includes(',')) {return val.split(',').map(item => trim(item));}if (isString(val)) {return [trim(val)];}if (Array.isArray(val)) {// give me to your hand// don't need trim, should be fix// return val.map((item: any) => (isArray(item) ? getTrimKeywords(item) : trim(item)));return val.reduce((prev, next) => prev.concat(Array.isArray(next) ? getTrimKeywords(next) : trim(next)), [])}return [];}/** 获取到对应的正则 */function getKeywordsReg(keywords: KeywordTypes) {const $newkeywords = getTrimKeywords(keywords);const $reg = [... new Set($newkeywords)].join('|');const keywordsReg = new RegExp($reg, 'ig');return keywordsReg;}/** 获取到对应的颜色 */function getColorByMatch(match: string, color?: { [key: string]: string } | string) {if (isObject(color)) {return color[match] || DEFAULT_COLOR;}return color;}/** 统计匹配字符位置及其他信息 */const getMatchMap = (text: string, reg: RegExp) => {const map = new Map();const range = Array.from(text.matchAll(reg));range.forEach(item => {map.set(item.index, {value: item[0],start: item.index,end: item.index + item[0].length});});return map;}/** 分隔字符串 拼接 */const getLastContent = (splited: string[], matchMap: Map<number, MatchRange>, color) => {/** 统计使用 */let count = 0;const list = splited.reduce((prev, next) => {count += next.length;prev.push(<Fragment key={count}>{next}</Fragment>);if (matchMap.has(count) && matchMap.get(count).start === count) {const matched = matchMap.get(count);count = matched.end;prev.push(<span key={`${count}-${matched.start}-${matched.end}`} style={{ color: getColorByMatch(matched.value, color) }}>{matched.value}</span>);}return prev;}, []);return list;}const HighLightWord: React.FunctionComponent<HighlightProps> = React.memo<HighlightProps>((props) => {const { text, color, style, className, keywords } = props;if (!text) return null;if ((Array.isArray(keywords) && keywords.length === 0 || !keywords) && !!text) {return <div className={className} style={style}>{text}</div>}const keywordReg = getKeywordsReg(keywords);const matchMap = getMatchMap(text, keywordReg);const splited = text.split(keywordReg);const content = getLastContent(splited, matchMap, color);return (<div className={className} style={style}>{content}</div>);});HighLightWord.defaultProps = {text: '',keywords: '',color: DEFAULT_COLOR,};export default HighLightWord;
以上可以根据具体情况做些改造, 比如关键词的分隔符, 加些类型判断
