流式响应 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
相关推荐
J***Q29215 小时前
Vue数据可视化
前端·vue.js·信息可视化
ttod_qzstudio17 小时前
深入理解 Vue 3 的 h 函数:构建动态 UI 的利器
前端·vue.js
_大龄17 小时前
前端解析excel
前端·excel
一叶茶17 小时前
移动端平板打开的三种模式。
前端·javascript
前端大卫17 小时前
一文搞懂 Webpack 分包:async、initial 与 all 的区别【附源码】
前端
Want59518 小时前
HTML音乐圣诞树
前端·html
老前端的功夫18 小时前
前端浏览器缓存深度解析:从网络请求到极致性能优化
前端·javascript·网络·缓存·性能优化
Running_slave19 小时前
你应该了解的TCP滑窗
前端·网络协议·tcp/ip
程序员小寒19 小时前
前端高频面试题之CSS篇(一)
前端·css·面试·css3
颜酱19 小时前
Monorepo 架构以及工具选型、搭建
前端·javascript·node.js