使用 Fragment 插入 HTML 通过 dangerouslySetInnerHTML 在后端渲染

Using Fragment to insert HTML rendered on the back end via dangerouslySetInnerHTML

我曾经通过

编译和插入JSX组件
<div key={ ID } dangerouslySetInnerHTML={ { __html: HTML } } />

将我的 HTML 包装成 <div>:

<div>my html from the HTML object</div>

现在 react > 16.2.0 has support for Fragments 我想知道我是否可以使用它来避免每次从后端获取数据时将我的 HTML 包装在 <div> 中。

运行

<Fragment key={ ID } dangerouslySetInnerHTML={ { __html: HTML } } />

会发出警告

Warning: Invalid prop `dangerouslySetInnerHTML` supplied to `React.Fragment`. React.Fragment can only have `key` and `children` props.
in React.Fragment

这完全受支持吗?还有其他方法可以解决这个问题吗?

更新

如果您想给它投票,请为它创建一个 issue in the react repo

简答

不可能:

key is the only attribute that can be passed to Fragment. In the future, we may add support for additional attributes, such as event handlers.

https://reactjs.org/docs/fragments.html

您可能想插话并建议将其作为未来的补充。

https://github.com/facebook/react/issues

同时

您可能需要考虑使用 HTML 解析库,例如:

https://github.com/remarkablemark/html-react-parser

查看此示例以了解它将如何实现您的目标:

http://remarkablemark.org/blog/2016/10/07/dangerously-set-innerhtml-alternative/

简而言之

您将能够做到这一点:

<>
{require('html-react-parser')(
    '<em>foo</em>'
)}
</>

2020 年 12 月更新

This issue (also mentioned by OP) was closed on Oct 2, 2019. - However, stemming from the original issue, it seems a RawHTML component has entered the RFC process 但尚未投入生产,也没有确定可行解决方案何时可用的时间表。

话虽如此,我现在想提及我目前用来解决这个问题的解决方案。

在我的例子中,dangerouslySetInnerHTML 用于呈现纯文本 HTML 供用户下载;在输出中包含额外的包装标签并不理想。

在浏览了网络和 Whosebug 之后,似乎大多数解决方案都提到了使用外部库,例如 html-react-parser

对于这个用例,html-react-parser 是不够的,因为它将 HTML 字符串转换为 React element(s)。意思是,它将删除所有非标准 JSX 的 HTML。


解法:

下面的代码是我选择使用的无库解决方案:

//HTML that will be set using dangerouslySetInnerHTML
const html = `<div>This is a div</div>`

RawHtml 组件中的包装器 div 被特意命名为“不需要的div”。

//Component that will return our dangerouslySetInnerHTML
//Note that we are using "unwanteddiv" as a wrapper
    const RawHtml = () => {
        return (
            <unwanteddiv key={[]}
                dangerouslySetInnerHTML={{
                    __html: html,
                }}
            />
        );
    };

出于本示例的目的,我们将使用 renderToStaticMarkup

const staticHtml = ReactDomServer.renderToStaticMarkup(
<RawHtml/>
);

ParseStaticHtml 函数是魔法发生的地方,在这里您将看到为什么我们将包装器 div 命名为“不需要的div”。

  //The ParseStaticHtml function will check the staticHtml 
  //If the staticHtml type is 'string' 
  //We will remove "<unwanteddiv/>" leaving us with only the desired output
    const ParseStaticHtml = (html) => {
        if (typeof html === 'string') {
            return html.replace(/<unwanteddiv>/g, '').replace(/<\/unwanteddiv>/g, '');
        } else {
            return html;
        }
  };

现在,如果我们将 staticHtml 传递给 ParseStaticHtml 函数,您将看到没有附加包装器的所需输出 div:

  console.log(ParseStaticHtml(staticHtml));

此外,我还创建了一个 codesandbox example 来展示这一点。

注意,控制台日志会抛出一条警告:“标签<unwanteddiv>在此浏览器中无法识别...” - 但是,这很好,因为我们有意给它一个唯一的名称,这样我们就可以使用 replace 方法轻松区分和定位包装器,并在输出前将其删除。

此外,受到代码 linter 的轻微责骂并不像为应该更简单地实现的东西添加更多依赖项那么糟糕。

这是一个仅适用于 <td> 元素的解决方案:

type DangerousHtml = {__html:string}

function isHtml(x: any): x is DangerousHtml {
    if(!x) return false;
    if(typeof x !== 'object') return false;
    const keys = Object.keys(x)
    if(keys.length !== 1) return false;
    return keys[0] === '__html'
}

const DangerousTD = forwardRef<HTMLTableCellElement,Override<React.ComponentPropsWithoutRef<'td'>,{children: ReactNode|DangerousHtml}>>(({children,...props}, ref) => {
    if(isHtml(children)) {
        return <td dangerouslySetInnerHTML={children} {...props} ref={ref}/>
    }
    return <td {...props} ref={ref}>{children}</td>
})

通过一些工作,您可以使它更通用,但这应该给出总体思路。

用法:

<DangerousTD>{{__html: "<span>foo</span>"}}</DangerousTD>