React 對(duì)接流式接口的具體使用
在現(xiàn)代 AI 對(duì)話應(yīng)用中,流式響應(yīng)(Streaming Response)已經(jīng)成為提升用戶體驗(yàn)的關(guān)鍵技術(shù)。本文將詳細(xì)介紹如何在 React 應(yīng)用中實(shí)現(xiàn)流式接口的對(duì)接。
一、流式接口的基本概念
流式接口允許服務(wù)器以流的形式持續(xù)發(fā)送數(shù)據(jù),而不是等待所有數(shù)據(jù)準(zhǔn)備就緒后一次性返回。在 AI 對(duì)話場(chǎng)景中,這意味著用戶可以實(shí)時(shí)看到 AI 的回復(fù),而不是等待完整回復(fù)后才能看到內(nèi)容。
二、技術(shù)實(shí)現(xiàn)
1. 服務(wù)端請(qǐng)求實(shí)現(xiàn)
const response = await fetch('/api/stream', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Accept': 'text/event-stream', }, cache: 'no-store', keepalive: true, body: JSON.stringify(payload) }); const reader = response.body?.getReader(); const decoder = new TextDecoder();
關(guān)鍵點(diǎn):
- 使用 fetch API 發(fā)起請(qǐng)求
- 設(shè)置 Accept: text/event-stream 頭部
- 使用 ReadableStream 讀取流數(shù)據(jù)
- 通過(guò) TextDecoder 解碼二進(jìn)制數(shù)據(jù)
2. 數(shù)據(jù)處理與解析
while (reader) { const { done, value } = await reader.read(); if (done) break; const chunk = decoder.decode(value, { stream: true }); const lines = chunk.split('\n').filter(line => line.trim()); for (const line of lines) { if (line.startsWith('data:')) { const jsonStr = line.replace(/^data:/, '').trim(); const message = JSON.parse(jsonStr); // 處理消息 } } }
關(guān)鍵點(diǎn):
- 循環(huán)讀取流數(shù)據(jù)
- 按行解析數(shù)據(jù)
- 處理 SSE(Server-Sent Events)格式
- JSON 解析與錯(cuò)誤處理
3. React 狀態(tài)更新
由于后端返回的分片長(zhǎng)度可能不一(網(wǎng)關(guān)、AP、協(xié)議等原因)以及React短時(shí)間多次更新狀態(tài)會(huì)合并成成一次更新,所以需要前端自己兼容實(shí)現(xiàn)穩(wěn)定的輸出
const [messages, setMessages] = useState<Message[]>([]);
const handleNewContent = (content: string) => { flushSync(() => { setMessages(oldMessages => { const newMessages = [...oldMessages]; newMessages[newMessages.length - 1] = { ...newMessages[newMessages.length - 1], content: newMessages[newMessages.length - 1].content + content }; return newMessages; }); }); };
關(guān)鍵點(diǎn):
- 使用 useState 管理消息狀態(tài)
- 使用 flushSync 確保狀態(tài)更新的同步性
- 增量更新消息內(nèi)容
4. 打字機(jī)效果實(shí)現(xiàn)
const chars = content.split(''); await Promise.all( chars.map((char, index) => new Promise(resolve => setTimeout(() => { onNewMsg(char); resolve(null); }, index * 50) ) ) );
關(guān)鍵點(diǎn):
- 字符分割
- 使用 Promise.all 和 setTimeout 實(shí)現(xiàn)打字效果
- 可配置的打字速度
三、錯(cuò)誤處理與中斷控制
try { if (options.abortSignal?.aborted) { reader.cancel(); return false; } // ... 處理邏輯 } catch (error) { console.error('Stream error:', error); return { content: '請(qǐng)求失敗', isError: true }; }
關(guān)鍵點(diǎn):
- 支持請(qǐng)求中斷
- 錯(cuò)誤狀態(tài)處理
- 用戶友好的錯(cuò)誤提示
四、性能優(yōu)化
- 批量更新:使用 flushSync 確保狀態(tài)更新的及時(shí)性
- 防抖處理:對(duì)頻繁的狀態(tài)更新進(jìn)行控制
- 內(nèi)存管理:及時(shí)清理不需要的數(shù)據(jù)和監(jiān)聽器
五、用戶體驗(yàn)提升
- 加載狀態(tài):顯示打字機(jī)效果
- 錯(cuò)誤處理:友好的錯(cuò)誤提示
- 實(shí)時(shí)反饋:即時(shí)顯示接收到的內(nèi)容
總結(jié)
實(shí)現(xiàn)流式接口不僅需要考慮技術(shù)實(shí)現(xiàn),還要注重用戶體驗(yàn)。通過(guò)合理的狀態(tài)管理、錯(cuò)誤處理和性能優(yōu)化,可以打造出流暢的 AI 對(duì)話體驗(yàn)。
關(guān)鍵代碼可參考:
請(qǐng)求:
export type AIStreamResponse = { content: string; hasDone: boolean; isError: boolean; }; export const postAIStream = async ( options: { messages: AIMessage[]; abortSignal?: AbortSignal; // 新增可中斷信號(hào) }, onNewMsg: (msg: string) => void, model: string, operator: string, ): Promise<AIStreamResponse | false> => { // 檢查是否中斷 if (options.abortSignal?.aborted) { return false; } // 新增 usage 變量 let usage: any = {}; // 將原來(lái)的 Modal.confirm 替換為統(tǒng)一函數(shù) showRetryConfirm try { const response = await fetch('/model/service/stream', { method: 'POST', credentials: 'include', headers: { 'Content-Type': 'application/json', Accept: 'text/event-stream', }, // 添加HTTP/2相關(guān)配置 cache: 'no-store', keepalive: true, body: JSON.stringify({ operator, model, messages: options.messages, stream: true, }), }); const reader = response.body?.getReader(); const decoder = new TextDecoder(); let hasDone = false; let content = ''; let incompleteLine = ''; // 存儲(chǔ)不完整的行 while (reader) { // 檢查中斷信號(hào) if (options.abortSignal?.aborted) { reader.cancel(); return false; } const { done, value } = await reader.read(); if (done) { break; } const chunk = decoder.decode(value, { stream: true }); // 將上一個(gè)不完整的行與當(dāng)前chunk拼接 const textToProcess = incompleteLine + chunk; incompleteLine = ''; // 按行分割,但保持事件標(biāo)記完整 const lines = textToProcess .split(/\n/) .map((line) => line.trim()) .filter((line) => line); for (let i = 0; i < lines.length; i++) { const line = lines[i]; // 標(biāo)記正常結(jié)束 if (line.includes(`event:done`)) { hasDone = true; } // 如果是最后一行且chunk沒有以換行符結(jié)束,認(rèn)為可能是不完整的 if (i === lines.length - 1 && !chunk.endsWith('\n')) { incompleteLine = line; continue; } if (line.startsWith('data:')) { try { const jsonStr = line.replace(/^data:/, '').trim(); // 確保不處理空字符串 if (!jsonStr) continue; const message = JSON.parse(jsonStr); // 新增:處理 usage 字段 if (message.data && message.data.usage) { usage = message.data.usage; } if (!message.finish && message.data?.choices?.[0]?.message?.content) { const currentContent = message.data.choices[0].message.content; // 按字符分割當(dāng)前內(nèi)容 const chars = currentContent.split(''); // 使用 Promise.all 和 setTimeout 實(shí)現(xiàn)均勻的打字效果 await Promise.all( chars.map( (char: string, index: number) => new Promise( (resolve) => setTimeout(() => { onNewMsg(char); resolve(null); }, index * 50), // 每個(gè)字符之間間隔 50ms ), ), ); content += currentContent; } } catch (e) { console.warn('Parse error, might be incomplete JSON:', line); // 如果不是最后一行卻解析失敗,記錄錯(cuò)誤 if (i < lines.length - 1) { console.error('JSON parse error in middle of chunk:', e); } continue; } } } } // 結(jié)束時(shí)返回 content 和 usage if (hasDone) { return { content, usage, hasDone, isError: false }; } return { content: '大模型調(diào)用失敗', usage, hasDone: true, isError: true }; } catch (error) { console.error('Stream error:', error); return { content: '大模型調(diào)用失敗', usage, hasDone: true, isError: true }; } };
調(diào)用
const res = await postAIStream( { messages: [newMessages], }, (content) => { flushSync(() => setMessages((oldMessage) => { const messages = [...oldMessage]; messages[messages.length - 1] = { content: messages[messages.length - 1].content + content, role: 'assistant', }; return messages; }), ); if (!isStart) { isStart = true; } }, currentModel, userInfo?.username, ).finally(() => { setLoading(false); });
到此這篇關(guān)于React 對(duì)接流式接口的具體使用的文章就介紹到這了,更多相關(guān)React 對(duì)接流式接口內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
- react 通過(guò)后端接口實(shí)現(xiàn)路由授權(quán)的示例代碼
- 在React和Vue中使用Mock.js模擬接口的實(shí)現(xiàn)方法
- vue3 reactive 請(qǐng)求接口數(shù)據(jù)賦值后拿不到的問(wèn)題及解決方案
- react+antd4實(shí)現(xiàn)優(yōu)化大批量接口請(qǐng)求
- react:swr接口緩存案例代碼
- React如何通過(guò)@craco/craco代理接口
- react實(shí)現(xiàn)每隔60s刷新一次接口的示例代碼
- Rainbond調(diào)用Vue?React項(xiàng)目的后端接口
- React項(xiàng)目中axios的封裝與API接口的管理詳解
相關(guān)文章
從零開始最小實(shí)現(xiàn)react服務(wù)器渲染詳解
這篇文章主要介紹了從零開始最小實(shí)現(xiàn)react服務(wù)器渲染詳解,小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2018-01-01React+Router多級(jí)導(dǎo)航切換路由方式
這篇文章主要介紹了React+Router多級(jí)導(dǎo)航切換路由方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2024-03-03React Native使用Modal自定義分享界面的示例代碼
本篇文章主要介紹了React Native使用Modal自定義分享界面的示例代碼,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-10-10詳解React Native開源時(shí)間日期選擇器組件(react-native-datetime)
本篇文章主要介紹了詳解React Native開源時(shí)間日期選擇器組件(react-native-datetime),具有一定的參考價(jià)值,有興趣的可以了解一下2017-09-09使用react+redux實(shí)現(xiàn)彈出框案例
這篇文章主要為大家詳細(xì)介紹了使用react+redux實(shí)現(xiàn)彈出框案例,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-08-08