《60天AI学习计划启动 | Day 32: 前端 AI 请求封装(SSE 流式 + 重试)》
Day 32:前端 AI 请求封装(SSE 流式 + 重试)
学习目标
- 封装 通用 SSE 流式请求 hook
- 实现 普通请求的重试工具(指数退避)
- 方便 以后任何 AI 接口直接复用
核心知识点(简记)
- SSE:
fetch + reader或EventSource,推荐统一封成 hook - 重试:指数退避
delay = base * 2^n,对超时/网络错用同一工具 - 分层:底层请求工具 → 上层
useChat、useImageQA直接调用
作业 1:通用 SSE 流式 hook
目标:给任意 url + payload 做文本流式读取,并通过回调输出内容
import {useRef,useState,useCallback} from 'react'
export interface SSEOptions {
url:string
onChunk?:(text:string)=>void
onDone?:(full:string)=>void
onError?:(err:Error)=>void
}
export function useSSE() {
const [loading,setLoading]=useState(false)
const abortRef=useRef<AbortController|null>(null)
const start=useCallback(async (opts:SSEOptions,payload:any)=>{
if(loading)return
setLoading(true)
const ctrl=new AbortController()
abortRef.current=ctrl
let full=''
try{
const res=await fetch(opts.url,{
method:'POST',
headers:{'Content-Type':'application/json'},
body:JSON.stringify(payload),
signal:ctrl.signal
})
if(!res.body)throw new Error('no body')
const reader=res.body.getReader()
const dec=new TextDecoder()
let done=false,buf=''
while(!done){
const {value,done:d}=await reader.read()
done=d
if(value){
buf+=dec.decode(value,{stream:true})
const parts=buf.split('\n\n')
buf=parts.pop()||''
for(const p of parts){
const line=p.trim()
if(!line.startsWith('data:'))continue
const dataStr=line.slice(5).trim()
if(!dataStr||dataStr==='[DONE]')continue
const {type,content,error}=JSON.parse(dataStr)
if(content){
full+=content
opts.onChunk?.(content)
}
if(type==='error'&&error){
throw new Error(error)
}
}
}
}
opts.onDone?.(full)
}catch(e:any){
if(e.name!=='AbortError'){
opts.onError?.(e)
}
}finally{
setLoading(false)
abortRef.current=null
}
},[loading])
const abort=useCallback(()=>{
abortRef.current?.abort()
},[])
return {start,abort,loading}
}
作业 2:通用重试请求工具(非流式)
目标:包装 fetchFn,自动重试 3 次(网络错/超时)
export async function requestWithRetry<T>(
fn:()=>Promise<T>,
maxRetries=3,
baseDelay=500
):Promise<T>{
let lastErr:unknown
for(let i=0;i<=maxRetries;i++){
try{
return await fn()
}catch(e:any){
lastErr=e
const msg=e?.message||''
const retryable=msg.includes('Network')||msg.includes('timeout')
if(i===maxRetries||!retryable)break
const delay=baseDelay*Math.pow(2,i)
await new Promise(r=>setTimeout(r,delay))
}
}
throw lastErr
}
// 使用示例
async function callAI(question:string){
return requestWithRetry(async()=>{
const res=await fetch('/api/chat',{
method:'POST',
headers:{'Content-Type':'application/json'},
body:JSON.stringify({question})
})
if(!res.ok)throw new Error('server error')
return res.json()
})
}
明日学习计划预告(Day 33)
- 主题:前端 AI 状态管理 & 缓存
- 内容方向:
- 用 Zustand/Redux/React Query 管理聊天会话 / 历史 / 多 Tab 同步
- 把最近 N 条会话做本地缓存(持久化 + 版本升级策略)

浙公网安备 33010602011771号