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

Wrap multiple strings in HTML the React way

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

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 事件 'the React way'在任何这些突出显示的实体上,这是我需要做的事情。

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

['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 道德和 dangerouslySetInnerHTML 以及直接绑定到 DOM 的事件的时候吗?

更新

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

有没有人知道更好的方法来执行此操作,既可以提高渲染大字符串的速度,又可以灵活地将 React 事件附加到元素上?或者 dangerouslySetInnerHTML 是这种情况下的最佳解决方案吗?

你试过这样的东西吗?

复杂度是段落数*关键字数。 一段22273个词(121104个字符)和3个关键字,在我的PC上生成数组需要44ms。

!!!更新: 我认为这是突出关键字的最清晰,最有效的方法。我使用 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( ' ' );

然后我将单词数组映射到一堆 <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, '').toLowerCase();
}

希望对您有所帮助!

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

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