《60天AI学习计划启动 | Day 32: 前端 AI 请求封装(SSE 流式 + 重试)》

Day 32:前端 AI 请求封装(SSE 流式 + 重试)

学习目标

  • 封装 通用 SSE 流式请求 hook
  • 实现 普通请求的重试工具(指数退避)
  • 方便 以后任何 AI 接口直接复用

核心知识点(简记)

  • SSE:fetch + readerEventSource,推荐统一封成 hook
  • 重试:指数退避 delay = base * 2^n,对超时/网络错用同一工具
  • 分层:底层请求工具 → 上层 useChatuseImageQA 直接调用

作业 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 条会话做本地缓存(持久化 + 版本升级策略)
posted @ 2025-12-17 09:55  XiaoZhengTou  阅读(2)  评论(0)    收藏  举报