AI答疑,如何使用 fetch 获取接口返回的流式数据

最近进行了AI答疑相关的需求开发,总共进行了两次版本迭代,在此做个记录并分享前端的实现过程。

需求描述

前端进行提问,接口这边使用AI进行答疑,回答内容流式输出到前端。接口的 Response Headers 的 Content-Type 为 text/event-stream; charset=utf-8

第一个版本

前端实现

/** 获取答案 */
export const getAnswer = (params) => {
  return fetch('https://gpt.xxxx.com/chat',
    {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        Authorization: `Bearer ${token}`
      },
      body: JSON.stringify(params)
    }
  )
}


// 调用
getAnswer({
  question: '为啥?'
})
  .then(async response => {
    if (!response.ok) {
      throw new Error('Network response was not ok')
    }

    // 确保响应是可读流
    if (!response.body) {
      throw new Error('Response body is not available')
    }

    const reader = response.body.getReader()
    const textDecoder = new TextDecoder()
    let result = true
    let output = ''
    while (result) {
      const { done, value } = await reader.read()

      if (done) {
        console.log('Stream ended')
        result = false
        break
      }

      const chunkText = textDecoder.decode(value)
      output += chunkText
    }
    console.log('output:', output)
  })
  .catch(() => {
  })

为什么不用 axios:开始也尝试使用过 axios,设置 responseType: 'stream',但并没有实现流式输出。

此版本存在的缺陷:后端将前端的提问传给 chatgpt,等带 chatgpt 输入完后才流式输出给前端…

第二个版本

后端修改了第一个版本的不合理的流式输出,采用了 SSE 模式实现,不用等待 chatgpt 回答完成才输出内容。

简单介绍下SSE

SSE:Server-Sent Events 服务器推送事件,简称 SSE,是一种服务端实时主动向浏览器推送消息的技术。

​SSE 是 HTML5 中一个与通信相关的 API,主要由两部分组成:服务端与浏览器端的通信协议(HTTP 协议)及浏览器端可供 JavaScript 使用的 EventSource 对象。

从“服务端主动向浏览器实时推送消息”这一点来看,该 API 与 WebSockets API 有一些相似之处。但是,该 API 与 WebSockers API 的不同之处在于:

Server-Sent Events APIWebSockets API
基于 HTTP 协议基于 TCP 协议
单工,只能服务端单向发送消息全双工,可以同时发送和接收消息
轻量级,使用简单相对复杂
内置断线重连和消息追踪的功能不在协议范围内,需手动实现
文本或使用 Base64 编码和 gzip 压缩的二进制消息类型广泛
支持自定义事件类型不支持自定义事件类型
连接数 HTTP/1.1 6 个,HTTP/2 可协商(默认 100)连接数无限制

前端实现

/** 获取答案 */
export const getAnswer = (
  params: IAnswerParams,
  config?: {
    onopen?: (response: Response) => Promise<void>
    onmessage?: (e: EventSourceMessage) => void
    onerror?: (e: any) => void
    onclose?: () => void
  }
) => {
  let p = Object.assign({}, params, {
    env: envObj[import.meta.env.VITE_ENV]
  })
  removeAbort()
  window.abortControllerAi = new AbortController()
  // console.log(' --- AbortController signal --- ', window.abortControllerAi.signal)
  window.abortControllerAi.signal.addEventListener('abort', () => {
    console.log(' --- AbortController abort --- ')
    // 用户手动中止
  })
  return fetchEventSource(
    'https://gpt.xxxx.com/chat',
    {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        Authorization: `Bearer ${token}`
      },
      body: JSON.stringify(p),
      signal: window.abortControllerAi.signal,
      openWhenHidden: true, // 页面不可见时不中断链接, 这个一定要设置!!!
      ...config
    }
  )
}

/** 中断ai请求 */
export const abortAiRequest = () => {
  if (window.abortControllerAi) {
    window.abortControllerAi.abort()
    removeAbort()
  }
}

function removeAbort() {
  if (window.abortControllerAi) {
    window.abortControllerAi.signal.removeEventListener('abort', () => {})
  }
}

// 调用
let answer = ref('')
getAnswer(
  { question: '为啥?' },
  {
    onmessage(event) {
      // console.log(event.data)
      // event.data 为 [DONE] 代表结束
      let value: any = {}
      try {
        value = JSON.parse(event.data)
      } catch (e) {}
      // 回答内容
      let str = ''
      try {
        str = value.choices.map(i => i.delta.content).join('')
      } catch (e) {}
      answer.value += str
    },
    onclose() {
      console.log('close')
    },
    onerror(err) {
      console.log('err', err)
      throw new Error() // 中断链接
    }
  }
)

这里使用了 '@microsoft/fetch-event-source' 库,以解决 EventSource 的不足(不支持自定义请求头、能使用get等)。

JavaScript 中,当你向服务器发送 HTTP 请求以获取响应数据时,`responseType`属性是一个关键的参数,用于指定服务器应如何将响应内容返回给客户端。如果你想要实现流式下载(即按需接收数据,而不是一次性加载整个文件),你可以设置 `responseType` 为 "arraybuffer"、"blob" 或 "stream",但请注意并非所有浏览器都支持所有的响应类型。 以下是使用不同 `responseType` 实现流式下载的例子: 1. **arraybuffer**: ```javascript const xhr = new XMLHttpRequest(); xhr.open('GET', 'your-url-here'); xhr.responseType = 'arraybuffer'; xhr.onload = function() { if (xhr.status === 200) { const arrayBuffer = xhr.response; // 数据作为ArrayBuffer // 接着你需要创建一个Blob或DataView来进一步处理这个数组缓冲区 } }; xhr.send(); ``` 2. **blob**: ```javascript xhr.responseType = 'blob'; // ...同上,加载完成后转为Blob对象 ``` 3. **stream (不常见支持)**: ```javascript xhr.responseType = 'blob'; // 因为XMLHttpRequest本身不支持Node.js-style streams, 需要借助fetch API或其他库如axios fetch('your-url-here') .then(response => response.blob()) .then(blob => { /* 处理blob */} ``` 注意:`fetch` 返回的是一个Promise,可以直接转化为Stream,然后处理流数据。 在实际应用中,由于浏览器兼容性和性能考虑,通常推荐使用 `blob` 或 `arraybuffer`,它们更容易与其他库配合处理二进制数据。至于 `stream`,更适合Node.js环境下的网络请求。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值