React大模型网站-流式推送markdown转换问题以及开启 rehype-raw,rehype-sanitize,remark-gfm等插件的使用

在React大模型网站中实现流式推送Markdown转换开启rehype-raw等支持HTML注入是一个常见且重要的需求。

  1. 实时渲染:大模型响应是流式的,需要边接收边渲染

  2. 完整支持:既要渲染标准Markdown,又要支持HTML内容

  3. 安全性:HTML注入需要可控,防止XSS攻击

一、ReactMarkdown 是什么

ReactMarkdown 是一个把 Markdown 转成 React 组件的库。

但它和 markedmarkdown-it 最大的区别是:

  1. 默认 不渲染 HTML
  2. 默认 不能把 HTML 字符串插入进去
  3. 默认 不能输出 dangerouslySetInnerHTML

它是 严格安全的 Markdown → React 转换工具

基础使用:

javascript 复制代码
import React from 'react'
import {createRoot} from 'react-dom/client'
import Markdown from 'react-markdown'

const markdown = '# Hi, *Pluto*!'

createRoot(document.body).render(<Markdown>{markdown}</Markdown>)

二、插件rehype-raw

rehype-raw处理 Markdown 中的原生 HTML 的插件库 ,专门用于 ReactMarkdown 生态。rehype-raw 是一个 rehype 插件 (rehype 是 HTML 的处理器),作用是让 ReactMarkdown 能够安全地解析并渲染 Markdown 内容中的 HTML 标签

默认情况下:<ReactMarkdown>{content}</ReactMarkdown> 不会解析 HTML 标签 (例如 <span><div>sup> 等都会被直接当作文本输出)。

javascript 复制代码
<ReactMarkdown rehypePlugins={[rehypeRaw]}>
  {content}
</ReactMarkdown>

加上 rehype-raw,HTML 标签就会被当作 HTML 并正常渲染

示例:

javascript 复制代码
const aa = {
  content:
    '<span class="ref" data-id="doc1">[1]</span>识别到当前用户诉求...'
};

import ReactMarkdown from 'react-markdown';
import rehypeRaw from 'rehype-raw';

<ReactMarkdown rehypePlugins={[rehypeRaw]}>
  {aa.content}
</ReactMarkdown>

这样 <span class="ref"> 就能被正常渲染

还可以对其进行定制化操作例如:

  • [1] 悬浮显示 segmentContent

  • 点击跳转参考文档

  • 自动编号

  • 多文档引用自动合并

javascript 复制代码
import ReactMarkdown from 'react-markdown';
import rehypeRaw from 'rehype-raw';
import { Tooltip } from 'antd';

<ReactMarkdown
  rehypePlugins={[rehypeRaw]}
  components={{
    span({node, ...props}) {
      if (props.className === 'ref') {
        const id = props['data-id'];
        return (
          <Tooltip title={`文档来源:${id}`}>
            <span style={{ color: 'blue', cursor: 'pointer' }}>
              {props.children}
            </span>
          </Tooltip>
        );
      }
      return <span {...props} />;
    }
  }}
>
  {aa.content}
</ReactMarkdown>

三、rehype-sanitize

rehype-sanitize 是一个用于 sanitize(净化)HTML 内容的 rehype 插件,专门用于防止 XSS 攻击。在 React 大模型网站中,当开启 rehype-raw 支持 HTML 注入时,必须 配合 rehype-sanitize 使用。

安全风险场景:

javascript 复制代码
// 大模型可能返回的危险内容
const dangerousContent = `
# 看起来正常的回复

<div onclick="alert('XSS')">点击我</div>
<script>stealCookies();</script>
<img src="x" onerror="maliciousCode()">
<iframe src="javascript:alert('attack')"></iframe>
<a href="javascript:stealData()">安全链接</a>
`;

// 如果没有 rehype-sanitize,这些代码会被执行!

使用方法:

javascript 复制代码
import ReactMarkdown from 'react-markdown';
import remarkGfm from 'remark-gfm';
import rehypeRaw from 'rehype-raw';
import rehypeSanitize from 'rehype-sanitize';

const SafeMarkdownRenderer = ({ content }) => {
  return (
    <ReactMarkdown
      remarkPlugins={[remarkGfm]}
      rehypePlugins={[
        rehypeRaw,        // 允许解析 HTML
        rehypeSanitize,   // 净化 HTML,防止 XSS
      ]}
    >
      {content}
    </ReactMarkdown>
  );
};

2. 默认的安全规则

rehype-sanitize 默认使用 GitHub 的 sanitization 规则:

  • ✅ 允许:大多数安全标签(div, span, p, br, strong, em 等)

  • ✅ 允许:安全属性(class, id, style, href, src 等)

  • ❌ 禁止:<script>, <iframe>, <object>, <embed>

  • ❌ 禁止:事件处理器(onclick, onerror, onload 等)

  • ❌ 禁止:JavaScript URL(javascript:, data: 等)

自定义允许的标签和属性:

javascript 复制代码
import ReactMarkdown from 'react-markdown';
import rehypeSanitize from 'rehype-sanitize';

const CustomSanitizeRenderer = () => {
  // 自定义 sanitize 配置
  const sanitizeOptions = {
    tagNames: [
      // 基础标签
      'h1', 'h2', 'h3', 'h4', 'h5', 'h6',
      'p', 'br', 'hr',
      'div', 'span',
      
      // 列表
      'ul', 'ol', 'li',
      
      // 强调
      'strong', 'em', 'b', 'i', 'u',
      'del', 'ins', 's', 'strike',
      
      // 链接和图片
      'a', 'img',
      
      // 代码
      'code', 'pre', 'blockquote',
      
      // 表格(如果需要)
      'table', 'thead', 'tbody', 'tr', 'th', 'td',
      
      // 自定义标签(大模型可能使用的)
      'details', 'summary', 'kbd', 'sup', 'sub',
    ],
    attributes: {
      // 全局允许的属性
      '*': ['className', 'style', 'title', 'id'],
      
      // 链接允许的属性
      'a': ['href', 'target', 'rel', 'download'],
      
      // 图片允许的属性
      'img': ['src', 'alt', 'width', 'height', 'loading'],
      
      // 代码块允许的属性
      'code': ['className'], // 用于语法高亮
      
      // 自定义属性
      'span': ['data-*'], // 允许所有 data-* 属性
      'div': ['data-*'],
    },
    protocols: {
      // 允许的协议
      href: ['http', 'https', 'mailto', 'tel'],
      src: ['http', 'https', 'data'], // 谨慎使用 data:
    },
    strip: ['script', 'style'], // 完全移除这些标签及其内容
  };
  
  return (
    <ReactMarkdown
      rehypePlugins={[
        rehypeRaw,
        [rehypeSanitize, sanitizeOptions] // 传入配置
      ]}
    >
      {content}
    </ReactMarkdown>
  );
};

四、remark-gfm

remark-gfm让 ReactMarkdown 支持 GitHub 风格 Markdown (GFM) 的官方插件。让 ReactMarkdown 支持 "更多 Markdown 语法。

包括:

功能 是否需要 remark-gfm
- 无序列表 不需要
1. 有序列表 不需要
粗体 / 斜体 不需要
表格(| col1 | col2 |) ✔️ 需要
任务列表(- [x] 已完成 ✔️ 需要
自动链接(直接粘贴 URL 自动变成 <a> ✔️ 需要
删除线(~~删除~~ ✔️ 需要

使用方法:

javascript 复制代码
import ReactMarkdown from 'react-markdown';
import remarkGfm from 'remark-gfm';
import rehypeRaw from 'rehype-raw';

const CompleteMarkdownRenderer = () => {
  const markdownContent = `
# GFM 完整示例

## 1. 表格示例
| 特性 | 说明 | 状态 |
|------|------|------|
| 表格 | 支持完整的表格语法 | ✅ |
| 删除线 | ~~过时内容标记~~ | ✅ |
| 任务列表 | 项目管理功能 | ✅ |

## 2. 自动链接
- 网站: https://github.com
- 邮箱: contact@example.com
- 普通文本不会自动链接

## 3. 任务列表
### 项目进度
- [x] 需求分析
- [x] 系统设计
- [ ] 编码实现
- [ ] 测试验证
- [ ] 部署上线

## 4. 删除线应用
原价:~~¥199~~ 现价:¥99

## 5. 表格中的复杂内容
| 项目 | 描述 | 包含 |
|------|------|------|
| 基础 | 核心功能 | **加粗**、*斜体*、\`代码\` |
| 扩展 | 高级功能 | ~~删除线~~、[链接](url) |
`;

  return (
    <div className="markdown-container">
      <ReactMarkdown
        remarkPlugins={[remarkGfm]}
        rehypePlugins={[rehypeRaw]}
        components={{
          // 自定义表格渲染
          table: ({ children }) => (
            <div className="table-wrapper">
              <table className="gfm-table">{children}</table>
            </div>
          ),
          // 自定义任务列表项
          input: ({ checked, node }) => {
            const type = node?.properties?.type;
            if (type === 'checkbox') {
              return (
                <input 
                  type="checkbox" 
                  checked={checked || false}
                  readOnly 
                  className="task-checkbox"
                />
              );
            }
            return <input {...node.properties} />;
          },
          // 处理删除线
          del: ({ children }) => (
            <span className="strikethrough-text">{children}</span>
          )
        }}
      >
        {markdownContent}
      </ReactMarkdown>
    </div>
  );
};

五、总结

基础不需要定制化的话直接引入即可,支持大部分基础场景:

javascript 复制代码
<ReactMarkdown
  remarkPlugins={[remarkGfm]}
  rehypePlugins={[rehypeRaw, rehypeSanitize]}>
  {content}
</ReactMarkdown>
  1. 完整可用的 React Markdown 渲染组件
  2. 支持流式渲染、GFM、HTML、表格、任务列表
  3. 防止 XSS 攻击

完整的 React SSE 推流 + Markdown 累积渲染模板:

javascript 复制代码
import React, { useEffect, useState } from 'react';
import ReactMarkdown from 'react-markdown';
import remarkGfm from 'remark-gfm';
import rehypeRaw from 'rehype-raw';
import _ from 'lodash';
import dayjs from 'dayjs';

interface ChatItem {
  id: string;
  role: 'user' | 'assistant';
  content: string;
  createdAt: number;
  updatedAt: number;
}

interface ChatProps {
  apiUrl: string;
  userInput: string;
}

export default function ChatAI({ apiUrl, userInput }: ChatProps) {
  const [chatList, setChatList] = useState<ChatItem[]>([]);
  const [generating, setGenerating] = useState(false);

  const startGenerating = (input: string) => {
    if (!input) return;

    setGenerating(true);

    const data = { question: input };
    const queryString = `data=${encodeURIComponent(JSON.stringify(data))}`;
    const eventSource = new EventSource(`${apiUrl}?${queryString}`);

    const conversationId = Date.now().toString();
    let accumulatedContent = '';

    // 创建占位消息
    setChatList((old) => [
      ...old,
      {
        id: conversationId,
        role: 'assistant',
        content: '',
        createdAt: dayjs().valueOf(),
        updatedAt: dayjs().valueOf(),
      },
    ]);

    eventSource.onmessage = (event) => {
      const parsed = JSON.parse(event.data);
      accumulatedContent += parsed.content || '';

      // 更新 chatList,触发 React 渲染
      setChatList((old) => {
        const next = _.cloneDeep(old);
        const idx = next.findIndex((i) => i.id === conversationId);
        if (idx !== -1) {
          next[idx].content = accumulatedContent;
          next[idx].updatedAt = dayjs().valueOf();
        }
        return next;
      });

      if (parsed.type === 'End') {
        eventSource.close();
        setGenerating(false);
      }
    };

    eventSource.onerror = () => {
      eventSource.close();
      setGenerating(false);
    };
  };

  useEffect(() => {
    if (userInput) startGenerating(userInput);
  }, [userInput]);

  return (
    <div>
      <h3>AI 对话助手</h3>

      <div
        style={{
          border: '1px solid #ccc',
          padding: 10,
          height: 400,
          overflowY: 'auto',
          display: 'flex',
          flexDirection: 'column',
          gap: 15,
        }}
      >
        {chatList.map((item) => (
          <div key={item.id} style={{ whiteSpace: 'pre-wrap' }}>
            <ReactMarkdown
              remarkPlugins={[remarkGfm]}
              rehypePlugins={[rehypeRaw]}
            >
              {item.content}
            </ReactMarkdown>
          </div>
        ))}
      </div>

      {generating && <div>生成中...</div>}
    </div>
  );
}

安装依赖:

bash 复制代码
npm install react-markdown remark-gfm rehype-raw
相关推荐
crary,记忆1 小时前
如何理解 React的UI渲染
前端·react.js·ui·前端框架
盛码笔记1 小时前
部署Django+React项目到服务器
服务器·react.js·django
我太想进步了C~~10 小时前
chatGPT的conversation management模块的加强
chatgpt
我要改名叫嘟嘟17 小时前
各个模型rate limit汇总
chatgpt
景联文科技18 小时前
景联文AI观察动态速递 第3期
人工智能·chatgpt
燃烧的土豆18 小时前
100¥ 实现的React项目 Keep-Alive 缓存控件
前端·react.js·ai编程
爱吃无爪鱼21 小时前
03-Bun vs Node.js:JavaScript 运行时的新旧之争
javascript·vue.js·react.js·npm·node.js
一颗烂土豆1 天前
React 大屏可视化适配方案:vfit-react 发布 🚀
前端·javascript·react.js
Li_na_na011 天前
React+dhtmlx实现甘特图
前端·react.js·甘特图