开发中常常遇到类似于搜索引擎一样的关键词着色,突出显示关键词比如标红, 以下只是解决问题的一两种方法, 当然方法有很多, 需要再进一步的思考
设定:
- 搜索文本时高亮
- 匹配替换(只允许在编辑情况下)
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(
<span
style={{ 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;
以上可以根据具体情况做些改造, 比如关键词的分隔符, 加些类型判断