手把手教你用React + Fetch API搞定DeepSeek流式回复(含完整代码和避坑点)

张开发
2026/4/13 8:35:13 15 分钟阅读

分享文章

手把手教你用React + Fetch API搞定DeepSeek流式回复(含完整代码和避坑点)
React Fetch API 实现 DeepSeek 流式对话全攻略在当今的 Web 开发中实现流畅的 AI 对话体验已成为许多应用的标配需求。本文将带你从零开始使用 React 和原生 Fetch API 构建一个完整的流式对话界面重点解决数据处理、状态管理和用户体验等核心问题。1. 环境准备与基础配置在开始编码前我们需要确保开发环境配置正确。创建一个新的 React 项目如使用 Create React App 或 Vite并安装必要的依赖npm install react react-dom antd ant-design/icons ahooks react-markdown remark-gfm基础项目结构建议如下/src /components AIChat.tsx # 主聊天组件 /styles index.less # 样式文件 App.tsx # 入口组件在AIChat.tsx中我们先导入必要的库并设置基础状态import { FC, useState, useEffect, useRef } from react; import { Input, Button, message } from antd; import { ArrowUpOutlined, LoadingOutlined, CopyOutlined, RedoOutlined } from ant-design/icons; import { useRequest } from ahooks; import Markdown from react-markdown; import remarkGfm from remark-gfm;2. 流式数据获取与处理流式数据处理是本项目的核心难点。我们将使用 Fetch API 的 ReadableStream 接口来逐步接收和处理数据。2.1 实现基础流式请求首先定义 API 请求函数注意这里使用的是原生 Fetch APIconst fetchStream async (question: string) { const response await fetch(/api/chat, { method: POST, headers: { Content-Type: application/json, }, body: JSON.stringify({ question }), }); if (!response.ok) throw new Error(请求失败); if (!response.body) throw new Error(无法获取响应体); return response.body.getReader(); };2.2 处理 SSE 格式数据服务端通常以 Server-Sent Events (SSE) 格式返回数据我们需要正确处理这种格式const processStream async (reader: ReadableStreamDefaultReader) { const decoder new TextDecoder(); let buffer ; while (true) { const { done, value } await reader.read(); if (done) break; buffer decoder.decode(value, { stream: true }); // 按行分割并过滤出有效数据 const lines buffer.split(\n); buffer lines.pop() || ; // 保存未完成的行 for (const line of lines) { if (line.startsWith(data:)) { try { const data JSON.parse(line.slice(5).trim()); // 处理有效数据... } catch (e) { console.error(解析JSON失败:, e); } } } } };3. 状态管理与UI交互良好的状态管理是实现流畅用户体验的关键。我们将使用 React 的 useState 和 useRef 来管理聊天状态。3.1 聊天状态设计interface Message { id: string; content: string; role: user | assistant; isStreaming?: boolean; timestamp: number; } const AIChat: FC () { const [messages, setMessages] useStateMessage[]([]); const [input, setInput] useState(); const [isLoading, setIsLoading] useState(false); const messagesEndRef useRefHTMLDivElement(null); // 自动滚动到底部 useEffect(() { messagesEndRef.current?.scrollIntoView({ behavior: smooth }); }, [messages]); // ...其他逻辑 };3.2 实现打字机效果为了让回复看起来更自然我们可以模拟打字机效果const updateStreamingMessage (newContent: string) { setMessages(prev { const last prev[prev.length - 1]; if (last?.isStreaming) { return [ ...prev.slice(0, -1), { ...last, content: last.content newContent } ]; } return prev; }); };4. 完整实现与优化技巧现在我们将所有部分组合起来实现完整的聊天组件。4.1 主聊天逻辑实现const handleSendMessage async () { if (!input.trim() || isLoading) return; // 添加用户消息 const userMessage: Message { id: Date.now().toString(), content: input, role: user, timestamp: Date.now(), }; // 添加初始AI消息空内容标记为流式 const aiMessage: Message { id: ai-${Date.now()}, content: , role: assistant, isStreaming: true, timestamp: Date.now(), }; setMessages(prev [...prev, userMessage, aiMessage]); setInput(); setIsLoading(true); try { const reader await fetchStream(userMessage.content); await processStream(reader, (data) { updateStreamingMessage(data.answer); }); } catch (error) { message.error(请求失败: error.message); } finally { setIsLoading(false); // 标记流式消息完成 setMessages(prev prev.map(msg msg.id aiMessage.id ? { ...msg, isStreaming: false } : msg )); } };4.2 输入框高级功能实现实现更智能的输入框交互const handleKeyDown (e: React.KeyboardEventHTMLTextAreaElement) { // Enter发送CtrlEnter换行 if (e.key Enter !e.ctrlKey !e.shiftKey) { e.preventDefault(); handleSendMessage(); } else if (e.key Enter (e.ctrlKey || e.shiftKey)) { // 允许换行 const { selectionStart, selectionEnd } e.currentTarget; setInput(prev prev.substring(0, selectionStart) \n prev.substring(selectionEnd) ); // 保持光标位置 setTimeout(() { e.currentTarget.selectionStart selectionStart 1; e.currentTarget.selectionEnd selectionStart 1; }, 0); } };5. 性能优化与错误处理确保应用在各种情况下都能稳定运行。5.1 流式请求中断处理const abortControllerRef useRefAbortController | null(null); const handleSendMessage async () { // 中断之前的请求 if (abortControllerRef.current) { abortControllerRef.current.abort(); } const abortController new AbortController(); abortControllerRef.current abortController; try { const response await fetch(/api/chat, { signal: abortController.signal, // ...其他配置 }); // ...处理响应 } catch (err) { if (err.name ! AbortError) { // 处理非中断错误 } } finally { if (abortControllerRef.current abortController) { abortControllerRef.current null; } } }; // 组件卸载时中断请求 useEffect(() { return () { if (abortControllerRef.current) { abortControllerRef.current.abort(); } }; }, []);5.2 内存管理优化长时间运行的聊天应用可能会积累大量消息需要合理管理内存// 限制消息历史数量 const MAX_MESSAGES 50; const addMessage (message: Message) { setMessages(prev { const newMessages [...prev, message]; if (newMessages.length MAX_MESSAGES) { return newMessages.slice(newMessages.length - MAX_MESSAGES); } return newMessages; }); };6. 样式与用户体验优化良好的UI设计能显著提升用户体验。6.1 基础聊天界面样式.chat-container { height: 100vh; display: flex; flex-direction: column; .messages { flex: 1; overflow-y: auto; padding: 16px; .message { margin-bottom: 12px; max-width: 80%; .user { align-self: flex-end; background: #1890ff; color: white; border-radius: 12px 12px 0 12px; } .assistant { align-self: flex-start; background: #f5f5f5; border-radius: 12px 12px 12px 0; } } } .input-area { padding: 12px; border-top: 1px solid #f0f0f0; textarea { resize: none; } } }6.2 加载状态指示器const LoadingIndicator () ( div classNameloading-indicator span classNamedot style{{ animationDelay: 0s }} / span classNamedot style{{ animationDelay: 0.2s }} / span classNamedot style{{ animationDelay: 0.4s }} / /div ); // 对应样式 .loading-indicator { display: flex; padding: 8px; .dot { width: 8px; height: 8px; margin: 0 2px; background-color: #999; border-radius: 50%; animation: bounce 1s infinite ease-in-out; } keyframes bounce { 0%, 100% { transform: translateY(0); } 50% { transform: translateY(-5px); } } }7. 生产环境注意事项将应用部署到生产环境前还需要考虑以下关键点7.1 错误边界处理class ErrorBoundary extends React.Component { state { hasError: false }; static getDerivedStateFromError() { return { hasError: true }; } componentDidCatch(error, info) { logErrorToService(error, info); } render() { if (this.state.hasError) { return ( div classNameerror-fallback h3聊天功能暂时不可用/h3 button onClick{() window.location.reload()}重试/button /div ); } return this.props.children; } } // 使用方式 ErrorBoundary AIChat / /ErrorBoundary7.2 性能监控与分析考虑添加性能监控代码useEffect(() { const measureInteraction () { const startTime performance.now(); return { end: () { const duration performance.now() - startTime; if (duration 1000) { trackSlowInteraction(duration); } } }; }; const interaction measureInteraction(); // 执行操作... interaction.end(); }, [/* 依赖项 */]);8. 扩展功能思路基础功能实现后可以考虑添加以下增强功能8.1 消息持久化// 使用 localStorage 保存聊天记录 const STORAGE_KEY ai_chat_history; const loadMessages () { try { const saved localStorage.getItem(STORAGE_KEY); return saved ? JSON.parse(saved) : []; } catch { return []; } }; const saveMessages (messages: Message[]) { try { localStorage.setItem(STORAGE_KEY, JSON.stringify(messages)); } catch (e) { console.error(保存消息失败:, e); } }; // 在组件中使用 useEffect(() { setMessages(loadMessages()); }, []); useEffect(() { if (messages.length 0) { saveMessages(messages); } }, [messages]);8.2 实现消息编辑与重新生成const handleRegenerate (messageId: string) { const messageIndex messages.findIndex(m m.id messageId); if (messageIndex -1) return; const previousMessages messages.slice(0, messageIndex); const messageToRegenerate messages[messageIndex]; if (messageToRegenerate.role user) { // 重新发送用户消息 setMessages(previousMessages); setInput(messageToRegenerate.content); handleSendMessage(); } else { // 找到对应的用户消息重新发送 const userMessage messages[messageIndex - 1]; if (userMessage?.role user) { setMessages(messages.slice(0, messageIndex - 1)); setInput(userMessage.content); handleSendMessage(); } } };

更多文章