【问题标题】:Wrap multiple strings in HTML the React way以 React 方式在 HTML 中包装多个字符串
【发布时间】:2017-10-11 11:33:47
【问题描述】:

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

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 这样做的方式是返回一个看起来像这样的数组:

['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 事件附加到元素上?或者在这种情况下,dangerouslySetInnerHTML 是最好的解决方案吗?

【问题讨论】:

  • 如果您使用 Stack Snippets([&lt;&gt;] 工具栏按钮)添加了一个可运行的minimal reproducible example,它会帮助人们回答这个问题,显示您想要添加文本的结构,文本来自哪里等。 Stack Snippets 支持 React,包括 JSX; here's how to do one.
  • DanV,您需要更好地解决您的问题吗?也许我理解错了你问的内容?
  • 嘿@AndriciCezar 你的回答看起来很棒,我只是没有时间付诸行动。谢谢顺便说一句!
  • DanV 你觉得我更新的答案怎么样?

标签: javascript reactjs jsx


【解决方案1】:

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

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

【讨论】:

  • 感谢您的代码,这看起来比@AndriciCezar 的回答更有效率。但是它对我来说会导致无限循环:jsfiddle.net/69z2wepo/78947 并且也没有解决渲染问题。即使有 10,000 个项目生成数组也不是很昂贵,它是渲染。
  • @DanV 哎呀,修复了编辑中的一个错误并在jsfiddle.net/69z2wepo/78956更新了小提琴
  • @DanV 如果您包含遇到性能问题的示例输入,将会有所帮助。
  • 这感觉非常快,尽管我正在努力测量渲染时间,因为由于某种原因 componentDidUpdate 没有在小提琴中触发。我用一些示例数据更新了它jsfiddle.net/69z2wepo/79008
  • @DanV 你有时间和 timeEnd 调用错误的方式。我已将它们切换到jsfiddle.net/69z2wepo/79009。对我来说它在 675-927 毫秒之间。
【解决方案2】:

你尝试过这样的事情吗?

复杂度是段落数*关键字数。 一段 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>

【讨论】:

  • 这很好用 - 谢谢!如果它不依赖于 Lodash,那就太好了;)
  • 您可以为flatten 创建一个polyfill,您将不依赖于lodash。用 lodash 创建 sn-p 要简单得多:)
  • 嘿@AndriciCezar 在用真实数据测试后我意识到你的答案只是解决方案的一半。我正在使用一个包含大约 100 个匹配实体的 150kb 文本文件,虽然生成数组并不太昂贵,但在 Chrome 中生成数组和更新 DOM 所需的总时间大约为 1.5 秒体面的 MBP,相比之下,如果我只生成一个字符串和危险的 SetInnerHTML,则需要 200 毫秒。
  • 那么这意味着渲染花费的时间最多。第一次渲染后,如果再添​​加一个段落,需要多少?
  • 我想知道Virtual Dom的创建需要时间还是只是渲染。
【解决方案3】:

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

const words = paragraph.split( ' ' );

然后我将单词数组映射到一堆&lt;span&gt; 标签。这允许我将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();
}

希望这会有所帮助!

【讨论】:

  • 不要认为这适用于包含多个单词的实体,即“John Smith”
猜你喜欢
  • 1970-01-01
  • 2011-12-23
  • 2016-07-20
  • 1970-01-01
  • 1970-01-01
  • 2020-09-02
  • 2018-09-22
  • 2018-07-28
  • 1970-01-01
相关资源
最近更新 更多