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

本文记录AI答疑需求开发的前端实现过程,进行了两次版本迭代。第一个版本存在缺陷,后端在第二个版本采用SSE模式。介绍了TextDecoder、SSE等技术,对比了EventSource与@microsoft/fetch - event - source,还提及了AbortController相关内容。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

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

1.需求描述

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

2.第一个版本

前端实现

/** 获取答案 */
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 输入完后才流式输出给前端…

TextDecoder

TextDecoder 是 JavaScript 中用于将字节流(通常是 Uint8Array 或 ArrayBuffer)解码为字符串的 API。

核心功能

  1. 字节到字符串的转换​​:
  • 将二进制数据(如网络传输的原始字节)转换为可读的字符串
  • 支持多种编码格式(UTF-8、ISO-8859-2 等)
  1. 流式处理​​:
  • ​可以分段解码大数据流,避免内存问题
  • 自动处理跨分块的字符(如一个多字节字符被分割在两个数据块中)

3.第二个版本

后端修改了第一个版本的不合理的流式输出,采用了 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 压缩再转 Base64 字符串)类型广泛(二进制、文本)
支持自定义事件类型不支持自定义事件类型
连接数 HTTP/1.1 6 个,HTTP/2 可协商(默认 100)连接数无限制

心跳检测

心跳检测是一种用于维持和监控网络连接状态的机制,通常用于长连接(如 WebSocket、TCP 连接),以确保通信双方仍然在线,并及时发现连接异常。它的核心原理是定期发送小型数据包(心跳包),如果对方未响应,则认为连接已断开。

自定义事件类型

SSE 协议允许服务器发送带有 ​​自定义事件名​​ 的消息,客户端可以监听特定事件。
示例:

event: userUpdate
data: {"id": 123, "name": "Alice"}
retry: 15000

客户端监听:

const eventSource = new EventSource("/updates");
eventSource.addEventListener("userUpdate", (e) => {
  console.log("User updated:", JSON.parse(e.data));
});

用途:

  • 分类推送不同业务逻辑的消息(如订单状态变更、通知、日志)。
  • 避免客户端解析 data 内容来区分消息类型。

前端实现

/** 获取答案 */
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,
      // true: 页面不可见时保持链接
      // false: 中断链接, 切回页面或app时再重新发送请求(会导致再次发起请求), 重新建立连接
      openWhenHidden: isApp ? false : 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等)。

4.EventSource 与 @microsoft/fetch-event-source 的比较

EventSource 是浏览器原生实现的 Server-Sent Events (SSE) 客户端 API,而 @microsoft/fetch-event-source 是一个基于 Fetch API 的 SSE 客户端库。

以下是 EventSource 相对于 @microsoft/fetch-event-source 的主要不足:

1. 功能限制
  • 缺乏请求自定义能力:原生 EventSource 不支持自定义请求头、请求方法或请求体
  • 无认证支持:无法轻松添加认证头信息(如 Bearer token)
  • 仅支持 GET 请求:不能使用其他 HTTP 方法
2. 错误处理不足
  • 有限的错误恢复:重连机制较为简单,缺乏精细控制
  • 无重试策略定制:无法自定义重试延迟、最大重试次数等
3. 连接控制
  • 无法手动关闭并重新连接:原生 EventSource 一旦关闭就不能重新打开
  • 缺乏连接状态管理:难以获取详细的连接状态信息
4. 数据格式限制
  • 仅支持文本数据:无法直接处理二进制数据
  • 固定的事件格式:必须遵循 event: name + data: ... 的严格格式
5. 浏览器兼容性
  • 不支持所有现代功能:如中止信号(AbortSignal)等新特性
  • 部分浏览器实现不一致:不同浏览器可能有微小行为差异
6. 缺乏扩展性
  • 无法中间件或拦截器:不能在请求/响应链中插入自定义逻辑
  • 无进度事件:无法监听连接建立进度

5.AbortController

AbortController 接口表示一个控制器对象,允许你根据需要中止一个或多个 Web 请求。

你可以使用 AbortController() 构造函数创建一个新的 AbortController 对象。使用 AbortSignal 对象可以完成与异步操作的通信。

AbortSignal 继承了其父接口 EventTarget 的属性,所以可以使用 addEventListener()事件监听器。

AbortController

AbortController.signal
返回一个 AbortSignal 对象实例,可以用它来和异步操作进行通信或者中止这个操作。

AbortController.abort()
中止一个尚未完成的异步操作。这能够中止 fetch 请求及任何响应体和流的使用

AbortSignal

AbortSignal.aborted
一个 Boolean,表示与之通信的请求是否被中止(true)或未中止(false)。

AbortSignal.reason
一旦信号被中止,提供一个使用 JavaScript 值表示的中止原因。

AbortSignal.abort()
返回一个已经被设置为中止的 AbortSignal 实例。

AbortSignal.any()
返回一个在任意给定的中止信号时中止时中止的 AbortSignal 实例。

AbortSignal.timeout()
返回一个在指定时间后自动中止的 AbortSignal 实例。

<think>嗯,用户问的是在Vue3项目中使用fetch获取接口返回的连续字节流。首先,我需要回忆一下在Vue中如何处理流数据,特别是使用fetch的情况。 首先,用户可能需要了解如何发起请求,正确设置fetch的参数。比如,GET方法可能不需要设置,但如果是POST,可能需要配置method。然后,需要确认接口返回的是流数据,比如二进制内容,这时候响应体应该是ReadableStream对象。 接下来,处理响应体的时候,可能需要使用getReader()来获取读取器,然后循环读取数据块。这里要注意异步操作,可能需要使用while循环和read()方法,直到done为true。每个chunk都是Uint8Array,可能需要处理或拼接这些数据。 然后,考虑到Vue3的组合式API,用户可能在setup或者script setup中使用,所以需要示例代码展示如何在组件中正确使用异步函数和响应式数据。比如,用ref来存储接收到的数据,或者实时更新进度。 另外,要注意可能的错误处理,比如网络问题或者流读取中的异常,所以需要try...catch块来包裹整个逻辑,并在catch中处理错误,可能更新一个错误状态给用户界面。 还有,用户可能需要了解如何将接收到的字节流转换为有用的数据。比如,如果是文本,可以用TextDecoder;如果是文件,可能需要创建Blob或Object URL。这部分需要根据具体场景给出不同的处理方式。 另外,性能方面,连续接收字节流可能需要分块处理,避免内存占用过大。比如,在接收到每个chunk时即时处理,而不是累积全部数据再处理。这在处理大文件或实时数据时很重要。 还有,是否需要设置请求头,比如Content-Type或Accept,确保服务器正确返回数据。比如,可能需要设置'Accept': 'application/octet-stream'或者其他MIME类型。 最后,给出一个完整的示例组件,展示从发起请求、读取流、处理数据到展示结果和错误的整个流程。这样用户可以直接参考代码结构,应用到自己的项目中。 总结一下,步骤大概是:使用fetch发起请求,获取响应流,用reader读取chunk,处理每个chunk,处理完成和错误情况,最后在Vue组件中整合这些逻辑,并处理好响应式数据和UI更新。</think>在 Vue3 项目中通过 `fetch` 获取连续字节流的实现步骤如下: --- ### 一、核心原理 1. **流式响应**:当接口返回 `Content-Type: application/octet-stream` 或其他流式数据时,响应体会以 `ReadableStream` 形式存在 2. **分块读取**:通过 `response.body.getReader()` 创建读取器,循环获取数据块 --- ### 二、实现步骤 #### 1. 发起 fetch 请求 ```javascript const response = await fetch('你的接口地址', { method: 'GET', // 根据接口要求修改 headers: { 'Content-Type': 'application/octet-stream' } }); ``` #### 2. 创建流读取器 ```javascript const reader = response.body.getReader(); ``` #### 3. 循环读取数据 ```javascript let chunks = []; while(true) { const { done, value } = await reader.read(); if(done) break; // value 是 Uint8Array 类型的字节块 chunks.push(value); // 实时处理逻辑(示例) console.log('收到数据块:', value); } ``` #### 4. 合并最终数据 ```javascript const totalLength = chunks.reduce((acc, chunk) => acc + chunk.length, 0); const finalBuffer = new Uint8Array(totalLength); let offset = 0; for(const chunk of chunks) { finalBuffer.set(chunk, offset); offset += chunk.length; } ``` --- ### 三、Vue3 组件示例 ```vue <script setup> import { ref } from 'vue'; const receivedData = ref(null); const isLoading = ref(false); const error = ref(null); const fetchStream = async () => { try { isLoading.value = true; const response = await fetch('your-api-endpoint'); if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`); const reader = response.body.getReader(); const chunks = []; while(true) { const { done, value } = await reader.read(); if(done) break; chunks.push(value); // 实时处理示例:显示进度 console.log(`已接收 ${chunks.length} 个数据块`); } // 合并数据 const totalLength = chunks.reduce((acc, chunk) => acc + chunk.length, 0); const finalBuffer = new Uint8Array(totalLength); let offset = 0; for(const chunk of chunks) { finalBuffer.set(chunk, offset); offset += chunk.length; } // 转换为需要的格式(示例:文本) const textDecoder = new TextDecoder('utf-8'); receivedData.value = textDecoder.decode(finalBuffer); } catch (err) { error.value = err.message; } finally { isLoading.value = false; } }; </script> <template> <div> <button @click="fetchStream" :disabled="isLoading"> {{ isLoading ? '接收中...' : '获取数据' }} </button> <div v-if="error" class="error">{{ error }}</div> <pre v-if="receivedData">{{ receivedData }}</pre> </div> </template> ``` --- ### 四、关键注意事项 1. **内存管理**:大文件建议分块处理而非全量存储 2. **实时处理**:可在 `while` 循环内直接处理每个数据块 3. **错误处理**:必须包含网络错误和流读取错误的捕获 4. **数据类型转换**: - 文本数据使用 `TextDecoder` - 二进制文件:使用 `Blob` 或 `File` 对象 ```javascript const blob = new Blob(chunks, { type: 'application/octet-stream' }); const url = URL.createObjectURL(blob); ``` --- ### 五、适用场景 1. 大文件下载 2. 实时日志流 3. 音视频流处理 4. 机器学习模型分块加载 通过这种方式可以实现高效的内存管理和实时数据处理能力,特别适合处理大规模或持续传输的二进制数据
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值