流式响应 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
相关推荐
мо仙堡杠把子ご灬4 分钟前
Flex 自适应布局:告别 calc,用一个 height-auto 搞定
前端·javascript
环境栈笔记24 分钟前
指纹浏览器检查代理失败怎么排查?先分清代理、网络和检测服务
前端·人工智能·后端·自动化
ShineWinsu29 分钟前
对于HTML:HTML常见标签的解析—下
前端·css·面试·html·标签·表单·emmet
恋猫de小郭30 分钟前
Kotlin 2.5 的 Companion 改造,对 KMP 的重要升级
android·前端·flutter
前端双越老师44 分钟前
程序员 1v1 访谈系列第二期:很焦虑但又不知道干啥
前端·ai编程·全栈
广州华水科技1 小时前
北斗形变监测传感器在大坝安全监测中的应用与优势分析
前端
孔令飞1 小时前
如何学习Go
前端
HUIBUR科技1 小时前
AI 中枢,面向企业场景构建的 AI 智能体
前端·数据库·人工智能
随便写个软件1 小时前
【新版本】EasyExplorer资源管理器 3.12.0——地址栏重要更新
运维·服务器·前端·everything·资源管理器
Jack N1 小时前
【Strapi 系列之二】Collection / Single / Component 怎么选?Strapi 字段大全一篇讲透
前端·javascript·strapi