React 對接流式接口的具體使用
在現(xiàn)代 AI 對話應(yīng)用中,流式響應(yīng)(Streaming Response)已經(jīng)成為提升用戶體驗(yàn)的關(guān)鍵技術(shù)。本文將詳細(xì)介紹如何在 React 應(yīng)用中實(shí)現(xiàn)流式接口的對接。
一、流式接口的基本概念
流式接口允許服務(wù)器以流的形式持續(xù)發(fā)送數(shù)據(jù),而不是等待所有數(shù)據(jù)準(zhǔn)備就緒后一次性返回。在 AI 對話場景中,這意味著用戶可以實(shí)時看到 AI 的回復(fù),而不是等待完整回復(fù)后才能看到內(nèi)容。
二、技術(shù)實(shí)現(xiàn)
1. 服務(wù)端請求實(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ā)起請求
- 設(shè)置 Accept: text/event-stream 頭部
- 使用 ReadableStream 讀取流數(shù)據(jù)
- 通過 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 解析與錯誤處理
3. React 狀態(tài)更新
由于后端返回的分片長度可能不一(網(wǎng)關(guān)、AP、協(xié)議等原因)以及React短時間多次更新狀態(tài)會合并成成一次更新,所以需要前端自己兼容實(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)打字效果
- 可配置的打字速度
三、錯誤處理與中斷控制
try {
if (options.abortSignal?.aborted) {
reader.cancel();
return false;
}
// ... 處理邏輯
} catch (error) {
console.error('Stream error:', error);
return { content: '請求失敗', isError: true };
}關(guān)鍵點(diǎn):
- 支持請求中斷
- 錯誤狀態(tài)處理
- 用戶友好的錯誤提示
四、性能優(yōu)化
- 批量更新:使用 flushSync 確保狀態(tài)更新的及時性
- 防抖處理:對頻繁的狀態(tài)更新進(jìn)行控制
- 內(nèi)存管理:及時清理不需要的數(shù)據(jù)和監(jiān)聽器
五、用戶體驗(yàn)提升
- 加載狀態(tài):顯示打字機(jī)效果
- 錯誤處理:友好的錯誤提示
- 實(shí)時反饋:即時顯示接收到的內(nèi)容
總結(jié)
實(shí)現(xiàn)流式接口不僅需要考慮技術(shù)實(shí)現(xiàn),還要注重用戶體驗(yàn)。通過合理的狀態(tài)管理、錯誤處理和性能優(yōu)化,可以打造出流暢的 AI 對話體驗(yàn)。
關(guān)鍵代碼可參考:
請求:
export type AIStreamResponse = {
content: string;
hasDone: boolean;
isError: boolean;
};
export const postAIStream = async (
options: {
messages: AIMessage[];
abortSignal?: AbortSignal; // 新增可中斷信號
},
onNewMsg: (msg: string) => void,
model: string,
operator: string,
): Promise<AIStreamResponse | false> => {
// 檢查是否中斷
if (options.abortSignal?.aborted) {
return false;
}
// 新增 usage 變量
let usage: any = {};
// 將原來的 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 = ''; // 存儲不完整的行
while (reader) {
// 檢查中斷信號
if (options.abortSignal?.aborted) {
reader.cancel();
return false;
}
const { done, value } = await reader.read();
if (done) {
break;
}
const chunk = decoder.decode(value, { stream: true });
// 將上一個不完整的行與當(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), // 每個字符之間間隔 50ms
),
),
);
content += currentContent;
}
} catch (e) {
console.warn('Parse error, might be incomplete JSON:', line);
// 如果不是最后一行卻解析失敗,記錄錯誤
if (i < lines.length - 1) {
console.error('JSON parse error in middle of chunk:', e);
}
continue;
}
}
}
}
// 結(jié)束時返回 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 對接流式接口的具體使用的文章就介紹到這了,更多相關(guān)React 對接流式接口內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
- react 通過后端接口實(shí)現(xiàn)路由授權(quán)的示例代碼
- 在React和Vue中使用Mock.js模擬接口的實(shí)現(xiàn)方法
- vue3 reactive 請求接口數(shù)據(jù)賦值后拿不到的問題及解決方案
- react+antd4實(shí)現(xiàn)優(yōu)化大批量接口請求
- react:swr接口緩存案例代碼
- React如何通過@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ù)器渲染詳解,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2018-01-01
React Native使用Modal自定義分享界面的示例代碼
本篇文章主要介紹了React Native使用Modal自定義分享界面的示例代碼,具有一定的參考價值,感興趣的小伙伴們可以參考一下2017-10-10
詳解React Native開源時間日期選擇器組件(react-native-datetime)
本篇文章主要介紹了詳解React Native開源時間日期選擇器組件(react-native-datetime),具有一定的參考價值,有興趣的可以了解一下2017-09-09
使用react+redux實(shí)現(xiàn)彈出框案例
這篇文章主要為大家詳細(xì)介紹了使用react+redux實(shí)現(xiàn)彈出框案例,文中示例代碼介紹的非常詳細(xì),具有一定的參考價值,感興趣的小伙伴們可以參考一下2022-08-08

