React组件开发实战-文字高亮组件
需求背景
产品: 我输入关键字搜索时候, 返回的结果能不能把关键字给我高亮起来? 开发: 可以, 安排. 产品: 我输入字母的时候, 希望不区分大小写. 开发: 可以, 安排. 产品: 如果有多个关键字, 都需要给我高亮起来. 还有还有, 如果我输入的关键字在结果中不是连续的, 也给我高亮出来. 开发: 可以. 产品: 我想起来了, 我们还有另外一个地方要用到这个功能, 但是高亮的颜色不大一样, 懂我的意思吧. 开发: ..., 我整理一份需求给你吧.
(半个小时后)
开发: 这个高亮功能包含这些功能, 你看满不满足要求:
- 没有匹配到关键字显示正常文本
- 支持高亮多组关键字
- 可忽略大小写
- 可自定义高亮样式
- 支持简单的多层级文本高亮
- 可以匹配特殊字符
产品: (点赞)
设计API
<HighlightText keywords={['1', 'a']}>匹配不到文本的情况正常显示</HighlightText>
<HighlightText keywords="关键字">匹配单个关键字</HighlightText>
<HighlightText keywords={['foo', 'bar']}>匹配多关键字, 例如 xxxfooxxxbar</HighlightText>
<HighlightText keywords={['a', 'c']}>忽略大小写, 例如 ABCDabcd</HighlightText>
<HighlightText keywords={['^', ']']}>正则测试: ^\$.\*+-?=!:|\/()[]{}</HighlightText>
<HighlightText keywords="样式" highlightStyle={{ color: '#f55', backgroundColor: 'rgba(0,0,0,.1)' }}>
自定义高亮样式
</HighlightText>
<HighlightText keywords="ron">
多层级
<div>
div
<strong>strong</strong>
</div>
</HighlightText>
这个是我期望的样子. 来吧, 撸起袖子开始干
实现
高亮文字比较好解决, 方法也比较多. 我这里用正则的方式切割字符串, 并把分割后的字符串与匹配到的字符串做个拼接, 把需要高亮的文字加个 mark
标签. 多个关键字的情况加个遍历就好.
我写个 util 还专门处理字符串, 因为是用正则的方式匹配字符串, 所以要对正则关键字做特殊处理.
// utils.tsx
const regAtom = '^\\$.\\*+-?=!:|\\/()[]{}';
// 关键词高亮
export const highlightText = (
text: string,
keywords: string | string[],
highlightStyle?: CSSProperties,
ignoreCase?: boolean
): string | [] | ReactNode => {
let keywordRegExp;
if (!text) {
return '';
}
// 把字符串类型的关键字转换成正则
if (keywords) {
if (keywords instanceof Array) {
if (keywords.length === 0) {
return text;
}
keywordRegExp = new RegExp(
(keywords as string[])
.filter(item => !!item)
.map(item => (regAtom.includes(item) ? '\\' + item : item))
.join('|'),
ignoreCase ? 'ig' : 'g'
);
} else if (typeof keywords === 'string') {
keywordRegExp = new RegExp(keywords, ignoreCase ? 'ig' : 'g');
}
}
if (text && keywordRegExp) {
const newData = text.split(keywordRegExp); // 通过关键字的位置开始截取,结果为一个数组
// eslint-disable-next-line
const matchWords = text.match(keywordRegExp); // 获取匹配的文本
const len = newData.length;
return (
<>
{newData.map((item, index) => (
// eslint-disable-next-line react/no-array-index-key
<React.Fragment key={index}>
{item}
{index !== len - 1 && <mark style={highlightStyle}>{matchWords?.[index]}</mark>}
</React.Fragment>
))}
</>
);
}
return text;
};
单个文本的话, 这段代码基本能解决问题了, 但是实际情况中可能会出现嵌套 html 的情况, 表现形式就不单单是字符串了, 需要特殊处理下.
// utils.tsx
export type PropsIncludeChildren = {
props: {
children: [] | string | ReactNode;
};
};
// 递归子组件
export const highlightChildComponent = (
item: PropsIncludeChildren,
keywords: string | [],
highlightStyle: CSSProperties,
ignoreCase: boolean
) => {
if (typeof item === 'string') {
return highlightText(item, keywords, highlightStyle, ignoreCase);
}
// children 如果是文本, item.props.children 会等于 'string'
if (item.props?.children && typeof item.props?.children === 'string') {
const newItem = { ...item };
newItem.props = {
...newItem.props,
children: highlightText(newItem.props.children as string, keywords, highlightStyle, ignoreCase)
};
return newItem;
}
// 如果还有其他元素, 会返回一个数组, 遍历做判断
if (item.props?.children && item.props?.children instanceof Array) {
const newItem = { ...item };
newItem.props = {
...newItem.props,
children: item.props?.children.map((child, index) => (
// eslint-disable-next-line react/no-array-index-key
<React.Fragment key={index}>
{highlightChildComponent(child as PropsIncludeChildren, keywords, highlightStyle, ignoreCase)}
</React.Fragment>
))
};
return newItem;
}
return item;
};
处理嵌套 html, React 组件还有可能存在多个同级的子组件, 这时候我们可以用 React.Children
的 API 来遍历子组件, 然后使用我们工具函数来遍历所有子组件.
export interface HighlightTextProp {
keywords: [] | string | null;
highlightStyle?: CSSProperties;
ignoreCase?: boolean;
children?: ReactElement;
}
const HighlightText: FC<HighlightTextProp> = ({
keywords,
highlightStyle = { color: '#ffa22d', backgroundColor: 'transparent', padding: 0 },
ignoreCase = true,
children
}) => (
<>
{children
? React.Children.map(children, item =>
highlightChildComponent(item as PropsIncludeChildren, keywords || '', highlightStyle, ignoreCase)
)
: ''}
</>
);
HighlightText.displayName = 'HighlightText';
export default HighlightText;
正常这样就基本实现需求了. 但是写 React 常常容易犯的一个性能错误是这样的:
<HighlightText keywords={['1', 'a']}>匹配不到文本的情况正常显示</HighlightText>
keywords 这个属性我们往往直接传了一个数组, 这样写相当于每次组件在更新的时候都告诉它, 这个是一个新的数组, 这个数组的引用地址是不一样的, React 默认比较的规则是浅比较. 所以如果不做任何处理, 这个组件的父组件每次刷新都会导致子组件刷新, 这是没有必要的.
// 测试代码, HighlightText 组件可以自己输出个日志看下
export default function Basic() {
const [, forceUpdate] = useState();
const clickFn = useCallback(() => {
forceUpdate({});
}, [])
return (
<>
<button onClick={clickFn}>强制刷新页面</button>
<HighlightText keywords={['1', 'a']}>匹配不到文本的情况正常显示</HighlightText>
</>
);
}
我们可以在结果这里加个 memo
, 来做性能优化.
export default memo(HighlightText, (prevProps, nextProps) => isEqual(prevProps, nextProps));
总结
高亮组件在实际项目中是很频繁的一个功能需求. 实现这个组件用到了几个相关知识:
React.Children
API 来遍及子组件- 递归处理嵌套 html
- 正则匹配高亮高亮文字
这些技巧熟悉起来, 在以后的组件开发中经常会用到.
期待后续的React组件开发分享吧.
转载自:https://juejin.cn/post/7096846838233301005