React+Antd Pro实现AI对话:优雅接收后端流式输出
在React函数组件中,定义对话列表、输入内容、加载状态等核心状态,同时用useRef// 类型声明(可选)// 对话列表:符合Pro Chat组件的消息格式要求{ content: '你好!我是AI助手,请问有什么可以帮你?]);// 用户输入内容// AI回复加载状态(控制输入框禁用、加载动画)// 保存AbortController实例,用于取消未完成的流式请求// 清空对话逻辑// 取
React+Antd Pro实现AI对话:优雅接收后端流式输出
在AI对话类产品开发中,“打字机效果”(后端逐段返回AI回复)是提升用户体验的关键特性。传统接口一次性返回完整数据的模式,会让用户面临长时间空白等待,而流式输出能让后端生成一段内容就返回一段,前端实时渲染,模拟“边想边说”的自然交互,大幅降低等待感知。本文结合React+Antd Pro技术栈,从原理、实现、优化到避坑,完整拆解后端流式输出的接收与AI对话模块的落地方案,适配企业级项目开发需求。
一、场景背景与核心技术解析
1. 流式输出的核心价值
AI大模型生成回复时,需经历语义理解、内容推理、逐句生成等过程,完整回复耗时可能达数秒。流式输出通过“分块返回、实时渲染”,将等待过程转化为可见的内容生成过程,既缓解用户焦虑,又让交互更贴近真人对话场景。此外,流式输出还支持“中途打断”功能,用户可在AI未生成完回复时停止请求,提升操作灵活性。
2. 前端流式接收方案选型
前端接收后端流式数据主要有三种技术方案,需结合AI对话场景的单向数据流特性选型,具体对比如下:
| 技术方案 | 核心特性 | 兼容性 | 适配场景 |
|---|---|---|---|
| Fetch API + ReadableStream | 原生支持、轻量无依赖、单向流、可通过AbortController取消请求 | 现代浏览器全支持,IE不兼容 | AI对话、日志实时输出(仅后端推数据场景) |
| Server-Sent Events (SSE) | 基于HTTP、自动重连、单向流、自带事件监听机制 | 主流浏览器支持,IE不兼容 | 实时通知、股票行情推送(长连接低频率更新场景) |
| WebSocket | TCP长连接、双向通信、低延迟 | 全浏览器支持(含IE10+) | 实时聊天、多人协作(需前后端双向交互场景) |
本文选用Fetch API + ReadableStream方案,核心原因的是AI对话仅需后端推流,无需前端向后端实时发送数据,原生方案无需额外依赖,更贴合Antd Pro轻量开发理念,且可通过AbortController灵活控制请求生命周期。
3. 后端接口规范约定
前端流式接收的前提是后端返回符合规范的数据,需提前与后端约定以下内容,避免解析异常:
-
响应头配置:必须设置
Content-Type: text/event-stream; charset=utf-8(标识为流式数据)和Transfer-Encoding: chunked(分块传输),同时需关闭缓存Cache-Control: no-cache。 -
数据格式约定:每行返回JSON格式数据,以
\n\n作为分块分隔符(后端需确保分隔符唯一,避免与内容冲突),示例如下:{"content":"你好,","finished":false}\n\n {"content":"我是AI助手,请问有什么可以帮你?","finished":true}\n\n -
状态标识字段:通过
finished: boolean标记流是否结束,便于前端处理加载状态重置、内容收尾等逻辑;可选添加error: string字段传递异常信息。
二、前端完整实现(React+Antd Pro)
本章节基于Antd Pro 5.x版本,结合官方@ant-design/pro-chat组件实现对话UI(内置气泡样式、加载动画,无需重复造轮子),全程使用TypeScript保证类型安全。
1. 环境准备与依赖安装
首先确保项目已集成Antd Pro核心依赖,再安装Pro Chat组件:
# 安装Pro Chat组件(适配Antd Pro的对话UI组件)
npm install @ant-design/pro-chat --save
# 若需处理节流/防抖,安装lodash(可选)
npm install lodash --save
2. 核心逻辑实现(分步骤拆解)
步骤1:定义状态与Ref变量
在React函数组件中,定义对话列表、输入内容、加载状态等核心状态,同时用useRef保存AbortController实例(用于取消流式请求),避免组件重渲染导致实例丢失:
import React, { useState, useRef, useCallback, useEffect, useMemo } from 'react';
import { Chat, ChatMessageItemProps } from '@ant-design/pro-chat';
import { Button, Input, message } from 'antd';
import type { AbortController } from 'node-abort-controller'; // 类型声明(可选)
const AIChatModule: React.FC = () => {
// 对话列表:符合Pro Chat组件的消息格式要求
const [messages, setMessages] = useState<ChatMessageItemProps[]>([
{ content: '你好!我是AI助手,请问有什么可以帮你?', role: 'assistant', key: 'init-1' },
]);
// 用户输入内容
const [inputValue, setInputValue] = useState('');
// AI回复加载状态(控制输入框禁用、加载动画)
const [isLoading, setIsLoading] = useState(false);
// 保存AbortController实例,用于取消未完成的流式请求
const abortControllerRef = useRef<AbortController | null>(null);
// 清空对话逻辑
const handleClearChat = useCallback(() => {
setMessages([]);
setInputValue('');
// 取消未结束的流式请求,避免内存泄漏
if (abortControllerRef.current) {
abortControllerRef.current.abort();
abortControllerRef.current = null;
}
setIsLoading(false);
}, []);
// 组件卸载时取消请求,清理资源
useEffect(() => {
return () => {
if (abortControllerRef.current) {
abortControllerRef.current.abort();
}
};
}, []);
// 缓存对话列表,减少不必要的重渲染
const memoizedMessages = useMemo(() => messages, [messages]);
步骤2:封装流式请求函数
核心实现sendMessage函数,负责发起流式请求、逐段解析数据、实时更新对话内容,同时处理异常、取消、流结束等边界场景:
const sendMessage = useCallback(async () => {
const content = inputValue.trim();
if (!content || isLoading) return;
// 1. 新增用户消息到对话列表,重置输入框
const userMessage: ChatMessageItemProps = {
content,
role: 'user',
key: Date.now().toString(), // 用时间戳保证key唯一
};
setMessages(prev => [...prev, userMessage]);
setInputValue('');
setIsLoading(true);
// 2. 创建AbortController,关联请求生命周期
const abortController = new AbortController();
abortControllerRef.current = abortController;
// 3. 初始化AI回复(空内容,用于实时更新)
const assistantKey = (Date.now() + 1).toString();
let assistantContent = '';
setMessages(prev => [
...prev,
{ content: '', role: 'assistant', key: assistantKey, loading: true },
]);
try {
// 4. 发起流式Fetch请求
const response = await fetch('/api/ai/chat', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
// 若项目有权限校验,添加Token头
'Authorization': `Bearer ${localStorage.getItem('token')}`,
},
body: JSON.stringify({
message: content,
// 携带历史对话,实现多轮上下文关联
history: messages.map(item => ({
role: item.role,
content: item.content,
})),
}),
signal: abortController.signal, // 绑定取消信号
});
// 处理HTTP错误(非2xx状态码)
if (!response.ok) {
throw new Error(`请求失败:${response.status} ${response.statusText}`);
}
// 5. 读取流式响应,逐段解析
const reader = response.body?.getReader();
if (!reader) {
throw new Error('后端未返回流式数据,无法解析');
}
const decoder = new TextDecoder('utf-8'); // 解决中文乱码问题
// 循环读取流数据,直到流结束
while (true) {
const { done, value } = await reader.read();
if (done) {
// 流结束,更新AI消息状态(移除loading)
setMessages(prev =>
prev.map(item =>
item.key === assistantKey ? { ...item, loading: false } : item
)
);
break;
}
// 解析分块数据(按约定的\n\n分隔,过滤空行)
const chunk = decoder.decode(value, { stream: true });
const validLines = chunk.split('\n\n').filter(line => line.trim() && line.startsWith('{'));
for (const line of validLines) {
try {
const data = JSON.parse(line);
// 累加AI回复内容,实时更新UI
assistantContent += data.content;
setMessages(prev =>
prev.map(item =>
item.key === assistantKey
? { ...item, content: assistantContent, loading: !data.finished }
: item
)
);
// 若流结束,提前退出循环
if (data.finished) {
reader.cancel(); // 主动关闭读取器
break;
}
} catch (parseError) {
console.error('解析流式数据失败:', parseError, '原始数据:', line);
}
}
}
} catch (error) {
// 排除用户主动取消请求的异常
if ((error as Error).name !== 'AbortError') {
message.error(`对话失败:${(error as Error).message}`);
// 更新AI消息为错误提示
setMessages(prev =>
prev.map(item =>
item.key === assistantKey
? { ...item, content: '抱歉,对话服务暂时不可用,请稍后重试。', loading: false }
: item
)
);
}
// 重置状态
setIsLoading(false);
abortControllerRef.current = null;
}
}, [inputValue, isLoading, messages, memoizedMessages]);
步骤3:渲染对话UI(集成Antd Pro组件)
结合Pro Chat组件快速实现对话界面,包含对话列表、输入框、发送按钮、清空按钮,同时支持回车发送功能:
// 处理输入框回车发送
const handleKeyDown = useCallback((e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === 'Enter' && !isLoading) {
sendMessage();
}
}, [isLoading, sendMessage]);
return (
<div style={: '100%', height: '100vh', padding: '20px', boxSizing: 'border-box' }}>
{/* 顶部操作栏 */}
<div style={', display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<h2 style={AI对话助手<Button type="default" onClick={handleClearChat} disabled={isLoading}>
清空对话
</Button>
{/* 对话列表区域 */}<Chat
messages={memoizedMessages}
style={{ height: 'calc(100% - 100px)', marginBottom: '16px' }}
// 自定义AI消息样式(可选)
renderAssistantMessage={(message) => (
<div style={<div style={0%', background: '#1890ff', display: 'flex', alignItems: 'center', justifyContent: 'center', color: 'white' }}>
AI
<div style={ '8px' }}>
{message.content}
)}
/>
{/* 输入框区域 */}
<div style={', gap: '8px' }}>
<Input
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
onKeyDown={handleKeyDown}
placeholder="请输入你的问题..."
disabled={isLoading}
style={{ flex: 1 }}
/>
<Button
type="primary"
onClick={sendMessage}
disabled={isLoading || !inputValue.trim()}
size="large"
>
发送
</Button>
);
};
export default AIChatModule;
步骤4:路由配置(Antd Pro规范)
将AI对话组件配置到Antd Pro的路由中(config/routes.ts),使其成为可访问页面:
import type { RouteObject } from '@umijs/max';
const routes: RouteObject[] = [
{
path: '/',
redirect: '/ai-chat',
},
{
path: '/ai-chat',
name: 'AI对话',
component: './AIChatModule', // 对应组件文件路径
icon: 'message', // 可选:配置菜单图标
access: 'canReadAI', // 可选:权限控制(需配合access.ts)
},
];
export default routes;
三、体验优化与避坑指南
1. 核心体验优化点
(1)自然打字机效果(控制渲染速度)
默认逐块渲染可能导致内容弹出过快,模拟真人打字速度可提升交互体验,实现字符级延迟渲染:
// 封装字符级渲染函数
const renderTextByChar = useCallback((targetKey: string, newContent: string) => {
let currentIndex = 0;
const totalLength = newContent.length;
// 清除之前的定时器(避免多次请求叠加)
const timer = window.setInterval(() => {
setMessages(prev =>
prev.map(item => {
if (item.key === targetKey) {
const currentText = item.content || '';
const nextText = newContent.slice(0, currentIndex + 1);
return { ...item, content: nextText };
}
return item;
})
);
currentIndex++;
if (currentIndex >= totalLength) {
clearInterval(timer);
}
}, 30); // 30ms/字符,可根据需求调整速度
}, []);
// 在流式解析中替换原直接赋值逻辑
// assistantContent += data.content;
// setMessages(...)
renderTextByChar(assistantKey, assistantContent + data.content);
(2)对话历史持久化
通过localStorage保存对话历史,刷新页面后自动恢复,提升用户体验:
// 组件挂载时加载历史对话
useEffect(() => {
const savedHistory = localStorage.getItem('ai_chat_history');
if (savedHistory) {
try {
const parsedHistory = JSON.parse(savedHistory) as ChatMessageItemProps[];
setMessages(parsedHistory);
} catch (e) {
console.error('解析对话历史失败:', e);
localStorage.removeItem('ai_chat_history');
}
}
}, []);
// 对话更新时保存到本地存储
useEffect(() => {
if (memoizedMessages.length > 0) {
localStorage.setItem('ai_chat_history', JSON.stringify(memoizedMessages));
}
}, [memoizedMessages]);
(3)请求中断与异常兜底
除组件卸载时取消请求外,可增加“打断AI”按钮,允许用户中途停止回复生成:
// 新增打断AI按钮逻辑
const handleInterruptAI = useCallback(() => {
if (abortControllerRef.current && isLoading) {
abortControllerRef.current.abort();
abortControllerRef.current = null;
setIsLoading(false);
// 更新AI消息状态,标记为已中断
setMessages(prev =>
prev.map(item =>
item.role === 'assistant' && item.loading
? { ...item, content: item.content + '(已中断)', loading: false }
: item
)
);
}
}, [isLoading]);
// 在UI中添加按钮(建议放在对话列表顶部)
<Button type="text" onClick={handleInterruptAI} disabled={!isLoading}>
打断AI
</Button>
(4)性能优化:减少重渲染
频繁更新对话列表会导致组件重复渲染,可通过以下方式优化:
用
useMemo缓存对话列表(前文已实现memoizedMessages);将AI消息更新逻辑拆分为独立函数,避免闭包导致的依赖项冗余;
对长文本回复做分段渲染,避免单次更新大量DOM。
2. 常见避坑点与解决方案
(1)跨域请求异常
流式请求的跨域配置比普通请求更严格,后端需额外暴露流式相关响应头,否则前端无法读取response.body。以下是不同后端的配置示例:
-
Spring Boot(Java):
@Configuration public class CorsConfig implements WebMvcConfigurer { @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/api/**") .allowedOrigins("前端域名,如http://localhost:8000") .allowedMethods("GET", "POST", "OPTIONS") .allowedHeaders("*") .exposedHeaders("Transfer-Encoding", "Content-Type") // 暴露流式响应头 .allowCredentials(true) .maxAge(3600); } } -
Node.js(Express):
const cors = require('cors'); app.use(cors({ origin: 'http://localhost:8000', exposedHeaders: ['Transfer-Encoding', 'Content-Type'], // 关键配置 credentials: true }));
(2)中文乱码问题
若后端返回的流式数据存在中文乱码,需确保两点:一是后端设置charset=utf-8,二是前端用TextDecoder('utf-8')解析(前文已实现),避免使用默认编码。
(3)流数据解析不完整
后端返回的分块数据若未严格按\n\n分隔,会导致前端解析出空行或畸形JSON。解决方案:
-
后端确保每个JSON对象末尾添加
\n\n,避免内容中包含该分隔符; -
前端解析时增加过滤逻辑,仅保留以
{开头、}结尾的有效行(前文validLines逻辑已处理)。
(4)组件卸载后内存泄漏
若用户在AI回复过程中切换页面,未取消的流式请求会继续占用资源,导致内存泄漏。前文通过useEffect监听组件卸载,自动取消请求,彻底解决该问题。
四、扩展场景与总结
1. 扩展场景落地
(1)多轮对话上下文优化
若需支持多轮对话(AI记忆历史内容),需在请求体中携带完整对话历史,后端结合历史记录生成回复。前文history参数已实现该逻辑,仅需后端配合处理即可。
(2)切换为WebSocket方案
若需实现“用户实时打断AI+AI实时推送通知”的双向交互场景,可切换为WebSocket,核心逻辑替换如下:
// 初始化WebSocket连接
const initWebSocket = useCallback(() => {
const ws = new WebSocket(`ws://${window.location.host}/ws/ai-chat`);
ws.onopen = () => console.log('WebSocket连接成功');
ws.onmessage = (event) => {
const data = JSON.parse(event.data);
// 复用前文的消息更新逻辑
assistantContent += data.content;
setMessages(prev => prev.map(...));
};
ws.onclose = () => console.log('WebSocket连接关闭');
ws.onerror = (error) => message.error(`WebSocket错误:${error.message}`);
return ws;
}, []);
// 发送消息
const sendWsMessage = useCallback((content: string) => {
const ws = initWebSocket();
ws.send(JSON.stringify({ message: content, history: messages }));
}, [messages]);
(3)语音交互集成
结合Web Speech API实现语音输入/输出:
-
语音输入:将语音转为文字后调用流式请求;
-
语音输出:AI回复内容生成后,通过
SpeechSynthesis播放语音。
2. 总结
本文基于React+Antd Pro技术栈,实现了AI对话模块中后端流式输出的接收与渲染,核心要点如下:
-
选型逻辑:根据AI对话单向流特性,选用Fetch+ReadableStream方案,兼顾轻量性与可控性;
-
核心实现:通过AbortController管理请求生命周期,逐段解析流式数据,结合Pro Chat组件快速落地UI;
-
体验优化:从打字机效果、历史持久化、性能优化多维度提升交互体验;
-
避坑核心:严格遵守前后端接口规范,处理跨域、解析异常、内存泄漏等边界问题。
该方案可直接复用至企业级AI对话项目,同时提供了扩展场景的落地思路,适配不同需求的迭代优化。
(注:文档部分内容可能由 AI 生成)
更多推荐



所有评论(0)