流式响应 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
相关推荐
excel4 分钟前
Vue SFC 编译全景总结:从源文件到运行时组件的完整链路
前端
excel5 分钟前
Vue SFC 编译核心解析(第 5 篇)——AST 遍历与声明解析:walkDeclaration 系列函数详解
前端
elvinnn7 分钟前
提升页面质感:CSS 重复格子背景的实用技巧
前端·css
excel7 分钟前
Vue SFC 编译核心解析(第 7 篇)——最终组件导出与运行时代码结构
前端
excel9 分钟前
Vue SFC 编译核心解析(第 6 篇)——代码生成与 SourceMap 合并:从编译结果到调试追踪
前端
rising start9 分钟前
五、CSS盒子模型(下)
前端·javascript·css
excel14 分钟前
Vue SFC 编译核心解析(第 3 篇)——绑定分析与作用域推断
前端
excel16 分钟前
Vue SFC 编译核心解析(第 4 篇)——普通 <script> 与 <script setup> 的合并逻辑
前端
excel16 分钟前
Vue SFC 编译核心解析(第 1 篇)——compileScript 总体流程概览
前端
excel17 分钟前
Vue 编译器中的 processAwait 实现深度解析
前端