我如何处理流式响应?
使用
setState
的函数方法与函数似乎不起作用:
钩子没有按正确的顺序执行,并且响应是无序的
const response = await fetch('/dostuff', {
method:'POST',
body
})
const reader = response.body!.getReader();
const chunks = [];
let done, value;
while (!done) {
({ value, done } = await reader.read());
if (done) {
return chunks;
}
const strval = new TextDecoder().decode(value)
chunks.push(strval);
await setResponseText((prev)=> {
return prev + strval
})
// console.log("EEE ",strval);
}
useEffect hook 用于启动获取过程,responseText 状态在循环结束时更新一次。这应该有助于避免异步状态更新的潜在问题。您可以添加任何依赖项,以便在它们发生更改时触发效果再次运行。
useEffect(() => {
const fetchData = async () => {
const response = await fetch('/dostuff', {
method: 'POST',
body,
});
const reader = response.body.getReader();
let chunks = '';
while (true) {
const { value, done } = await reader.read();
if (done) {
break;
}
const strVal = new TextDecoder().decode(value);
chunks += strVal;
}
setResponseText(chunks);
};
fetchData();
}, []);
最终问题有两个不同点:
解码器
解码器需要位于循环之外,因为流响应可能在多字节字符的中间结束。在每次迭代时创建新的解码器不允许管理这种情况。
严格模式
老实说,我不认为 useEffect 钩子以某种方式参与了这个错误,所以我限制了原始问题中的代码,我的错。
发生的情况是,在开发模式下,React严格模式会导致组件的渲染和安装/卸载两次。
这会造成竞争状况,因为当第二个渲染的流响应开始时,第一个渲染的流响应仍然处于活动状态并正在接收数据。
因此,包含响应文本的状态有时会使用第一个流数据块进行更新,有时会使用第二个流数据块进行更新。
因此,解决方案是在 useEffect 调用中添加 AbortController。
useEffect( ()=> {
const controller = new AbortController();
const run = async (controller:AbortController) => {
let response
try {
response = await fetch('/doStuff', {
method:'POST',
body
})
} catch (error) {
if (error.name === "AbortError") {
return;
}
...
}
const reader = response.body!.getReader();
const chunks = [];
let done, value;
const dec = new TextDecoder()
while (!done) {
({ value, done } = await reader.read());
if (done) {
return chunks;
}
const strval = dec.decode(value, { stream: true })
chunks.push(strval);
text.current += strval
setResponseText(text.current)
}
}
run(controller)
return () => controller.abort();
},[])