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

