流式响应 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
相关推荐
我命由我123453 小时前
CesiumJS 笔记 - 获取容器中心点、Cartesian3 clone 方法、修改 Cartesian3 对象的高度
前端·javascript·css·前端框架·html·html5·js
Hopebearer_4 小时前
页面突然只剩 DOM?一次静态资源版本错配排查
前端·部署
抱抱宝4 小时前
Agent-study项目教程(03):手写 Mini-ReAct Agent(不依赖框架)
javascript·人工智能·gpt·react.js·prompt·agent
东风破_4 小时前
ESLint 是什么?为什么你的项目需要它?
前端·后端·代码规范
Python私教6 小时前
API 输出模型怎么设计:从 model_dump() 到显式展示层
python·fastapi
圣殿骑士-Khtangc6 小时前
Go字符串高效拼接性能对比与底层原理分析
服务器·前端·golang
Python私教6 小时前
本地 AI 工具服务该绑定 127.0.0.1 还是 0.0.0.0?
python·fastapi
BigTopOne7 小时前
【ijkplayer】 硬解码流程
前端
kyriewen7 小时前
DeepSeek Harness开源第一天我就上手了——和Claude Code的差距比想象中大
前端·ai编程·deepseek
捡田螺的小男孩7 小时前
什么是 Skill?手把手带你写一个简单有用的 Skill!
前端·后端·程序员