排查点
- 是否是跨域CORS问题
- 后端必须用yield产生流式信息,返回类型为
fastapi.responses.StreamingResponse
,数据格式为data: XXXX \n\n
,如果要返回JSON,需要将其字符串化。数据类型为text/event-stream
注意:
数据格式必须以
data
开头,以\n\n
结尾。如果数据格式没有以data开头,在浏览器网络栏目,EventStrem不会有返回信息,数据信息在响应里面。
stream结束后,前端会重连,需要后端发送一个终止标志,前端判断后进行主动关闭。
// 响应事件关闭, 后端需要发送数据格式event: close\ndata: bye\n\n
eventSource.addEventListener("close", (event) => {
console.log("Received close event");
eventSource.close();
});
// 判断关闭, 判断关键字
eventSource.onmessage = (event) => {
console.log("Message:", event.data);
if (event.data === "[done]") {
eventSource.close();
console.log("Closed on 'done' message");
}
};
- 前端接收可以用EventSource也可以用fetch,看完整代码示例
完整代码
fastapi:
@app.get("/stream")
async def stream_video():
async def video_stream():
for i in range(10):
await asyncio.sleep(3)
print(f"i = {i}")
yield f"data: Streamed line {i}\n\n"
yield "event: close\ndata: bye\n\n" # Custom event to signal end
return StreamingResponse(
video_stream(),
media_type="text/event-stream",
headers={
"Cache-Control": "no-cache",
"X-Accel-Buffering": "no",
},
)
单脚本vue,直接浏览器打开:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width">
<title></title>
</head>
<body>
<div id="app">
<div class="flex justify-between px-10 borderd border-1">
<div class="flex flex-col w-[200px] mt-10">
<button @click="start_stream_1" class="mb-5 bg-teal-700 px-4 py-1 text-white rounded-2xl cursor-pointer">
start_stream_1
</button>
<button @click="start_stream_2" class="mb-5 bg-teal-700 px-4 py-1 text-white rounded-2xl cursor-pointer">
start_stream_2
</button>
<div class="w-fit flex justify-between">
<div class="block w-[500px] h-[300px] whitespace-pre-line p-6 bg-white border border-gray-200 rounded-lg shadow-sm hover:bg-gray-100">
{{msgs.content}}
</div>
<div class="ml-10 block w-[500px] h-[300px] whitespace-pre-line p-6 bg-white border border-gray-200 rounded-lg shadow-sm hover:bg-gray-100">
{{msgs2.content}}
</div>
</div>
</div>
</div>
<script src="https://unpkg.com/@tailwindcss/browser@4"></script>
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
<script>
const { createApp, ref, onMounted, reactive } = Vue;
createApp({
setup() {
const msgs = reactive({
"content": ""
})
const msgs2 = reactive({
"content": ""
})
const start_stream_1 = async () => {
const eventSource = new EventSource('http://127.0.0.1:8111/stream');
eventSource.onopen = function(){
alert('connection readyState: '+ eventSource.readyState);
};
// 监听消息事件
eventSource.onmessage = function(event) {
console.log("Received:", event.data);
Object.assign(msgs, {"content": msgs.content + "\n" + event.data})
//console.log(JSON.parse(event.data)); // 如果fastapi返回json字符串
};
eventSource.addEventListener("close", (event) => {
console.log("Received close event");
Object.assign(msgs, {"content": msgs.content + "\nfinish receive"})
eventSource.close();
});
}
const start_stream_2 = async () => {
const eventSource = new EventSource('http://127.0.0.1:8111/stream');
fetch("http://127.0.0.1:8111/stream")
.then(response => {
const reader = response.body.getReader();
const decoder = new TextDecoder();
function read() {
reader.read().then(({ done, value }) => {
if (done) {
Object.assign(msgs2, {"content": msgs.content + "\nfinish receive"})
return;
}
const chunk = decoder.decode(value, { stream: true });
console.log("Chunk:", chunk); // Handle/display chunk
Object.assign(msgs2, {"content": msgs.content + "\n" + chunk})
read(); // Continue reading
});
}
read();
})
.catch(err => console.error("Fetch error:", err));
}
onMounted(() => {
console.log("on mounted")
})
return {
start_stream_1,
start_stream_2,
msgs,
msgs2
};
}
}).mount('#app');
</script>
</body>
</html>