用HTML包装多个字符串React方式

时间:2017-05-12 08:18:59

标签: javascript reactjs jsx

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

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事件&#39; React方式&#39 ;在任何这些突出显示的实体上,这是我需要做的事情。

这样做的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渲染的字符串和对象数组,但是一旦实体数组很大(&gt; 100)它就不是很有效并且文本正文也很大(> 100kb)。我们要查看大约10倍的时间来将其渲染为数组V&#39。是一个字符串。

有没有人知道更好的方法来提供渲染大字符串的速度,但是能够在元素上附加React事件的灵活性?或者危险地SetInnerHTML是这种情况下的最佳解决方案吗?

3 个答案:

答案 0 :(得分:5)

你尝试过这样的事吗?

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

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

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

一个JS小提琴你的例子: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>

答案 1 :(得分:4)

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

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

答案 2 :(得分:2)

我做的第一件事就是将段落分成几个单词。

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, '$1').toLowerCase();
}

希望这有帮助!