流式响应 sse 系统全流程 react + fastapi为例子

前端

用户输入发送

js 复制代码
<button onClick={
	sendMessage(input)
}>
send
</button>

sendMessage函数签名

js 复制代码
//async await后段响应等待通信
const sendMessage = async (input) => {
	//message是用户输入的文本woshuodeshi
	//1. 确认session
	const sessionId = currentSessionId
	if(isNewSession || !sessionId){
		tempSessionId = `${uuidv4()}`
		const Session = {
			... // session创建
			temp:true
		}
		// 乐观更新 立刻给前端展示
		setSessions(prev=>[tempSession, ...prev])
		navgitate(`${tempSessionId}`)
		setIsNewSession(true)

		//backend creates new session with uuid from frontend
		const realSession = await createSession(session)
		setSessions(prev=> prev.filter(session => session.session_id !== tempSessionId))
		setCurrentSessionId(realSession.session_id)
	}

	//make sure the session exists
	//then add user message to the end of list
	const newUserMessage = {..., input}
	setMessages(prev => [...messages, newUserMessage]
	
	// 2. add streaming message flag
	// optmistic update
	const streamId = uuid4()
	const streamingMessage = {...}
	setMessages(prev => [...messages, streamingMessage]
	setStreamingMessageId(streamId)
	setStreamingChunks([])

	//3. streaming请求
	const controller = await sendStreamingMessage(
		userMessage,
		(chunk) => {
			//onChunk回调
			const updatedstage = ['md','text','header', 'finish', ...].includes(chunk.type)
			// 自己加状态区分要输出啥
			if(updatedstage){
				// 批量更新
				setPendingMessageUpdate({streamId, chunk})
			}
			setMessages(prev => 
				prev.map(msg => 
					msg.message_id === streamId 
					? {...msg, streamingChunks:[...(streamingChunks || []), chunk}] }
					: msg
			))
		},
		// onComplete - 结束stream
		() => {
			setStreamingMessageId(null)
			setStreamingController(null)
			setStreamingChunks([])
		},
		// error hanlder
		(error) => {
			...
		}
	)
	setStreamingController(controller)
	return 
}

fetch代码

js 复制代码
export const sendStreamingMessage = async(message, onChunk, onComplete, onError, cancelSignal) => {
try{
	const controller = new AbortController()
	//1. 判断是否需要 中断sse
	if(cancelSignal){
		cancelSignal.addEventListener('abort', 
			() => {
				if(controller){
					controller.abort()
				}
			}
		)
	}
	// 2. request
	const requestBody = {...}
	const header = {...}
	const response = await fetch(`url`, {
		method: 'post',
		header:header,
		body:requestBody,
		signal:controller.signal // key variable, bind with signal of controller
	})

	//3. read streaming and check if it is cancelled
	const reader = response.body.getReader()
	const decoder = new TextDecoder()
	let buffer = ''
	let chunkId = 0
	
	while(true){
		//输出前看是否取消
		if(controller.signal.aborted){
			if(reader){
				await reader.cancel('user cancel')
			}
		}
		throw new DOMException('cancelled by user', 'AbortError')
		
		const{done, value} = value reader.read() // 读取body的ReadableStream类
		if(done){ 
			if(onComplete) {onComplete()}
		 	break
	 	}
		buffer += decoder.decode(value, {stream:true})

		const lines = buffer.split('\n')
		buffer = lines.pop()
		for(const line of lines){
			//处理data逻辑略过
			
			chunk.id = chunkId++
			if(controller.signal.abort){throw)
			
			if(onChunk){
				onChunk(chunk)
			}
			
			if(chunk.type === 'error' ){
				if(onError){onError()}
			 	return controller
		 	}
		 	if(chunk.type === 'cancelled' ){
				if(onError){onError()}
			 	return controller
		 	}
		 	if(chunk.type === 'ok' ){
			 	break
		 	}
			

		}
	}	
} //忽略catch 
finally{
	if(reader) await reader.cancel()
	if(controller) controller.abort();
	return controller
}

}

前端取消sse的话

js 复制代码
const stop = () => {
	if(streamController && !steamController.signal.aborted){
		streamController.abort()
	}
}

后端

py 复制代码
@app.post("/api/streaming")
async def streaming(request:Request):
	async gen_sse_stream():
		try: 
			service = Service()
			async for chunk in service.stream(request.start):
				chunkData = chunk.model_dump()
			
				if chunkData.type == 'done':
					# save to database
				
				yield f'data: {json.dumps(chunkData}\n\n'
		except Exception as e:
			yiele f'data: {json.dumps({"type": "error", })}'
		
	return StreamingResponse(
		gen_sse_stream(),
		media_type='text/plain',
		header={'Connection':'keep-alive'}
	)

Service

py 复制代码
class Service:
	async def stream(self, start:int) -> AsyncGenerator[StreamChunk,None]:
		processor = CountProecessor()
		async for chunk in processor.count(start):
			yiled chunk

class CountProecessor:
	def __init__(self):
		self.chunk_id = 0
	def _create_chunk(self, chunk_type:str, content: str = '', data:dict=None):
		self.chunk_id += 1
		return StreamChunk(
			type = chunk_type,
			content = content,
			data = data or {},
			chunk_id = chunk_id
			timestamp = ...
		)

Chunk

py 复制代码
class StreamChunk():
	type:str,
	content:str,
	data:dict={},
	chunk_id:int,
	timestamp:str
相关推荐
徐同保3 小时前
react useState ts定义类型
前端·react.js·前端框架
liangshanbo12153 小时前
React 19 vs React 18全面对比
前端·javascript·react.js
望获linux3 小时前
【实时Linux实战系列】Linux 内核的实时组调度(Real-Time Group Scheduling)
java·linux·服务器·前端·数据库·人工智能·深度学习
Never_Satisfied3 小时前
在 JavaScript 中,删除数组中内容为xxx的元素
java·前端·javascript
_菜鸟果果3 小时前
Vue3+echarts 3d饼图
前端·javascript·echarts
Luffe船长4 小时前
前端vue2+js+springboot实现excle导入优化
前端·javascript·spring boot
Demoncode_y5 小时前
前端布局入门:flex、grid 及其他常用布局
前端·css·布局·flex·grid
明天最后5 小时前
使用 Service Worker 限制请求并发数
前端·service worker
无敌糖果5 小时前
FastAPI请求会话context上下文中间件
fastapi