前端 + AI 进阶Day 2:流式 Markdown 渲染

前端 + AI 进阶学习路线|Week 1-2:流式体验优化

Day 2:流式 Markdown 渲染入门

学习时间:2025年12月26日(星期五)
关键词:流式渲染、Markdown、代码高亮、KaTeX、react-markdown


🎯 今日学习目标

  1. 理解 AI 响应为何常以 Markdown 格式返回
  2. 掌握安全、高效的 Markdown 渲染方案
  3. 实现支持 代码高亮 + 表格 + 数学公式 的流式渲染组件
  4. 为后续“打字机效果”打下内容解析基础

💡 为什么 AI 聊天需要 Markdown 渲染?

大型语言模型(如 GPT、Claude、Ollama)通常以 Markdown 格式 返回结构化响应,包括:

  • 代码块(需高亮)
  • 表格(对比数据)
  • 数学公式(科研/教育场景)
  • 列表、引用、加粗等排版

如果直接用 dangerouslySetInnerHTML 渲染原始字符串:

  • ❌ 存在 XSS 风险
  • ❌ 无法高亮代码
  • ❌ 公式显示为乱码

因此,我们需要一个 安全、可扩展、支持流式追加 的 Markdown 渲染器。


📚 核心技术选型

需求 推荐方案
安全解析 Markdown react-markdown(基于 AST,自动转义)
代码高亮 rehype-highlight + highlight.js
数学公式 rehype-katex(需引入 KaTeX CSS)
表格支持 react-markdown 原生支持(需启用 remarkGfm

✅ 优势:组件化、插件化、天然支持 React,避免手动操作 DOM


🔧 动手实践:构建流式 Markdown 渲染器

步骤 1:安装依赖

npx create-react-app day02-streaming-markdown
cd day02-streaming-markdown
npm install react-markdown remark-gfm rehype-highlight rehype-katex katex

💡 注意:rehype-katex 依赖 katex,且需手动引入 CSS(已在组件中处理)

步骤 2:编写流式 Markdown 组件

// src/components/StreamingMarkdown.jsx
import React from 'react';
import ReactMarkdown from 'react-markdown';
import remarkGfm from 'remark-gfm';           // 支持表格、任务列表等 GitHub Flavored Markdown
import rehypeHighlight from 'rehype-highlight'; // 代码高亮
import rehypeKatex from 'rehype-katex';       // 数学公式
import 'katex/dist/katex.min.css';            // 必须引入 KaTeX 样式

const StreamingMarkdown = ({ content }) => {
  return (
    <ReactMarkdown
      remarkPlugins={[remarkGfm]}
      rehypePlugins={[rehypeHighlight, rehypeKatex]}
      components={{
        // 自定义代码块样式
        code({ node, inline, className, children, ...props }) {
          const match = /language-(\w+)/.exec(className || '');
          return !inline && match ? (
            <pre
              className={className}
              {...props}
              style={{
                background: '#f8f8f8',
                padding: '12px',
                borderRadius: '6px',
                overflowX: 'auto',
                margin: '12px 0',
              }}
            >
              <code>{children}</code>
            </pre>
          ) : (
            <code
              className={className}
              {...props}
              style={{
                background: '#f0f0f0',
                padding: '2px 4px',
                borderRadius: '3px',
                fontSize: '0.9em',
              }}
            >
              {children}
            </code>
          );
        },
        // 表格容器支持横向滚动
        table({ children }) {
          return (
            <div style={{ overflowX: 'auto', margin: '12px 0' }}>
              <table
                style={{
                  borderCollapse: 'collapse',
                  minWidth: '100%',
                  width: 'max-content',
                }}
              >
                {children}
              </table>
            </div>
          );
        },
        th({ children }) {
          return (
            <th
              style={{
                border: '1px solid #ddd',
                padding: '8px 12px',
                backgroundColor: '#f5f5f5',
                fontWeight: '600',
              }}
            >
              {children}
            </th>
          );
        },
        td({ children }) {
          return (
            <td
              style={{
                border: '1px solid #ddd',
                padding: '8px 12px',
                verticalAlign: 'top',
              }}
            >
              {children}
            </td>
          );
        },
        // 防止公式过宽
        div: ({ children, ...props }) => <div {...props}>{children}</div>,
      }}
    >
      {content}
    </ReactMarkdown>
  );
};

export default StreamingMarkdown;

步骤 3:在 App 中实现流式追加效果

// src/App.jsx
import { useEffect, useState } from 'react';
import StreamingMarkdown from './components/StreamingMarkdown';

function App() {
  const [displayedText, setDisplayedText] = useState('');

  // 模拟 AI 返回的完整 Markdown 响应
  const fullResponse = `# 欢迎使用 AI 助手 🤖

这是一个支持 **流式渲染** 的 Markdown 示例。

## 代码高亮
\`\`\`javascript
// 打字机效果核心逻辑
useEffect(() => {
  let index = 0;
  const interval = setInterval(() => {
    if (index < fullResponse.length) {
      setDisplayedText(fullResponse.slice(0, ++index));
    } else {
      clearInterval(interval);
    }
  }, 20);
  return () => clearInterval(interval);
}, []);
\`\`\`

## 表格支持
| 特性 | 支持情况 | 说明 |
|------|---------|------|
| 代码高亮 | ✅ | 使用 highlight.js |
| 数学公式 | ✅ | 使用 KaTeX |
| 表格 | ✅ | GitHub Flavored Markdown |
| 流式输出 | ✅ | 逐字符渲染 |

## 数学公式
欧拉恒等式:$e^{i\\pi} + 1 = 0$

贝叶斯定理:
$$
P(A|B) = \\frac{P(B|A)P(A)}{P(B)}
$$

> 💡 所有内容安全渲染,无 XSS 风险!
`;

  // 模拟流式输出(每 20ms 追加一个字符)
  useEffect(() => {
    let index = 0;
    const interval = setInterval(() => {
      if (index < fullResponse.length) {
        setDisplayedText(fullResponse.slice(0, index + 1));
        index++;
      } else {
        clearInterval(interval);
      }
    }, 20);

    return () => clearInterval(interval);
  }, []);

  return (
    <div style={{ padding: '20px', fontFamily: 'Inter, -apple-system, sans-serif', maxWidth: '900px', margin: '0 auto' }}>
      <h1 style={{ textAlign: 'center', fontSize: '24px', fontWeight: '700', color: '#333' }}>
        📝 流式 Markdown 渲染
      </h1>
      <p style={{ textAlign: 'center', color: '#666', marginBottom: '20px' }}>
        代码高亮 + 表格 + 数学公式,逐字输出
      </p>

      <div
        style={{
          border: '1px solid #e8e8e8',
          borderRadius: '12px',
          padding: '24px',
          backgroundColor: '#fff',
          boxShadow: '0 2px 12px rgba(0,0,0,0.05)',
          lineHeight: 1.6,
        }}
      >
        <StreamingMarkdown content={displayedText} />
      </div>

      <div style={{ marginTop: '24px', padding: '16px', backgroundColor: '#f9f9f9', borderRadius: '12px', fontSize: '14px' }}>
        <strong>✅ 验证方法:</strong>
        <ul style={{ marginTop: '8px', paddingLeft: '20px' }}>
          <li>文字像<strong>打字机</strong>一样逐字出现</li>
          <li>JavaScript 代码块有<strong>语法高亮</strong></li>
          <li>表格对齐整齐,有边框</li>
          <li>数学公式渲染为<strong>图形</strong>(非原始 $...$ 字符)</li>
          <li>无 XSS 风险(尝试输入 &lt;script&gt;,应被转义)</li>
        </ul>
      </div>
    </div>
  );
}

export default App;

✅ 效果验证

  • 打开页面,文字像“打字机”一样逐字出现
  • 代码块有语法高亮(JavaScript 关键字着色)
  • 表格对齐整齐,带边框
  • 数学公式正确渲染(非原始 $...$ 字符)
  • 无 XSS 风险(输入 <script>alert(1)</script> 会被转义)

🤔 思考与延伸

  1. 性能问题:每追加一个字符就重新渲染整个 Markdown,是否高效?
    → 可通过 useMemo 缓存中间 AST,或使用增量解析库优化(后续探索)

  2. 动态高度:如果 Markdown 渲染后高度变化,如何通知虚拟列表更新?
    → 可结合 react-virtualmeasureElement API(Day 3 将整合)

  3. 中断与恢复:用户暂停后,如何从断点继续流式?
    → 需保存当前 index 和内容状态(Day 3 实现交互控制)

提示:为提升体验,可将字符追加节奏从“逐字符”改为“逐 token”(如按句子或单词),但需后端支持。


📅 明日预告

Day 3:打字机效果 + 流式交互控制

  • 实现 暂停 / 继续 / 回退 控制按钮
  • 将流式 Markdown 与虚拟聊天列表集成
  • 支持“新消息到达时自动滚动到底部”

✍️ 小结

今天,我们让 AI 的响应“活”了起来——不仅安全地渲染了富文本,还实现了逐字输出的流式效果。这是构建沉浸式 AI 对话体验的关键一步。通过 react-markdown + 插件生态,我们轻松支持了代码、表格、公式等复杂内容,为后续的交互控制和真实 AI 对接铺平了道路。

💬 实践提示:如果 KaTeX 公式未渲染,请检查是否遗漏 import 'katex/dist/katex.min.css'。欢迎在评论区分享你的流式渲染效果截图或遇到的问题!

posted @ 2025-12-26 11:28  XiaoZhengTou  阅读(53)  评论(0)    收藏  举报