🚀 EventSource、fetch流式读取......总有一款适合你
嘿,老朋友!上次咱们把FastAPI后端改造成了"水龙头",AI一个字一个字往外蹦。然后我偷偷懒,说"前端代码网上大把",结果后台好多小伙伴留言:"后端搞定了,前端死活接不上,救救我!"
**明明后端打印得好好的,浏览器就是没反应;**要么收到一堆乱码,要么连接一会儿就断。这感觉就像水龙头打开了,管子却是堵的。今天咱们就把这根"管子"彻底疏通,顺便把那些隐藏的"水垢"全清掉!
📡 前端接流三大主流方式
后端返回的是**text/event-stream** ,也就是 Server-Sent Events (SSE)。前端接收它主要有三种姿势:
🔹 方式1:浏览器原生EventSource ------ 最简单,但功能有限
🔹 方式2:fetch API + 流式读取 ------ 更灵活,可自定义请求头
🔹 方式3:第三方库 @microsoft/fetch-event-source ------ 全能选手,自动重连
🌰 方式1:EventSource ------ 杀鸡用牛刀?够用就行!
如果你不需要自定义请求头(比如不需要带Token),EventSource是最快的接入方式。几行代码搞定:
const eventSource = new EventSource('http://localhost:8000/chat?prompt=你好');
eventSource.onmessage = (event) => {
if (event.data === '[DONE]') {
console.log('对话结束');
eventSource.close();
return;
}
// 把内容追加到页面
document.getElementById('output').innerText += event.data;
};
eventSource.onerror = (err) => {
console.error('连接出错', err);
eventSource.close();
};
注意:EventSource只能发送GET请求,且不能添加自定义Headers(比如Authorization)。 如果你需要POST携带复杂参数,就得用下面两种。
🌊 方式2:fetch + 流式读取 ------ 真正的全栈式控制
fetch API 从 Chrome 95 开始完美支持流式响应。我们可以像读小说一样逐段读取数据:
async function fetchStream() {
const response = await fetch('http://localhost:8000/chat', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ prompt: '讲个故事' })
});
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);
// 假设后端每块返回 "data: xxx\n\n" 格式,需要解析
const lines = chunk.split('\n');
for (const line of lines) {
if (line.startsWith('data: ')) {
const data = line.slice(6);
if (data === '[DONE]') return;
document.getElementById('output').innerText += data;
}
}
}
}
fetchStream();
这种方式自由度超高,可以加Token,可以POST,甚至可以处理二进制流。但要自己手动解析SSE格式,容易在换行符上踩坑。
🛡️ 方式3:@microsoft/fetch-event-source ------ 躺平式接入
微软出品,专治各种SSE不服。它内置了断线重连、自动解析、错误恢复等功能,强烈推荐在生产环境使用:
import { fetchEventSource } from '@microsoft/fetch-event-source';
await fetchEventSource('http://localhost:8000/chat', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ prompt: '写首诗' }),
onmessage(ev) {
if (ev.data === '[DONE]') {
console.log('完成');
return;
}
document.getElementById('output').innerText += ev.data;
},
onerror(err) {
console.error('大事不妙', err);
},
onclose() {
console.log('连接关闭');
}
});
它会自动处理SSE格式,只要关注onmessage里的ev.data即可。连[DONE]都得自己判断,它只管传数据。
⚠️ 前方高能:前端最容易翻车的五个坑
💥 坑1:CORS 跨域 ------ 万恶之源
后端就算配了CORS,如果前端用了withCredentials或自定义Header,依然可能触发预检请求(OPTIONS),后端必须处理。用fetch-event-source时,它默认不会发credentials,注意设置。
💥 坑2:数据格式必须严格遵循 SSE 规范
后端每块必须是 data: xxx\n\n,两个换行符不能少。否则EventSource和微软库都可能解析失败。fetch方式自己解析也要注意换行可能跨块。
💥 坑3:连接意外断开与重连机制
EventSource 默认会自动重连,但如果是HTTP 401/500等错误,它会一直重连直到地老天荒。最好监听onerror判断状态码,必要时关闭。微软库提供了onerror回调,可以控制是否终止重连。
💥 坑4:浏览器兼容性
EventSource 和 fetch 的流式读取在 IE 和部分旧手机浏览器上不可用。可以用微软库的 polyfill,或者提示用户升级浏览器。
💥 坑5:内存泄漏与连接未关闭
组件卸载或页面跳转时,一定要调用eventSource.close()或中止fetch的AbortController,否则连接会一直挂起,浪费资源。
🎯 进阶小贴士:让打字机体验更逼真
前端显示时,可以用定时器稍微打散一下文字出现节奏,模拟真人打字。但注意不要过度,否则用户会疯:
let buffer = '';
onmessage(ev) {
buffer += ev.data;
// 每50ms渲染一个字符
if (!typingTimer) {
typingTimer = setInterval(() => {
if (buffer.length > 0) {
output.innerText += buffer[0];
buffer = buffer.slice(1);
} else {
clearInterval(typingTimer);
typingTimer = null;
}
}, 50);
}
}
另外,记得在UI上给个"停止生成"的按钮,调用abort()或close(),让用户随时打断。
好啦,前端三大流派和五个大坑都交代清楚了。你现在可以自信地拍着胸脯说:"流式输出,前端后端我全栈!"
如果还遇到怪问题,八成是换行符或者CORS,再不行就在评论区贴代码,咱们一起捉虫。觉得有用的话,点个⭐收藏,下次再遇到SSE,翻出这篇文章复习下再动手~
------ 依然是你那个爱踩坑的一名程序媛 👩💻