React 中 dangerouslySetInnerHTML 的替代方案

FreeGuideOnline 最新 2026-07-07

为什么需要替代 dangerouslySetInnerHTML

dangerouslySetInnerHTML 是 React 提供的特殊属性,允许你将 HTML 字符串直接插入 DOM。它的名字里就带着“危险”二字,主要原因有两个:

  • XSS(跨站脚本攻击)风险:如果内容中包含恶意脚本,直接插入会使得攻击者能够执行任意 JavaScript,窃取用户数据、劫持会话。
  • 违背 React 的声明式哲学:直接操作 innerHTML 绕过了 React 的虚拟 DOM 差异算法,导致组件难以维护,且无法利用 React 的事件系统和组件生命周期。

因此,大多数场景下我们应当寻找更安全的替代方案。

替代方案全景图

根据内容的来源和复杂度,可以选择不同的方法:

  • 纯文本内容:直接使用 {} 渲染即可,React 会自动转义。
  • 简单的 Markdown 或标签:使用轻量级解析器,如 marked 结合 DOMPurify,或使用 react-markdown 等组件。
  • 富文本且结构可控:采用无头 CMS 返回的结构化数据自行渲染。
  • 需要完整 HTML 的复杂场景:配合 DOMPurify 进行消毒后,再使用 dangerouslySetInnerHTML

纯文本方案:让 React 安全转义

如果后端返回的内容只是普通文本(可能含有 <> 等字符),直接放在 JSX 表达式中是最安全的。

function TextContent({ text }) {
  // React 自动转义 < 和 > 等符号,防止脚本注入
  return <p>{text}</p>;
}

适用场景:不需要任何富文本格式,仅展示用户昵称、短描述等。

Markdown 渲染方案

很多网站允许用户输入 Markdown 来生成富文本。我们可以使用专门的 React 组件库,它们内部已经处理了安全问题。

使用 react-markdown

react-markdown 是一个流行的库,它只会生成安全的 HTML 标签(如 <p><strong><a> 等),默认不会解析原始的 HTML,因此天生防 XSS。

安装:

npm install react-markdown

基本用法:

import ReactMarkdown from 'react-markdown';

function MarkdownRenderer({ markdown }) {
  return <ReactMarkdown>{markdown}</ReactMarkdown>;
}

如果需要更精细的控制,可以覆盖组件:

<ReactMarkdown
  components={{
    a: ({node, ...props}) => <a target="_blank" rel="noopener" {...props} />
  }}
>
  {markdown}
</ReactMarkdown>

使用 marked + DOMPurify(传统方案)

如果你不想引入一个新的 React 组件,可以用 marked 将 Markdown 转成 HTML 字符串,然后用 DOMPurify 消毒,最后再用 dangerouslySetInnerHTML。但注意,这种方式还是用到了 dangerouslySetInnerHTML,只是 HTML 经过了彻底净化。

import { marked } from 'marked';
import DOMPurify from 'dompurify';

function SafeMarkdown({ markdown }) {
  const rawHtml = marked.parse(markdown);
  const cleanHtml = DOMPurify.sanitize(rawHtml);
  return <div dangerouslySetInnerHTML={{ __html: cleanHtml }} />;
}

注意DOMPurify 允许你配置白名单,确保只有安全的标签和属性可通过。

结构化数据渲染

最安全、最符合 React 理念的方式,就是避免处理 HTML 字符串,转而使用结构化的 JSON 数据驱动渲染。例如,富文本可以表示为 blocks(段落、标题、列表等),由 React 组件负责渲染。

假设后端返回一个类似 Notion 或 Quill 的数据结构:

{
  "blocks": [
    { "type": "heading", "level": 2, "text": "标题" },
    { "type": "paragraph", "children": [
      { "text": "普通文字 " },
      { "text": "加粗文字", "bold": true }
    ]}
  ]
}

自定义渲染器:

function RichTextRenderer({ blocks }) {
  return blocks.map((block, i) => {
    if (block.type === 'heading') {
      const HeadingTag = `h${block.level}`;
      return <HeadingTag key={i}>{block.text}</HeadingTag>;
    }
    if (block.type === 'paragraph') {
      return (
        <p key={i}>
          {block.children.map((span, j) => (
            <span key={j} style={{ fontWeight: span.bold ? 'bold' : 'normal' }}>
              {span.text}
            </span>
          ))}
        </p>
      );
    }
    // ...更多类型
    return null;
  });
}

这种方式100%避免了 XSS,因为文本只会出现在 {} 中,React 会转义。同时,它也让你对 UI 拥有完全的可定制性。

最小安全的 HTML 净化方案

如果确实需要渲染一小段受信任的 HTML(例如来自你自己的 CMS),可以用 DOMPurify 作为最后一道防线,再结合 dangerouslySetInnerHTML

import DOMPurify from 'dompurify';

function SafeHtml({ htmlString }) {
  const clean = DOMPurify.sanitize(htmlString, {
    ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'a', 'p', 'br'],
    ALLOWED_ATTR: ['href', 'target', 'rel']
  });
  return <div dangerouslySetInnerHTML={{ __html: clean }} />;
}

注意事项:

  • 永远不要直接将用户输入作为 htmlString 传入而不净化。
  • 尽量限制白名单,不要允许 <script><iframe><style> 等危险标签。
  • DOMPurify 需要在浏览器环境中使用,在服务端渲染时要小心(可借助 jsdom)。

对比总结

方案 安全性 灵活性 适用场景
{文本} ★★★★★ ★☆☆ 纯文本展示
react-markdown ★★★★★ ★★★★ Markdown 内容
marked + DOMPurify ★★★★☆ ★★★ 需要将 Markdown 转为 HTML 再用
结构化数据渲染 ★★★★★ ★★★★★ 富文本编辑器输出规范数据
DOMPurify + dangerouslySetInnerHTML ★★★★☆ ★★ 必须渲染受控 HTML 片段

常见错误与防范

  1. 错误地使用 dangerouslySetInnerHTML 直接塞用户输入

    // 危险!
    <div dangerouslySetInnerHTML={{ __html: userComment }} />
    

    只要输入中包含 <img onerror="alert(1)"> 就可能执行脚本。

  2. 认为 innerHTMLdangerouslySetInnerHTML 更安全
    在 React 中直接操作 DOM 的 innerHTML 也会导致同样的 XSS 漏洞,而且更不符合 React 理念。

  3. 误用 react-html-parser 等停止维护的库
    一些旧的库可能不再维护,存在安全漏洞,避免使用。

最终建议

  • 如果可能,将内容转化为结构化数据,用纯 React 组件渲染,这是最安全、最灵活的方式。
  • 如果不得不处理 HTML 字符串,始终使用 DOMPurify 做严格净化,并限制允许的标签和属性。
  • 对于 Markdown,推荐使用 react-markdown,简洁且安全。
  • 定期审查你的依赖,确保没有已知的安全漏洞。

通过上述方案,你完全可以在 React 应用中告别 dangerouslySetInnerHTML 带来的安全焦虑,同时保持丰富的富文本呈现能力。