React-highlight-words 文本高亮库 源码解析

在做日志关键字高亮的需求时, 有发现到一个文本高亮的库 react-highlight-words

实际它的核心是 : highlight-words-core

用法非常简单 :

import Highlighter from "react-highlight-words";

  <Highlighter
    highlightClassName="hightlight样式的 Class"
    searchWords={["emmm"]} // 需要高亮的关键字
    autoEscape={true}
    textToHighlight="需要高亮的日志内容 emmm !!! "
  />

由于比较好奇他对日志这种超长文本是如何解析高亮, 并且兼顾性能的, 带着问题去看一波源码 ~

补课

关键字高亮这个功能的实现涉及到编译原理, 这里不具体展开

语法分析 parsing

词法分析 lexing

对于前端同学来说, 最熟悉编译流程如下 :

读取 字符串流 => 词法分析, 生成 token => 语法分析, 生成 AST => babel 操作 AST => 生成 js 代码 => 浏览器执行

React hightlight words 实现

核心方法 findAll : 找到所有匹配 searchWords 的字符串的位置

  /**
   * @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
    }, [])
  }

fillInChunks 将所有内容转换为 token 数组

上面的 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
}

到这里 highlight-words-core 做的事就结束了

接下来是 React-hightlight-words 渲染 token 数组形式的 文本

  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
          })
        }
      })
    })
  }

emmm

实际上, 该库实现非常简单, 也并无技巧性的优化

所以性能要求不高, 文本内容长度可控的普通场景可以使用该库, 大量日志文本的高亮, 不推荐使用该库