React Native 中的高亮实现

452 阅读3分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第3天,点击查看活动详情

背景

高亮的注释:现指指让某人的帖子或用户名等突出、醒目,更易被点击或搜索。

因为我们做的是搜索项目,需要在搜索后把召回结果中和搜索词相关的文字高亮,类似百度这种:

屏幕快照 2022-08-15 下午2.24.48.png

在H5中相对简单,由后台直接生产富文本,通过html渲染即可。但因为项目使用的是React Native,对于富文本支持度不高,一些第三方库在把富文本转React Native效果并不是很好,所以自己写了一个高亮组件,用于处理内容高亮。

数据格式

因为之前否决过富文本的格式,所以和后端协商后,将高亮词直接返给了前端,在渲染时的数据大概是这样的:

// 下面是格式和简化后的数据
const data = {
    title: '平安好车主车险险情救援',
    highLightWords: ['车险', '险情']
}

因为在React Native中文本内容不能像html一样可以放在任意一个标签内,只能放在内置的 Text 组件中。接下来我们开始设计这个组件。

所以我们高亮后的组件应该是这样的:

const { title, highLightWords } = data
<TextHighLight
    text={title}
    highLightWords={highLightWords}
    style={{ fontSize: 12 }}
    hightLigthColor={'pink'}
    />

由上可以看到我们的高亮组件应该是一个无状态组件,所以只需要设计 props,必须传的属性有 2个 即需要高亮的 text 和 高亮词 highLightWords,另外还需要一些可选的样式定义 style,高亮词的颜色 hightLigthColor。现在来写下高亮组件:

function TextHighLight(props) {
    const { title = '', highLightWords = [] } = props
    const { numberOfLines, ellipsizeMode } = props
    const { style } = props
    
    const { markKeywordList, hightList } = markKeywords(title, highLightWords)
    
    return <Text
            numberOfLines={numberOfLines}
            ellipsizeMode={ellipsizeMode}
            style={style}
        >
            {
                markKeywordList ?
                    markKeywordList.map((item,index) => (
                        (hightList && hightList.some(i => (i.toLocaleUpperCase().includes(item) || i.toLowerCase().includes(item))))
                            ? <Text key={index} style={{ color: '#FF6300' }}>{item}</Text>
                            : item
                    ))
                    : null
            }
        </Text>
}

其实组件的实现并不复杂,关键的难点在于对 内容文本 和 高亮词 的解析。接下来我们来写 markKeywords 这个工具函数。

首先我们根据上面的组件分析下 markKeywords 解析出的数据结果

const title = '好车主车险情问题好车主车险'
const highLightWords = ['车险', '险情']

我们需要把 title 也拆成数组,在遍历时,根据是否匹配 highLightWords 中的高亮词去判断是否要拼接上高亮的标签

const markKeywords = (text, highlight) => {
    if (!text || !highlight) return { value: [text], highlight: [] }
    
    for (let index = 0; index < highlight.length; index++) {
        const reg = new RegExp(highlight[index], 'g');
        text = text.replace(reg, `**${highlight[index]}**`)
    }
    
    return {
        markKeywordList: text.split('**').filter(item => item),
        hightList: highlight.map(item => item.toLocaleUpperCase())
    }
} 

上面的方法中,通过正则先遍历高亮词对文本做标记,然后再拼回数组,最后把一些空值过滤掉,组成相应的格式返回。上述方法在大部分场景够用,但是在处理一些特殊场景时会出现问题,比如上面的

const title = '好车主车险情问题好车主车险'
const highLightWords = ['车险', '险情']

此时因为打上 ** 标记的原因,车险情 在被标记成 **车险**情 后,并不能被 险情 匹配,而且即使能匹配也不能把 车险情 拆成 **车险******险情**,如果能拆成 **车险情** 就好了。

function sort(letter, substr) {
  letter = letter.toLocaleUpperCase()
  substr = substr.toLocaleUpperCase()
  var pos = letter.indexOf(substr)
  var positions = []
  while(pos > -1) {
     positions.push(pos)
     pos = letter.indexOf(substr, pos + 1)
  }

  return positions.map(item => ([item, item + substr.length]))
}

// 高亮词第一次遍历索引
function format (text, hight) {
  var arr = []
  // hight.push(hight.reduce((prev, curr) => prev+curr), '')
  hight.forEach((item, index) => {
    arr.push(sort(text, item))
  })

  return arr.reduce((acc, val) => acc.concat(val), []);
}

// 合并索引区间
var merge = function(intervals) {
  const n = intervals.length;

  if (n <= 1) {
      return intervals;
  }

  intervals.sort((a, b) => a[0] - b[0]);

  let refs = [];
  refs.unshift([intervals[0][0], intervals[0][1]]);

  for (let i = 1; i < n; i++) {
      let ref = refs[0];

      if (intervals[i][0] < ref[1]) {
          ref[1] = Math.max(ref[1], intervals[i][1]);
      } else {
          refs.unshift([intervals[i][0], intervals[i][1]]);
      }
  }

  return refs.sort((a,b) => a[0] - b[0]);
}

function getHightLightWord (text, hight) {
  var bj = merge(format(text, hight))
  const c = text.split('')
  var bjindex = 0
  try {
    bj.forEach((item, index) => {
      item.forEach((_item, _index) => {
          c.splice(_item + bjindex, 0, '**')
          bjindex+=1
      })
    })
  } catch (error) {
  }
  return c.join('').split('**')
}

export const markKeywords = (text, keyword) => {

  if (!text || !keyword || keyword.length === 0 ) {
    return { value: [text], keyword: [] }
  }
  if (Array.isArray(keyword)) {
    keyword = keyword.filter(item => item)
  }
  let obj = { value: [text], keyword };
  obj = {
    value: getHightLightWord(text, keyword).filter((item) => item),
    keyword: keyword.map((item) => item.toLocaleUpperCase()),
  };
  return obj;
};

上述方法中我们先使用了下标匹配的方式,得到一个下标值的映射,然后通过区间合并的方式把连着的词做合并处理,最后再用合并后的下标值映射去打 ** 标记即可。