在做日志关键字高亮的需求时, 有发现到一个文本高亮的库 react-highlight-words
实际它的核心是 : highlight-words-core
用法非常简单 :
import Highlighter from "react-highlight-words";
<Highlighter
highlightClassName="hightlight样式的 Class"
searchWords={["emmm"]} // 需要高亮的关键字
autoEscape={true}
textToHighlight="需要高亮的日志内容 emmm !!! "
/>
由于比较好奇他对日志这种超长文本是如何解析高亮, 并且兼顾性能的, 带着问题去看一波源码 ~
关键字高亮这个功能的实现涉及到编译原理, 这里不具体展开
Highlight-words-core 相当于 词法分析中的 tokenizer
React-highlight-words 相当于把 结构化的 token 可视化的展示出来 (单词高亮)
对于前端同学来说, 最熟悉编译流程如下 :
读取 字符串流 => 词法分析, 生成 token => 语法分析, 生成 AST => babel 操作 AST => 生成 js 代码 => 浏览器执行
/**
* @param searchWords 需要高亮的关键词
* @param textToHighlight 需要被高亮的完整文本
*/
const findAll = (searchWords: string[], textToHighlight: string) => {
return searchWords.reduce((chunks, searchWord) => {
// 逐个取出 searchWord, 并创建一个正则对象
const regex = new RegExp(searchWord, caseSensitive ? 'g' : 'gi')
let match
while ((match = regex.exec(textToHighlight))) {
// 正则遍历, 获取到所有匹配的字符串, 保存为 token : 记录 {startIndex, endIndex}
const start = match.index
const end = regex.lastIndex
// We do not return zero-length matches
if (end > start) {
chunks.push({ highlight: false, start, end })
}
}
return chunks
}, [])
}
上面的 findAll 方法已经知道了所有需要高亮的字符串的位置, 这个方法是, 将所有非高亮的文字位置记录下来, 这样所有的文本内容, 都可以用一个 token 数组来表示了
export const fillInChunks = ({
chunksToHighlight,
totalLength
}): Array<Chunk> => {
const allChunks = []
const append = (start, end, highlight) => {
if (end - start > 0) {
allChunks.push({
start,
end,
highlight
})
}
}
if (chunksToHighlight.length === 0) {
append(0, totalLength, false)
} else {
let lastIndex = 0
chunksToHighlight.forEach((chunk) => {
append(lastIndex, chunk.start, false) // 保存 非高亮 token
append(chunk.start, chunk.end, true) // 保存 高亮 token
lastIndex = chunk.end
})
append(lastIndex, totalLength, false)
}
return allChunks
}
const Highlighter = ({
// 1. 主要的配置参数
highlightClassName = '', // 主要是 高亮和非高亮文字的 className
textToHighlight, // 和传入的文本内容
unhighlightClassName = '',
}) {
const chunks = findAll({ // 2. 调用 findAll 将文本内容解析为 token 数组
autoEscape,
caseSensitive,
findChunks,
sanitize,
searchWords,
textToHighlight
})
const HighlightTag = 'mark'
let highlightIndex = -1
let highlightClassNames = ''
let highlightStyles
return createElement('span', {
className,
...rest,
children: chunks.map((chunk, index) => { // 3. 根据 token 的 start 和 end
// 渲染每一个 token 的文本内容
const text = textToHighlight.substr(chunk.start, chunk.end - chunk.start)
if (chunk.highlight) { // 4. 根据是否高亮渲染对应样式的 dom
highlightIndex++
let highlightClass = highlightClassName
const isActive = highlightIndex === +activeIndex
highlightClassNames = `${highlightClass} ${isActive ? activeClassName : ''}`
highlightStyles = isActive === true && activeStyle != null
? Object.assign({}, highlightStyle, activeStyle)
: highlightStyle
const props = {
children: text,
className: highlightClassNames,
key: index,
style: highlightStyles
}
return createElement(HighlightTag, props)
} else {
return createElement('span', { // 5. 非高亮, 创建普通的 span
children: text,
className: unhighlightClassName,
key: index,
style: unhighlightStyle
})
}
})
})
}
实际上, 该库实现非常简单, 也并无技巧性的优化
所以性能要求不高, 文本内容长度可控的普通场景可以使用该库, 大量日志文本的高亮, 不推荐使用该库