Javascript中Fetch請求Coze API并流式展示請求結果
01 思路技術選型Fetch
用戶現(xiàn)在的問題是如何將coze API返回的流式響應展示出來。之前已經幫忙封裝了axios請求,現(xiàn)在需要處理流式數(shù)據(jù)。我需要回想一下流式請求的處理方式,通常SSE(Server-Sent Events)或者使用fetch的流式讀取,但用戶使用的是axios,可能需要配置responseType為stream或者處理分塊數(shù)據(jù)。
首先,用戶之前的代碼中設置了stream: true,這可能意味著API返回的是流式數(shù)據(jù),比如每個chunk逐步返回。但axios默認情況下可能不會處理這種流式響應,可能需要調整配置。例如,使用fetch的話,可以監(jiān)聽chunk,但axios是否支持呢?
根據(jù)之前的知識,處理流式響應通??梢允褂肊ventSource,但需要服務器支持SSE。如果API返回的是普通的分塊數(shù)據(jù)流,可能需要使用responseType: ‘stream’(在Node.js中)或者在前端使用ReadableStream。不過在前端瀏覽器環(huán)境中,axios可能無法直接處理流式數(shù)據(jù),或者需要特定的配置。
用戶現(xiàn)在用的是瀏覽器環(huán)境嗎?假設是前端項目,比如Vue或React。這時候可能需要使用fetch API,因為fetch可以處理流式數(shù)據(jù),而axios在這方面的支持可能有限?;蛘撸瑱z查coze的API文檔,確認流式返回的數(shù)據(jù)格式,比如是否每個chunk是一個JSON對象或特定格式的數(shù)據(jù)。
例如,如果API使用SSE,每個事件是一個數(shù)據(jù)塊,那么在前端應該使用EventSource來監(jiān)聽事件。但如果API是普通的流式HTTP響應,分塊發(fā)送數(shù)據(jù),那么可能需要使用fetch并逐步讀取響應體。
在用戶之前的封裝中,使用的是axios.post,并且設置了stream: true。但可能沒有正確配置axios來處理流式響應。因此,可能需要改用fetch,或者調整axios的配置,比如設置responseType為’stream’(但瀏覽器中可能不支持),或者使用onDownloadProgress回調來處理接收到的數(shù)據(jù)片段。
查閱資料發(fā)現(xiàn),axios在瀏覽器中的responseType支持’arraybuffer’, ‘blob’, ‘document’, ‘json’, ‘text’, ‘stream’(stream僅在Node.js環(huán)境中有效)。因此,在瀏覽器中,可能無法直接使用stream類型。這時可能需要使用text類型,并手動處理分塊數(shù)據(jù)。但這種方式可能不可靠,因為axios會等待整個響應完成后再觸發(fā)then。
因此,正確的做法應該是使用fetch API,因為fetch支持流式讀取響應體。例如,使用fetch并設置body為可讀流,然后通過reader來逐步讀取數(shù)據(jù)。
或者,如果服務器支持SSE,則使用EventSource。但需要確認coze的API是否使用SSE格式,即返回的數(shù)據(jù)格式是否符合text/event-stream,并且每個事件由開頭。
假設用戶需要處理的是普通的流式JSON數(shù)據(jù),每個chunk是一個JSON對象,那么可能需要使用fetch來獲取響應體,并通過讀取器逐步處理數(shù)據(jù)塊。
總結處理步驟:
- 使用fetch代替axios,因為fetch支持流式讀取。
- 設置請求參數(shù),包括method: ‘POST’, headers, body等。
- 讀取響應體流,使用ReadableStream.getReader()。
- 循環(huán)讀取每個chunk,處理并更新UI。
接下來需要編寫示例代碼,展示如何發(fā)起請求并處理流式響應,逐步將數(shù)據(jù)展示在頁面上。同時,注意處理文本的拼接和解析,特別是如果每個chunk可能包含部分數(shù)據(jù),需要正確拼接成完整的JSON對象或文本內容。
可能的實現(xiàn)方式:
- 使用fetch發(fā)起POST請求。
- 獲取響應體的reader。
- 創(chuàng)建解碼器來處理Uint8Array數(shù)據(jù)為文本。
- 循環(huán)讀取chunk,直到done為true。
- 處理每個chunk的文本,可能按特定分隔符(如\n\n)分割,解析每個事件的數(shù)據(jù)。
- 如果coze的API返回的是類似OpenAI的流式響應,每個chunk是一個{…}\n\n的結構,那么需要按行分割處理。
最后,將處理后的數(shù)據(jù)逐步顯示在頁面上,比如通過React的state更新,或者直接操作DOM。
在代碼中,需要注意錯誤處理,比如網絡錯誤或流讀取中的錯誤,以及如何中止請求(例如用戶取消操作)。
現(xiàn)在,將這些思路整理成步驟化的解答,并提供示例代碼,指導用戶如何修改之前的封裝以支持流式展示。
要實現(xiàn)從 coze API 獲取流式數(shù)據(jù)并實時展示,可以使用 fetch API 處理流式響應,逐步讀取并渲染數(shù)據(jù)。以下是完整實現(xiàn)方案:
// 封裝流式請求方法
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;
// 持續(xù)讀取流數(shù)據(jù)
while (true) {
const { done, value } = await reader.read();
if (done) break;
// 解碼并處理數(shù)據(jù)塊
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); // 觸發(fā)回調更新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 ? '請求中...' : '發(fā)送流式請求'}
</button>
<div style={{ whiteSpace: 'pre-wrap' }}>
{message}
</div>
</div>
);
}核心實現(xiàn)原理:
流式協(xié)議處理
- 使用
fetch原生API獲取可讀流(ReadableStream) - 通過
TextDecoder處理二進制流為文本 - 按
\n分割數(shù)據(jù)行,處理服務端可能合并發(fā)送的多條消息
數(shù)據(jù)格式解析
- 假設服務端采用
JSON格式發(fā)送事件 - 自動過濾非數(shù)據(jù)行(如心跳檢測的空行)
- 使用
JSON.parse嚴格解析數(shù)據(jù)格式
實時渲染優(yōu)化
- 通過回調函數(shù)逐塊傳遞數(shù)據(jù)
- 前端框架中通過狀態(tài)合并(如React的setState)優(yōu)化渲染性能
- 保留未完成數(shù)據(jù)塊實現(xiàn)斷包續(xù)傳
高級功能擴展建議:
中斷控制
// 創(chuàng)建AbortController
const controller = new AbortController();
// 在fetch配置中添加
signal: controller.signal,
// 暴露中斷方法
return {
abort: () => controller.abort()
};性能優(yōu)化
// 使用防抖渲染(每秒更新不超過60次)
let renderQueue = '';
const debouncedUpdate = useRef(debounce(() => {
setMessage(prev => prev + renderQueue);
renderQueue = '';
}, 16)).current;
// 在回調中累積數(shù)據(jù)
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 服務端轉發(fā)
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開發(fā)者工具中開啟「保留日志」查看完整流式請求
- 使用
curl -N URL驗證原始API的流式特性 - 添加調試日志記錄原始數(shù)據(jù)塊:
console.log('[Raw Chunk]', chunk);02 過程分析
用戶現(xiàn)在想要在前端項目中實現(xiàn)流式接收并展示數(shù)據(jù),特別是在對話框中使用類似setHistoryContent這樣的狀態(tài)更新函數(shù)。用戶給出的代碼片段顯示他們正在使用React的狀態(tài)管理,將消息添加到歷史記錄中。用戶可能已經有一個基本的聊天界面,現(xiàn)在需要集成Coze API的流式響應。
首先,我需要理解用戶的具體需求。他們可能已經能夠發(fā)送消息到后端,但接收響應時是流式的,需要逐步顯示數(shù)據(jù),而不是等待整個響應完成。這可能涉及到使用Fetch API的流式處理功能,或者WebSocket,但根據(jù)之前的對話,用戶使用的是Fetch。
接下來,用戶提供的代碼示例中,使用setHistoryContent來更新消息歷史。當前的實現(xiàn)可能是在接收到完整響應后一次性添加消息,但現(xiàn)在需要改為每收到一個數(shù)據(jù)塊就更新一次。這需要處理流式數(shù)據(jù),逐步拼接內容,并更新對應的消息條目。
需要考慮的問題包括:
- 如何正確處理流式數(shù)據(jù),確保每個數(shù)據(jù)塊都能被及時處理。
- 如何管理React的狀態(tài),特別是當需要更新現(xiàn)有消息而不是添加新消息時。
- 如何處理可能的并發(fā)請求,避免狀態(tài)混亂。
- 錯誤處理和加載狀態(tài)的顯示。
用戶之前的例子中,每次接收到新的內容,都會添加一個新的消息對象到歷史記錄中。但流式響應應該是逐步更新同一個消息的內容,而不是創(chuàng)建多個消息。因此,可能需要調整狀態(tài)結構,使得可以更新特定消息的文本內容。
例如,初始時添加一個帶有臨時ID的消息對象,表示機器人的響應正在加載,然后隨著數(shù)據(jù)塊的到來,不斷更新該消息的text屬性。完成后再標記為完成。
可能的實現(xiàn)步驟:
- 用戶發(fā)送消息時,立即將用戶的消息添加到歷史記錄中。
- 發(fā)送請求到Coze API,開始接收流式響應。
- 在接收到第一個數(shù)據(jù)塊時,創(chuàng)建一個新的消息對象,標記為機器人,并開始填充text。
- 后續(xù)的數(shù)據(jù)塊不斷追加到該消息的text中。
- 流式結束時,可能標記該消息為完成,或處理可能的錯誤。
需要處理的技術點包括使用Fetch API的ReadableStream,正確讀取數(shù)據(jù)塊,并在React中高效更新狀態(tài)。同時,需要確保在組件卸載時取消請求,避免內存泄漏。
另外,用戶可能需要處理消息的ID管理,確保在更新時能找到正確的消息條目。例如,使用唯一ID來標識每個消息,并在流式響應開始時生成一個ID,后續(xù)更新都針對該ID。
可能的代碼結構:
- 發(fā)送用戶消息時,添加到歷史記錄。
- 發(fā)起Fetch請求,獲取響應流。
- 讀取流中的數(shù)據(jù),逐步更新對應的機器人消息內容。
- 使用useState或useReducer管理消息歷史狀態(tài)。
- 使用AbortController來允許取消請求。
需要確保在React的嚴格模式下,狀態(tài)更新不會導致問題,尤其是在異步處理流時??赡苄枰褂胷ef來保持最新狀態(tài)的引用,避免閉包問題。
最后,測試流式響應是否正確,確保每個數(shù)據(jù)塊都能實時顯示,并且界面不會因為頻繁的狀態(tài)更新而卡頓。可能需要優(yōu)化渲染性能,比如使用React.memo或適當?shù)臓顟B(tài)結構。
以下是針對對話框場景的流式數(shù)據(jù)接收和展示完整實現(xiàn)方案:
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的工具函數(shù)
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 }
]);
// 創(chuàng)建機器人消息占位符
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 ? '回復中...' : '發(fā)送消息'}
</button>
</div>
);
}實現(xiàn)說明:
消息狀態(tài)管理:
// 使用數(shù)組存儲對話歷史 const [history, setHistory] = useState<Message[]>([]); // 當前正在流式接收的消息ID const [currentStreamId, setCurrentStreamId] = useState<string | null>(null);
流式處理核心邏輯:
// 創(chuàng)建機器人消息占位符
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 優(yōu)化功能:
// 流式輸入時的閃爍光標
{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; }
}優(yōu)化建議:
添加消息分塊緩沖:
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
));
}
};
// 在循環(huán)中調用
processChunk(chunk);性能優(yōu)化:
// 使用防抖更新
const updateText = debounce((newText: string) => {
setHistory(prev => prev.map(msg =>
msg.id === botMessageId
? { ...msg, text: newText }
: msg
));
}, 50);
// 在循環(huán)中
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>該方案實現(xiàn)了以下核心功能:
- 完整的對話歷史管理
- 實時流式消息展示
- 自動滾動支持
- 錯誤狀態(tài)處理
- 流暢的交互反饋
- 防止重復請求
可根據(jù)實際需求添加以下擴展功能:
- 消息編輯功能
- 對話持久化存儲
- 消息類型支持(圖片/文件)
- 打字機滾動效果
- 消息翻譯功能
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 ? '發(fā)送中...' : '開始對話'}
</button>
<div style={{ border: '1px solid #ccc', padding: '1rem' }}>
{message}
</div>
</div>
);
}到此這篇關于Javascript中Fetch請求Coze API并流式展示請求結果的文章就介紹到這了,更多相關js Fetch請求Coze API內容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
JavaScript中Textarea滾動條不能拖動的解決方法
這篇文章主要介紹了JavaScript中Textarea滾動條不能拖動的解決方法,主要針對IE瀏覽器中Textarea滾動條綁定了onfocus事件時分析對應的處理方法,需要的朋友可以參考下2015-12-12
JavaScript實現(xiàn)獲取指定音視頻流鏈接的時長
這篇文章主要為大家詳細介紹了JavaScript實現(xiàn)獲取指定音頻或視頻流鏈接的時長,文中的示例代碼講解詳細,感興趣的小伙伴可以跟隨小編一起學習一下2026-02-02
通過Tabs方法基于easyUI+bootstrap制作工作站
本教程給大家介紹如何制作easyUI+bootstrap工作站,主要學習tabs方法,本文介紹非常詳細,具有參考借鑒價值,需要的朋友參考下吧2016-03-03

