前端 + AI 进阶Day 2:流式 Markdown 渲染
前端 + AI 进阶学习路线|Week 1-2:流式体验优化
Day 2:流式 Markdown 渲染入门
学习时间:2025年12月26日(星期五)
关键词:流式渲染、Markdown、代码高亮、KaTeX、react-markdown
🎯 今日学习目标
- 理解 AI 响应为何常以 Markdown 格式返回
- 掌握安全、高效的 Markdown 渲染方案
- 实现支持 代码高亮 + 表格 + 数学公式 的流式渲染组件
- 为后续“打字机效果”打下内容解析基础
💡 为什么 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 风险(尝试输入 <script>,应被转义)</li>
</ul>
</div>
</div>
);
}
export default App;
✅ 效果验证
- 打开页面,文字像“打字机”一样逐字出现
- 代码块有语法高亮(JavaScript 关键字着色)
- 表格对齐整齐,带边框
- 数学公式正确渲染(非原始
$...$字符) - 无 XSS 风险(输入
<script>alert(1)</script>会被转义)
🤔 思考与延伸
-
性能问题:每追加一个字符就重新渲染整个 Markdown,是否高效?
→ 可通过useMemo缓存中间 AST,或使用增量解析库优化(后续探索) -
动态高度:如果 Markdown 渲染后高度变化,如何通知虚拟列表更新?
→ 可结合react-virtual的measureElementAPI(Day 3 将整合) -
中断与恢复:用户暂停后,如何从断点继续流式?
→ 需保存当前 index 和内容状态(Day 3 实现交互控制)
提示:为提升体验,可将字符追加节奏从“逐字符”改为“逐 token”(如按句子或单词),但需后端支持。
📅 明日预告
Day 3:打字机效果 + 流式交互控制
- 实现 暂停 / 继续 / 回退 控制按钮
- 将流式 Markdown 与虚拟聊天列表集成
- 支持“新消息到达时自动滚动到底部”
✍️ 小结
今天,我们让 AI 的响应“活”了起来——不仅安全地渲染了富文本,还实现了逐字输出的流式效果。这是构建沉浸式 AI 对话体验的关键一步。通过 react-markdown + 插件生态,我们轻松支持了代码、表格、公式等复杂内容,为后续的交互控制和真实 AI 对接铺平了道路。
💬 实践提示:如果 KaTeX 公式未渲染,请检查是否遗漏
import 'katex/dist/katex.min.css'。欢迎在评论区分享你的流式渲染效果截图或遇到的问题!

浙公网安备 33010602011771号