React 中 dangerouslySetInnerHTML 的替代方案
为什么需要替代 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 片段 |
常见错误与防范
-
错误地使用
dangerouslySetInnerHTML直接塞用户输入// 危险! <div dangerouslySetInnerHTML={{ __html: userComment }} />只要输入中包含
<img onerror="alert(1)">就可能执行脚本。 -
认为
innerHTML比dangerouslySetInnerHTML更安全
在 React 中直接操作 DOM 的innerHTML也会导致同样的 XSS 漏洞,而且更不符合 React 理念。 -
误用
react-html-parser等停止维护的库
一些旧的库可能不再维护,存在安全漏洞,避免使用。
最终建议
- 如果可能,将内容转化为结构化数据,用纯 React 组件渲染,这是最安全、最灵活的方式。
- 如果不得不处理 HTML 字符串,始终使用
DOMPurify做严格净化,并限制允许的标签和属性。 - 对于 Markdown,推荐使用
react-markdown,简洁且安全。 - 定期审查你的依赖,确保没有已知的安全漏洞。
通过上述方案,你完全可以在 React 应用中告别 dangerouslySetInnerHTML 带来的安全焦虑,同时保持丰富的富文本呈现能力。