如何使用React渲染原始HTML?

274
这是使用ReactJS渲染原始HTML的唯一方法吗?
// http://facebook.github.io/react/docs/tutorial.html
// tutorial7.js
var converter = new Showdown.converter();
var Comment = React.createClass({
  render: function() {
    var rawMarkup = converter.makeHtml(this.props.children.toString());
    return (
      <div className="comment">
        <h2 className="commentAuthor">
          {this.props.author}
        </h2>
        <span dangerouslySetInnerHTML={{__html: rawMarkup}} />
      </div>
    );
  }
});

我知道有一些很酷的方法可以使用JSX标记内容,但我主要感兴趣的是能够渲染原始的HTML(包括所有的类、内联样式等)。像这样复杂的内容:
<!-- http://getbootstrap.com/components/#dropdowns-example -->
<div class="dropdown">
  <button class="btn btn-default dropdown-toggle" type="button" id="dropdownMenu1" data-toggle="dropdown" aria-expanded="true">
    Dropdown
    <span class="caret"></span>
  </button>
  <ul class="dropdown-menu" role="menu" aria-labelledby="dropdownMenu1">
    <li role="presentation"><a role="menuitem" tabindex="-1" href="#">Action</a></li>
    <li role="presentation"><a role="menuitem" tabindex="-1" href="#">Another action</a></li>
    <li role="presentation"><a role="menuitem" tabindex="-1" href="#">Something else here</a></li>
    <li role="presentation"><a role="menuitem" tabindex="-1" href="#">Separated link</a></li>
  </ul>
</div>

我不想用JSX重新写所有的东西。
也许我对这个问题的思考方式有问题。请纠正我。

1
这几乎就是JSX。如果您将大量标记呈现为原始HTML,则会失去使用React等库的好处。我建议进行小的更改(例如“class”->“className”),以便让React处理元素。 - Ross Allen
2
对于这个具体的例子,有人已经为您完成了工作(http://react-bootstrap.github.io/components.html#btn-dropdowns),但是问题仍然存在于一般情况下。 - Randy Morris
https://medium.com/@to_pe/how-to-add-react-to-a-simple-html-file-a11511c0235f - TechDog
HTML转JSX转换工具:https://transform.tools/html-to-jsx - Lazor
14个回答

265

现在有更安全的方法来呈现HTML。我在之前的回答中 这里 已经谈到了这个话题。你有4个选项,最后一个使用了dangerouslySetInnerHTML

呈现HTML的方法

  1. 最简单 - 使用Unicode,将文件保存为UTF-8并将charset设置为UTF-8。

    <div>{'First · Second'}</div>

  2. 更加安全 - 在Javascript字符串中使用实体的Unicode编号。

    <div>{'First \u00b7 Second'}</div>

    或者

    <div>{'First ' + String.fromCharCode(183) + ' Second'}</div>

  3. 或者使用包含字符串和JSX元素的混合数组。

    <div>{['First ', <span>&middot;</span>, ' Second']}</div>

  4. 最后选择 - 使用dangerouslySetInnerHTML插入原始的HTML。

    <div dangerouslySetInnerHTML={{__html: 'First &middot; Second'}} />


2
想知道除了__html之外,dangerouslySetInnerHTML还接收哪些其他属性。 - Juan Solano
1
@JuanSolano 在 TypeScript 环境中,根据自动完成条目,没有任何内容。 - Andreas Linnert
在一个类似的问题中,https://dev59.com/gmIk5IYBdhLWcg3wdd0l#27938353,这个答案表现不太好。也许它更适合这个问题,或者也许人们没有阅读答案... :D - 425nesp
4
人们对于dangerousSetInnerHtml过于敏感。虽然开发人员应该了解它潜在地会成为XSS攻击的一个途径,但是有一些合理的使用情况使得这是可行的实用方法,特别是在你设置的HTML内容不是来自用户输入或者来源已经被消毒的用户输入的情况下。如果你知道这个HTML并且有一个将其存储到变量或其他东西中的原因,那么这绝对是最优雅的方法。 - hurlbz

91

dangerouslySetInnerHTML 是 React 中替代在浏览器 DOM 中使用 innerHTML 的方法。通常情况下,从代码中设置 HTML 是有风险的,因为很容易无意中使用户暴露于跨站脚本攻击 (XSS)。

最好/更安全的做法是在使用 dangerouslySetInnerHTML 将其注入到 DOM 之前,对原始 HTML 进行净化处理(例如使用 DOMPurify)。

DOMPurify 是一个仅针对 HTML、MathML 和 SVG 的 DOM 清理库,运行速度超快且非常宽容。DOMPurify 采用了安全的默认设置,同时提供了许多可配置和钩子选项。

示例

import React from 'react'
import createDOMPurify from 'dompurify'
import { JSDOM } from 'jsdom'

const window = (new JSDOM('')).window
const DOMPurify = createDOMPurify(window)

const rawHTML = `
<div class="dropdown">
  <button class="btn btn-default dropdown-toggle" type="button" id="dropdownMenu1" data-toggle="dropdown" aria-expanded="true">
    Dropdown
    <span class="caret"></span>
  </button>
  <ul class="dropdown-menu" role="menu" aria-labelledby="dropdownMenu1">
    <li role="presentation"><a role="menuitem" tabindex="-1" href="#">Action</a></li>
    <li role="presentation"><a role="menuitem" tabindex="-1" href="#">Another action</a></li>
    <li role="presentation"><a role="menuitem" tabindex="-1" href="#">Something else here</a></li>
    <li role="presentation"><a role="menuitem" tabindex="-1" href="#">Separated link</a></li>
  </ul>
</div>
`

const YourComponent = () => (
  <div>
    { <div dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(rawHTML) }} /> }
  </div>
)

export default YourComponent

1
我在不使用JSDOM的情况下尝试了这个方法,它非常成功。非常感谢!通过阅读这个线程,我认识到一个带有CMS的Web应用程序可能会暴露于XSS攻击,特别是给非技术人员使用时。 - Vladyn

78

您可以利用html-to-react npm模块。

注:我是该模块的作者,几小时前刚刚发布。如有任何错误或可用性问题,请随时报告。


33
它在内部使用 dangerouslySetInnerHTML 吗? - Yash Sharma
4
我刚刚检查了代码,似乎没有使用dangerouslySetInnerHTML。 - Ajay Raghav
1
Mike,你还在积极维护这个npm模块吗? - Daniel
1
如果不使用dangerouslySetInnerHTML,那么它是如何工作的? - 425nesp
8
这个程序很臃肿,使用了Ramda等大型库——我不建议使用。 - evanjmg
显示剩余3条评论

40

我在快速而不精细的情况下使用过这个:

// react render method:

render() {
    return (
      <div>
        { this.props.textOrHtml.indexOf('</') !== -1
            ? (
                <div dangerouslySetInnerHTML={{__html: this.props.textOrHtml.replace(/(<? *script)/gi, 'illegalscript')}} >
                </div>
              )
            : this.props.textOrHtml
          }

      </div>
      )
  }

9
这不安全,如果HTML包含<img src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7" onload="alert('test');">会怎样? - Yong Jie Wong
31
只要您掌控渲染的HTML,就是安全的。 - John

25

我尝试过这个纯组件:

const RawHTML = ({children, className = ""}) => 
<div className={className}
  dangerouslySetInnerHTML={{ __html: children.replace(/\n/g, '<br />')}} />

特点

  • 接受className属性(更易于样式化)
  • \n替换为<br />(通常您会想要这样做)
  • 在使用组件时,将内容作为children放置,例如:
  • <RawHTML>{myHTML}</RawHTML>

我已经在Github的Gist中放置了该组件:RawHTML: ReactJS 纯组件用于呈现HTML


14
export class ModalBody extends Component{
    rawMarkup(){
        var rawMarkup = this.props.content
        return { __html: rawMarkup };
    }
    render(){
        return(
                <div className="modal-body">
                     <span dangerouslySetInnerHTML={this.rawMarkup()} />

                </div>
            )
    }
}

以上对我有效,我将HTML传递给模态框主体。 - Jozcar
如果您在锚点标签中使用了哈希符号作为内部锚点,它将无法正常工作。到目前为止,我还没有找到解决方案来引入具有内部哈希标记的HTML,并使其作为内部哈希标记正常工作,因为锚点会重新渲染页面到错误位置。 - Eric Clarke

11

我使用了名为 Parser 的 这个库。它对我所需的内容起作用了。

import React, { Component } from 'react';    
import Parser from 'html-react-parser';

class MyComponent extends Component {
  render() {
    <div>{Parser(this.state.message)}</div>
  }
};

6
21KB(压缩后为8KB)的大小对于浏览器代码来说有些难以接受,我无法为此进行辩解。 - Peter Bengtsson
这个库导致我的Expo应用程序出现故障。 - ekkis

6

dangerouslySetInnerHTML应该只在绝对必要的情况下使用。根据文档,"这主要是为了与DOM字符串操作库协作"。当你使用它时,你会失去React DOM管理的好处。

在您的情况下,将class属性更改为className即可轻松转换为有效的JSX语法。或者,如上面的评论中所提到的那样,您可以使用ReactBootstrap库,该库将Bootstrap元素封装到React组件中。


8
谢谢您的提问。我理解使用innerHTML存在的安全问题,并且知道我可以将其转换为JSX。但是我的具体问题是有关React支持使用原始HTML代码片段的情况。 - vinhboy
“React 支持使用原始 HTML 片段”这句话的确切含义是什么? - Breno Ferreira
我别无选择,只能使用“dangerouslySetInnerHTML”!!! - Zia Ullah

4
这里是之前发布的RawHTML函数的一个不那么主观的版本。它可以让你:
  • 配置标签
  • 可选地将换行符替换为<br />
  • 传递额外的属性,RawHTML会将其传递给创建的元素
  • 提供空字符串(RawHTML></RawHTML>)
这是组件:
const RawHTML = ({ children, tag = 'div', nl2br = true, ...rest }) =>
    React.createElement(tag, {
        dangerouslySetInnerHTML: {
            __html: nl2br
                ? children && children.replace(/\n/g, '<br />')
                : children,
        },
        ...rest,
    });
RawHTML.propTypes = {
    children: PropTypes.string,
    nl2br: PropTypes.bool,
    tag: PropTypes.string,
};

使用方法:

<RawHTML>{'First &middot; Second'}</RawHTML>
<RawHTML tag="h2">{'First &middot; Second'}</RawHTML>
<RawHTML tag="h2" className="test">{'First &middot; Second'}</RawHTML>
<RawHTML>{'first line\nsecond line'}</RawHTML>
<RawHTML nl2br={false}>{'first line\nsecond line'}</RawHTML>
<RawHTML></RawHTML>

输出:

<div>First · Second</div>
<h2>First · Second</h2>
<h2 class="test">First · Second</h2>
<div>first line<br>second line</div>
<div>first line
second line</div>
<div></div>

它会在以下情况下出现错误:

<RawHTML><h1>First &middot; Second</h1></RawHTML>

3
这里提供一个只有两个步骤的解决方案:
  1. 使用内置API将原始HTML字符串解析为HTML Element
  2. 递归地将Element对象(及其子对象)转换为ReactElement对象。

注意:这是一个学习的好例子。但请考虑其他答案中描述的选项,如html-to-react库。

此解决方案的特点:


以下是.jsx代码:
// RawHtmlToReactExample.jsx
import React from "react";

/**
 * Turn a raw string representing HTML code into an HTML 'Element' object.
 *
 * This uses the technique described by this StackOverflow answer: https://dev59.com/nnRB5IYBdhLWcg3w1Kr0#35385518
 * Note: this only supports HTML that describes a single top-level element. See the linked post for more options.
 *
 * @param {String} rawHtml A raw string representing HTML code
 * @return {Element} an HTML element
 */
function htmlStringToElement(rawHtml) {
    const template = document.createElement('template');
    rawHtml = rawHtml.trim();
    template.innerHTML = rawHtml;
    return template.content.firstChild;
}

/**
 * Turn an HTML element into a React element.
 *
 * This uses a recursive algorithm. For illustrative purposes it logs to the console.
 *
 * @param {Element} el
 * @return {ReactElement} (or a string in the case of text nodes?)
 */
function elementToReact(el) {
    const tagName = el.tagName?.toLowerCase(); // Note: 'React.createElement' prefers lowercase tag names for HTML elements.
    const descriptor = tagName ?? el.nodeName;
    const childNodes = Array.from(el.childNodes);
    if (childNodes.length > 0) {
        console.log(`This element ('${descriptor}') has child nodes. Let's transform them now.`);
        const childReactElements = childNodes.map(childNode => elementToReact(childNode)).filter(el => {
            // In the edge case that we found an unsupported node type, we'll just filter it out.
            return el !== null
        });
        return React.createElement(tagName, null, ...childReactElements);
    } else {
        // This is a "bottom out" point. The recursion stops here. The element is either a text node, a comment node,
        // and maybe some other types. I'm not totally sure. Reference the docs to understand the different node
        // types: https://developer.mozilla.org/en-US/docs/Web/API/Node/nodeType
        console.log(`This element ('${descriptor}') has no child nodes.`);

        // For simplicity, let's only support text nodes.
        const nodeType = el.nodeType;
        if (nodeType === Node.TEXT_NODE) {
            return el.textContent;
        } else {
            console.warn(`Unsupported node type: ${nodeType}. Consider improving this function to support this type`);
            return null;
        }
    }
}

export function RawHtmlToReactExample() {
    const myRawHtml = `<p>This is <em>raw</em> HTML with some nested tags. Let's incorporate it into a React element.`;
    const myElement = htmlStringToElement(myRawHtml);
    const myReactElement = elementToReact(myElement);

    return (<>
        <h1>Incorporate Raw HTML into React</h1>

        {/* Technique #1: Use React's 'dangerouslySetInnerHTML' attribute */}
        <div dangerouslySetInnerHTML={{__html: myRawHtml}}></div>

        {/* Technique #2: Use a recursive algorithm to turn an HTML element into a React element */}
        {myReactElement}
    </>)
}

const descriptor = tagName ?? el.nodeName; 这行代码是什么意思? - Tayyab Ferozi
1
好问题,该语句仅有助于稍后发生的调试控制台日志记录。该行获取“可读性描述符”el对象。有时,el是HTML元素,它具有标记名称(例如,adivli),但有时el是TextNode。 TextNode没有标记名称,但它们确实有节点名称。??运算符是这个 https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Operators/Nullish_coalescing_operator - David Groomes

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接