以 React 方式在 HTML 中包装多个字符串

IT技术 javascript reactjs jsx
2021-04-27 11:11:19

我正在构建一个实体荧光笔,以便我可以上传文本文件,查看屏幕上的内容,然后突出显示数组中的单词。这是数组由用户在手动突出显示选择时填充,例如..

const entities = ['John Smith', 'Apple', 'some other word'];

This is my text document that is displayed on the screen. It contains a lot of text, and some of this text needs to be visually highlighted to the user once they manually highlight some text, like the name John Smith, Apple and some other word

现在,我想通过将其包装在一些标记中来直观地突出显示文本中实体的所有实例,并且执行以下操作非常有效:

getFormattedText() {
    const paragraphs = this.props.text.split(/\n/);
    const { entities } = this.props;

    return paragraphs.map((p) => {
        let entityWrapped = p;

        entities.forEach((text) => {
        const re = new RegExp(`${text}`, 'g');
        entityWrapped =
            entityWrapped.replace(re, `<em>${text}</em>`);
        });

        return `<p>${entityWrapped}</p>`;
    }).toString().replace(/<\/p>,/g, '</p>');
}

...但是(!),这只是给了我一个大字符串,所以我必须危险地设置内部 HTML,因此我不能在任何这些突出显示的实体上附加一个 onClick 事件“react方式”,这是我需要做的事情。

React 这样做的方法是返回一个看起来像这样的数组:

['This is my text document that is displayed on the screen. It contains a lot of text, and some of this text needs to be visually highlighted to the user, like the name', {}, {}, {}]{}包含 JSX 内容的 React 对象在哪里

我已经用几个嵌套循环对此进行了尝试,但它有很多问题,难以阅读,并且随着我逐渐添加更多实体,性能受到了巨大打击。

所以,我的问题是......解决这个问题的最佳方法是什么?确保代码简单易读,并且我们不会遇到巨大的性能问题,因为我可能会处理很长的文档。这是我放弃我的 React 道德和危险的 SetInnerHTML 以及直接绑定到 DOM 的事件的时候吗?

更新

@AndriciCezar 在下面的回答在格式化准备好 React 渲染的字符串和对象数组方面做得很好,但是一旦实体数组很大(> 100)并且文本主体也很大(> 100kb),它的性能就不是很好. 我们正在寻找大约 10 倍的时间来将其呈现为数组 V 的字符串。

有谁知道一种更好的方法来做到这一点,它可以提高渲染大字符串的速度,但可以灵活地将 React 事件附加到元素上?或者是在这种情况下危险的SetInnerHTML 是最好的解决方案?

3个回答

这是一个使用正则表达式拆分每个关键字上的字符串的解决方案。如果您不需要它不区分大小写或突出显示多个单词的关键字,则可以简化此操作。

import React from 'react';

const input = 'This is a test. And this is another test.';
const keywords = ['this', 'another test'];

export default class Highlighter extends React.PureComponent {
    highlight(input, regexes) {
        if (!regexes.length) {
            return input;
        }
        let split = input.split(regexes[0]);
        // Only needed if matches are case insensitive and we need to preserve the
        // case of the original match
        let replacements = input.match(regexes[0]);
        let result = [];
        for (let i = 0; i < split.length - 1; i++) {
            result.push(this.highlight(split[i], regexes.slice(1)));
            result.push(<em>{replacements[i]}</em>);
        }
        result.push(this.highlight(split[split.length - 1], regexes.slice(1)));
        return result;
    }
    render() {
        let regexes = keywords.map(word => new RegExp(`\\b${word}\\b`, 'ig'));
        return (
            <div>
                { this.highlight(input, regexes) }
            </div>);
    }
}

你有没有尝试过这样的事情?

复杂度是段落数*关键字数。对于一段 22,273 个单词(121,104 个字符)和 3 个关键字,在我的 PC 上生成数组需要 44 毫秒。

!!!更新:我认为这是突出关键字的最清晰、最有效的方式。我使用 James Brierley 的答案来优化它。

我用 500 个关键字对 320kb 的数据进行了测试,但加载速度很慢。另一个想法是使段落渐进。渲染前 10 个段落,然后在滚动时或一段时间后渲染其余的段落。

还有一个 JS Fiddle 和你的例子:https : //jsfiddle.net/69z2wepo/79047/

const Term = ({ children }) => (
  <em style={{backgroundColor: "red"}} onClick={() => alert(children)}>
    {children}
  </em>
);

const Paragraph = ({ paragraph, keywords }) => {
  let keyCount = 0;
  console.time("Measure paragraph");

  let myregex = keywords.join('\\b|\\b');
  let splits = paragraph.split(new RegExp(`\\b${myregex}\\b`, 'ig'));
  let matches = paragraph.match(new RegExp(`\\b${myregex}\\b`, 'ig'));
  let result = [];

  for (let i = 0; i < splits.length; ++i) {
    result.push(splits[i]);
    if (i < splits.length - 1)
      result.push(<Term key={++keyCount}>{matches[i]}</Term>);
  }

  console.timeEnd("Measure paragraph");

  return (
    <p>{result}</p>
  );
};


const FormattedText = ({ paragraphs, keywords }) => {
    console.time("Measure");

    const result = paragraphs.map((paragraph, index) =>
      <Paragraph key={index} paragraph={paragraph} keywords={keywords} /> );

    console.timeEnd("Measure");
    return (
      <div>
        {result}
      </div>
    );
};

const paragraphs = ["Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla ornare tellus scelerisque nunc feugiat, sed posuere enim congue. Vestibulum efficitur, erat sit amet aliquam lacinia, urna lorem vehicula lectus, sit amet ullamcorper ex metus vitae mi. Sed ullamcorper varius congue. Morbi sollicitudin est magna. Pellentesque sodales interdum convallis. Vivamus urna lectus, porta eget elit in, laoreet feugiat augue. Quisque dignissim sed sapien quis sollicitudin. Curabitur vehicula, ex eu tincidunt condimentum, sapien elit consequat enim, at suscipit massa velit quis nibh. Suspendisse et ipsum in sem fermentum gravida. Nulla facilisi. Vestibulum nisl augue, efficitur sit amet dapibus nec, convallis nec velit. Nunc accumsan odio eu elit pretium, quis consectetur lacus varius"];
const keywords = ["Lorem Ipsum"];

class App extends React.Component {
  constructor(props) {
    super(props);

    this.state = {
      limitParagraphs: 10
    };
  }

  componentDidMount() {
    setTimeout(
      () =>
        this.setState({
          limitParagraphs: 200
        }),
      1000
    );
  }

  render() {
    return (
      <FormattedText paragraphs={paragraphs.slice(0, this.state.limitParagraphs)} keywords={keywords} />
    );
  }
}

ReactDOM.render(
  <App />, 
  document.getElementById("root"));
<script src="https://cdn.jsdelivr.net/lodash/4.17.4/lodash.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>

<div id="root">
</div>

我做的第一件事是将段落拆分为一组单词。

const words = paragraph.split( ' ' );

然后我将 words 数组映射到一堆<span>标签。这允许我将onDoubleClick事件附加到每个单词。

return (
  <div>
    {
      words.map( ( word ) => {
        return (
          <span key={ uuid() }
                onDoubleClick={ () => this.highlightSelected() }>
                {
                  this.checkHighlighted( word ) ?
                  <em>{ word } </em>
                  :
                  <span>{ word } </span>
                }
          </span>
        )
      })
    }
  </div>
);

因此,如果双击某个单词,我会触发该this.highlightSelected()函数,然后根据是否突出显示该单词有条件地呈现该单词。

highlightSelected() {

    const selected = window.getSelection();
    const { data } = selected.baseNode;

    const formattedWord = this.formatWord( word );
    let { entities } = this.state;

    if( entities.indexOf( formattedWord ) !== -1 ) {
      entities = entities.filter( ( entity ) => {
        return entity !== formattedWord;
      });
    } else {
      entities.push( formattedWord );
    }  

    this.setState({ entities: entities });
}

我在这里所做的只是在我的组件状态下将单词删除或推送到数组中。checkHighlighted()只会检查正在呈现的单词是否存在于该数组中。

checkHighlighted( word ) {

    const formattedWord = this.formatWord( word );

    if( this.state.entities.indexOf( formattedWord ) !== -1 ) {
      return true;
    }
    return false;
  }

最后,该formatWord()函数只是删除任何句点或逗号并使所有内容小写。

formatWord( word ) {
    return word.replace(/([a-z]+)[.,]/ig, '$1').toLowerCase();
}

希望这可以帮助!