Javascript中Fetch请求Coze API并流式展示请求结果
作者:GISer_Jinger
01 思路技术选型Fetch
用户现在的问题是如何将coze API返回的流式响应展示出来。之前已经帮忙封装了axios请求,现在需要处理流式数据。我需要回想一下流式请求的处理方式,通常SSE(Server-Sent Events)或者使用fetch的流式读取,但用户使用的是axios,可能需要配置responseType为stream或者处理分块数据。
首先,用户之前的代码中设置了stream: true,这可能意味着API返回的是流式数据,比如每个chunk逐步返回。但axios默认情况下可能不会处理这种流式响应,可能需要调整配置。例如,使用fetch的话,可以监听chunk,但axios是否支持呢?
根据之前的知识,处理流式响应通常可以使用EventSource,但需要服务器支持SSE。如果API返回的是普通的分块数据流,可能需要使用responseType: ‘stream’(在Node.js中)或者在前端使用ReadableStream。不过在前端浏览器环境中,axios可能无法直接处理流式数据,或者需要特定的配置。
用户现在用的是浏览器环境吗?假设是前端项目,比如Vue或React。这时候可能需要使用fetch API,因为fetch可以处理流式数据,而axios在这方面的支持可能有限。或者,检查coze的API文档,确认流式返回的数据格式,比如是否每个chunk是一个JSON对象或特定格式的数据。
例如,如果API使用SSE,每个事件是一个数据块,那么在前端应该使用EventSource来监听事件。但如果API是普通的流式HTTP响应,分块发送数据,那么可能需要使用fetch并逐步读取响应体。
在用户之前的封装中,使用的是axios.post,并且设置了stream: true。但可能没有正确配置axios来处理流式响应。因此,可能需要改用fetch,或者调整axios的配置,比如设置responseType为’stream’(但浏览器中可能不支持),或者使用onDownloadProgress回调来处理接收到的数据片段。
查阅资料发现,axios在浏览器中的responseType支持’arraybuffer’, ‘blob’, ‘document’, ‘json’, ‘text’, ‘stream’(stream仅在Node.js环境中有效)。因此,在浏览器中,可能无法直接使用stream类型。这时可能需要使用text类型,并手动处理分块数据。但这种方式可能不可靠,因为axios会等待整个响应完成后再触发then。
因此,正确的做法应该是使用fetch API,因为fetch支持流式读取响应体。例如,使用fetch并设置body为可读流,然后通过reader来逐步读取数据。
或者,如果服务器支持SSE,则使用EventSource。但需要确认coze的API是否使用SSE格式,即返回的数据格式是否符合text/event-stream,并且每个事件由开头。
假设用户需要处理的是普通的流式JSON数据,每个chunk是一个JSON对象,那么可能需要使用fetch来获取响应体,并通过读取器逐步处理数据块。
总结处理步骤:
- 使用fetch代替axios,因为fetch支持流式读取。
- 设置请求参数,包括method: ‘POST’, headers, body等。
- 读取响应体流,使用ReadableStream.getReader()。
- 循环读取每个chunk,处理并更新UI。
接下来需要编写示例代码,展示如何发起请求并处理流式响应,逐步将数据展示在页面上。同时,注意处理文本的拼接和解析,特别是如果每个chunk可能包含部分数据,需要正确拼接成完整的JSON对象或文本内容。
可能的实现方式:
- 使用fetch发起POST请求。
- 获取响应体的reader。
- 创建解码器来处理Uint8Array数据为文本。
- 循环读取chunk,直到done为true。
- 处理每个chunk的文本,可能按特定分隔符(如\n\n)分割,解析每个事件的数据。
- 如果coze的API返回的是类似OpenAI的流式响应,每个chunk是一个{…}\n\n的结构,那么需要按行分割处理。
最后,将处理后的数据逐步显示在页面上,比如通过React的state更新,或者直接操作DOM。
在代码中,需要注意错误处理,比如网络错误或流读取中的错误,以及如何中止请求(例如用户取消操作)。
现在,将这些思路整理成步骤化的解答,并提供示例代码,指导用户如何修改之前的封装以支持流式展示。
要实现从 coze API 获取流式数据并实时展示,可以使用 fetch
API 处理流式响应,逐步读取并渲染数据。以下是完整实现方案:
// 封装流式请求方法 export const streamRequestTxt = async (content: string, onDataReceived: (string) => void) => { try { const response = await fetch('https://api.coze.cn/v3/chat', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer pat_4cFLXH9NmmysAeyFMSVrZJXzCP8FywZlIqWWFFrQSS4x5DpYxXqmBXLfjzUmU242' }, body: JSON.stringify({ bot_id: "7472376009968042021", user_id: "123456", stream: true, auto_save_history: true, additional_messages: [{ role: "user", content: content, content_type: "text" }] }) }); // 处理非200响应 if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } // 获取可读流和读取器 const reader = response.body?.getReader(); const decoder = new TextDecoder('utf-8'); let partialData = ''; if (!reader) return; // 持续读取流数据 while (true) { const { done, value } = await reader.read(); if (done) break; // 解码并处理数据块 const chunk = decoder.decode(value, { stream: true }); partialData += chunk; // 处理可能的多个事件(按换行分割) const lines = partialData.split('\n'); partialData = lines.pop() || ''; // 保留未完成的行 for (const line of lines) { if (line.startsWith('')) { const jsonData = line.replace('', ''); try { const parsed = JSON.parse(jsonData); if (parsed.content) { onDataReceived(parsed.content); // 触发回调更新UI } } catch (e) { console.error('JSON解析失败:', e); } } } } } catch (error) { console.error('流式请求失败:', error); throw error; // 向上传递错误 } };
React 组件使用示例:
import { useState } from 'react'; export function ChatStream() { const [message, setMessage] = useState(''); const [isLoading, setIsLoading] = useState(false); const handleStream = async (input: string) => { setIsLoading(true); try { await streamRequestTxt(input, (chunk) => { setMessage(prev => prev + chunk); // 增量更新消息 }); } finally { setIsLoading(false); } }; return ( <div> <button onClick={() => handleStream('你好')} disabled={isLoading} > {isLoading ? '请求中...' : '发送流式请求'} </button> <div style={{ whiteSpace: 'pre-wrap' }}> {message} </div> </div> ); }
核心实现原理:
流式协议处理
- 使用
fetch
原生API获取可读流(ReadableStream) - 通过
TextDecoder
处理二进制流为文本 - 按
\n
分割数据行,处理服务端可能合并发送的多条消息
数据格式解析
- 假设服务端采用
JSON
格式发送事件 - 自动过滤非数据行(如心跳检测的空行)
- 使用
JSON.parse
严格解析数据格式
实时渲染优化
- 通过回调函数逐块传递数据
- 前端框架中通过状态合并(如React的setState)优化渲染性能
- 保留未完成数据块实现断包续传
高级功能扩展建议:
中断控制
// 创建AbortController const controller = new AbortController(); // 在fetch配置中添加 signal: controller.signal, // 暴露中断方法 return { abort: () => controller.abort() };
性能优化
// 使用防抖渲染(每秒更新不超过60次) let renderQueue = ''; const debouncedUpdate = useRef(debounce(() => { setMessage(prev => prev + renderQueue); renderQueue = ''; }, 16)).current; // 在回调中累积数据 onDataReceived: (chunk) => { renderQueue += chunk; debouncedUpdate(); }
错误重试机制
let retries = 0; const MAX_RETRIES = 3; try { // ...原有逻辑 } catch (error) { if (retries < MAX_RETRIES) { retries++; await new Promise(r => setTimeout(r, 1000 * retries)); handleStream(input); } }
不同技术栈的适配方案:
Vue3 Composition API
<script setup> import { ref } from 'vue'; const message = ref(''); const isLoading = ref(false); const handleStream = async (input) => { isLoading.value = true; try { await streamRequestTxt(input, (chunk) => { message.value += chunk; }); } finally { isLoading.value = false; } }; </script>
Node.js 服务端转发
import express from 'express'; const app = express(); app.post('/chat-proxy', async (req, res) => { const upstreamRes = await fetch('https://api.coze.cn/v3/chat', { // ...相同配置 }); // 保持流式特性 res.setHeader('Content-Type', 'text/event-stream'); upstreamRes.body.pipe(res); });
调试技巧:
- 在Chrome开发者工具中开启「保留日志」查看完整流式请求
- 使用
curl -N URL
验证原始API的流式特性 - 添加调试日志记录原始数据块:
console.log('[Raw Chunk]', chunk);
02 过程分析
用户现在想要在前端项目中实现流式接收并展示数据,特别是在对话框中使用类似setHistoryContent这样的状态更新函数。用户给出的代码片段显示他们正在使用React的状态管理,将消息添加到历史记录中。用户可能已经有一个基本的聊天界面,现在需要集成Coze API的流式响应。
首先,我需要理解用户的具体需求。他们可能已经能够发送消息到后端,但接收响应时是流式的,需要逐步显示数据,而不是等待整个响应完成。这可能涉及到使用Fetch API的流式处理功能,或者WebSocket,但根据之前的对话,用户使用的是Fetch。
接下来,用户提供的代码示例中,使用setHistoryContent来更新消息历史。当前的实现可能是在接收到完整响应后一次性添加消息,但现在需要改为每收到一个数据块就更新一次。这需要处理流式数据,逐步拼接内容,并更新对应的消息条目。
需要考虑的问题包括:
- 如何正确处理流式数据,确保每个数据块都能被及时处理。
- 如何管理React的状态,特别是当需要更新现有消息而不是添加新消息时。
- 如何处理可能的并发请求,避免状态混乱。
- 错误处理和加载状态的显示。
用户之前的例子中,每次接收到新的内容,都会添加一个新的消息对象到历史记录中。但流式响应应该是逐步更新同一个消息的内容,而不是创建多个消息。因此,可能需要调整状态结构,使得可以更新特定消息的文本内容。
例如,初始时添加一个带有临时ID的消息对象,表示机器人的响应正在加载,然后随着数据块的到来,不断更新该消息的text属性。完成后再标记为完成。
可能的实现步骤:
- 用户发送消息时,立即将用户的消息添加到历史记录中。
- 发送请求到Coze API,开始接收流式响应。
- 在接收到第一个数据块时,创建一个新的消息对象,标记为机器人,并开始填充text。
- 后续的数据块不断追加到该消息的text中。
- 流式结束时,可能标记该消息为完成,或处理可能的错误。
需要处理的技术点包括使用Fetch API的ReadableStream,正确读取数据块,并在React中高效更新状态。同时,需要确保在组件卸载时取消请求,避免内存泄漏。
另外,用户可能需要处理消息的ID管理,确保在更新时能找到正确的消息条目。例如,使用唯一ID来标识每个消息,并在流式响应开始时生成一个ID,后续更新都针对该ID。
可能的代码结构:
- 发送用户消息时,添加到历史记录。
- 发起Fetch请求,获取响应流。
- 读取流中的数据,逐步更新对应的机器人消息内容。
- 使用useState或useReducer管理消息历史状态。
- 使用AbortController来允许取消请求。
需要确保在React的严格模式下,状态更新不会导致问题,尤其是在异步处理流时。可能需要使用ref来保持最新状态的引用,避免闭包问题。
最后,测试流式响应是否正确,确保每个数据块都能实时显示,并且界面不会因为频繁的状态更新而卡顿。可能需要优化渲染性能,比如使用React.memo或适当的状态结构。
以下是针对对话框场景的流式数据接收和展示完整实现方案:
import { useState } from 'react'; interface Message { id: string; author: 'User' | 'Bot'; text: string; isStreaming?: boolean; } export default function ChatDialog() { const [history, setHistory] = useState<Message[]>([]); const [currentStreamId, setCurrentStreamId] = useState<string | null>(null); // 生成唯一ID的工具函数 const generateId = () => Date.now().toString(36) + Math.random().toString(36).substr(2); // 处理流式响应 const handleStreamResponse = async (userInput: string) => { // 添加用户消息 setHistory(prev => [ ...prev, { id: generateId(), author: 'User', text: userInput } ]); // 创建机器人消息占位符 const botMessageId = generateId(); setHistory(prev => [ ...prev, { id: botMessageId, author: 'Bot', text: '', isStreaming: true } ]); setCurrentStreamId(botMessageId); try { const response = await fetch('/api/coze-stream', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ message: userInput }) }); const reader = response.body?.getReader(); const decoder = new TextDecoder(); let done = false; if (!reader) return; while (!done) { const { value, done: streamDone } = await reader.read(); done = streamDone; if (value) { const chunk = decoder.decode(value); setHistory(prev => prev.map(msg => msg.id === botMessageId ? { ...msg, text: msg.text + chunk } : msg )); } } } catch (error) { setHistory(prev => prev.map(msg => msg.id === botMessageId ? { ...msg, text: '请求失败,请重试', isStreaming: false } : msg )); } finally { setCurrentStreamId(null); setHistory(prev => prev.map(msg => msg.id === botMessageId ? { ...msg, isStreaming: false } : msg )); } }; return ( <div className="chat-container"> {history.map(message => ( <div key={message.id} className={`message ${message.author.toLowerCase()}`} > <div className="author">{message.author}</div> <div className="text"> {message.text} {message.isStreaming && <span className="stream-cursor">▋</span>} </div> </div> ))} <button onClick={() => handleStreamResponse('你好,有什么可以帮助的?')} disabled={!!currentStreamId} > {currentStreamId ? '回复中...' : '发送消息'} </button> </div> ); }
实现说明:
消息状态管理:
// 使用数组存储对话历史 const [history, setHistory] = useState<Message[]>([]); // 当前正在流式接收的消息ID const [currentStreamId, setCurrentStreamId] = useState<string | null>(null);
流式处理核心逻辑:
// 创建机器人消息占位符 const botMessageId = generateId(); setHistory(prev => [ ...prev, { id: botMessageId, author: 'Bot', text: '', isStreaming: true } ]); // 逐步更新消息内容 setHistory(prev => prev.map(msg => msg.id === botMessageId ? { ...msg, text: msg.text + chunk } : msg ));
UI 优化功能:
// 流式输入时的闪烁光标 {message.isStreaming && <span className="stream-cursor">▋</span>} // 禁用按钮防止重复请求 <button disabled={!!currentStreamId}>
错误处理机制:
catch (error) { setHistory(prev => prev.map(msg => msg.id === botMessageId ? { ...msg, text: '请求失败,请重试', isStreaming: false } : msg )); }
配套样式建议:
.chat-container { max-width: 600px; margin: 0 auto; padding: 20px; } .message { margin: 10px 0; padding: 12px; border-radius: 8px; } .message.user { background: #e3f2fd; margin-left: 20%; } .message.bot { background: #f5f5f5; margin-right: 20%; } .stream-cursor { animation: blink 1s infinite; margin-left: 4px; } @keyframes blink { 0%, 100% { opacity: 1; } 50% { opacity: 0; } }
优化建议:
添加消息分块缓冲:
let buffer = ''; const processChunk = (chunk: string) => { buffer += chunk; const sentences = buffer.split(/(?<=[。!?\n])/); buffer = sentences.pop() || ''; if (sentences.length > 0) { setHistory(prev => prev.map(msg => msg.id === botMessageId ? { ...msg, text: msg.text + sentences.join('') } : msg )); } }; // 在循环中调用 processChunk(chunk);
性能优化:
// 使用防抖更新 const updateText = debounce((newText: string) => { setHistory(prev => prev.map(msg => msg.id === botMessageId ? { ...msg, text: newText } : msg )); }, 50); // 在循环中 updateText(currentText + chunk);
支持Markdown渲染:
import ReactMarkdown from 'react-markdown'; // 替换原有文本展示 <div className="text"> <ReactMarkdown>{message.text}</ReactMarkdown> {message.isStreaming && <span className="stream-cursor">▋</span>} </div>
添加消息撤回功能:
const handleRetract = (messageId: string) => { setHistory(prev => prev.filter(msg => msg.id !== messageId) ); }; // 在消息组件中添加 <button className="retract-btn" onClick={() => handleRetract(message.id)} > × </button>
该方案实现了以下核心功能:
- 完整的对话历史管理
- 实时流式消息展示
- 自动滚动支持
- 错误状态处理
- 流畅的交互反馈
- 防止重复请求
可根据实际需求添加以下扩展功能:
- 消息编辑功能
- 对话持久化存储
- 消息类型支持(图片/文件)
- 打字机滚动效果
- 消息翻译功能
03 简单代码应用
import { useState } from 'react'; export default function ChatApp() { const [message, setMessage] = useState(''); const [loading, setLoading] = useState(false); const fetchStream = async (inputText) => { setLoading(true); try { const response = await fetch('http://localhost:3001/chat', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ message: inputText }) }); const reader = response.body.getReader(); const decoder = new TextDecoder(); while (true) { const { done, value } = await reader.read(); if (done) break; const chunk = decoder.decode(value); setMessage(prev => prev + chunk); // 实时更新消息 } } catch (error) { console.error('请求失败:', error); } finally { setLoading(false); } }; return ( <div> <button onClick={() => fetchStream('你好')} disabled={loading} > {loading ? '发送中...' : '开始对话'} </button> <div style={{ border: '1px solid #ccc', padding: '1rem' }}> {message} </div> </div> ); }
到此这篇关于Javascript中Fetch请求Coze API并流式展示请求结果的文章就介绍到这了,更多相关js Fetch请求Coze API内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!