vue项目中使用@microsoft/fetch-event-source库实现流式接口对接

项目中使用 fetch-event-source 实现调用大模型的流式接口,实现类似chatGPT流式输出效果

@microsoft/fetch-event-source 使用说明

github地址: fetch-event-source
MDN解释: event source

fetch-event-source提供了一个更好的 API,用于发出事件源请求( Event Source requests 也称为服务器发送事件),并具有Fetch API中提供的所有功能。

  • 基于 Fetch API 提供了一个用于使用服务器发送事件的备用接口,可以使用任何请求方法、头信息和请求体,以及 fetch() 提供的所有其他功能。
  • 如果连接中断或发生错误,可以控制重试

安装

npm install @microsoft/fetch-event-source

用法

import { fetchEventSource } from '@microsoft/fetch-event-source'

class RetriableError extends Error { }
class FatalError extends Error { }
const ctrl = new AbortController();
//AbortController 是一个用于控制和取消异步操作的接口。它通常与 AbortSignal 一起使用,后者是由 AbortController 生成的信号对象。AbortController 和 AbortSignal 主要用于取消 fetch 请求或其他需要取消的异步任务。

fetchEventSource('/api/sse', {
    method: 'POST',
    headers: {
        'Content-Type': 'application/json',
    },
    body: JSON.stringify({
        foo: 'bar'
    }),
    signal: ctrl.signal,//用于控制和取消 fetch 请求
    async onopen(response) {
        if (response.ok && response.headers.get('content-type') === EventStreamContentType) {
            return; // 一切正常
        } else if (response.status >= 400 && response.status < 500 && response.status !== 429) {
            // 客户端错误通常是不可重试的:
            throw new FatalError();
        } else {
            throw new RetriableError();
        }
    },
    onmessage(msg) {
        // 如果服务器发出错误消息,抛出异常
        // 以便它由下面的 onerror 回调处理:
        if (msg.event === 'FatalError') {
            throw new FatalError(msg.data);
        }
    },
    onclose() {
        // 如果服务器意外关闭连接,重试:
        throw new RetriableError();
    },
    onerror(err) {
        if (err instanceof FatalError) {
            throw err; 
        } else {
            // 什么都不做以自动重试。您也可以
            // 在这里返回一个特定的重试间隔。
        }
    }
});

兼容性

ES2017

vue项目示例

  1. 获取流式数据,拼接流式显示
  2. 自定义规则拼接并截取句子,调用语音接口播放
import { fetchEventSource } from '@microsoft/fetch-event-source'

const ctrl=new AbortController()
const textBuffer = ref(''); //文字缓冲区 
const reportHtml = ref('')
const eventSource =()=>{
  reportHtml.value=''
  fetchEventSource(`/xxx/openai/dataStream`, {
        method: 'POST',
        headers: {
            'Content-Type': 'application/json',
            'Authorization': `bearerToken`
        },
        body: JSON.stringify({}),//参数
        mode:'cors',
        signal:ctrl.signal,
        onmessage: (event) => {
          let res = event.data
          let content = res
          reportHtml.value = reportHtml.value+(res===null?'':res)
          if(content){
            textBuffer.value += content;
          }
          // 如果缓冲区中的文本达到一定长度 并有句号时,进行语音转换和播放 ,最大长度100时转换
          const textLen = 5
          const maxLen = 10
           if (textBuffer.value.length >= textLen
                && textBuffer.value.length <= maxLen 
                && (textBuffer.value.slice(textLen).indexOf('。') != -1 || textBuffer.value.slice(textLen).indexOf('!') != -1)  
                || (
                    textBuffer.value.length > maxLen 
                    && (textBuffer.value.slice(maxLen).indexOf(',') != -1 ||textBuffer.value.slice(maxLen).indexOf(',') != -1 || textBuffer.value.slice(maxLen).indexOf('.') != -1)
                )
            ) {
            // 分隔符
            let  delimiter = ''
            let baseText = ''
            let extraText = ''
            // 超出xxx
            if(textBuffer.value.length > maxLen){
              // 前 xx 个字符都要播报 
              baseText = textBuffer.value.slice(0,maxLen)
              extraText = textBuffer.value.slice(maxLen)
            }else if(textBuffer.value.length > textLen && textBuffer.value.length <= maxLen){ 
              baseText = textBuffer.value.slice(0,textLen)
              extraText = textBuffer.value.slice(textLen)
            }
            if(extraText.indexOf('。')>-1){
              delimiter = '。'
            }else if(extraText.indexOf('!')>-1){
              delimiter = '!'
            }else if(extraText.indexOf(',')>-1){
              delimiter = ','
            }else if(extraText.indexOf(',')>-1){
              delimiter = ','
            }
            else if(extraText.indexOf('.')>-1){
              delimiter = '.'
            }
            let splitText  = ''

            let leftText = ''
            if(delimiter){
              splitText = extraText.split(delimiter)[0]
              leftText = extraText.split(delimiter)[1]
            }
            let wholeText = baseText + splitText + delimiter
            const currentTime = new Date().toLocaleTimeString();
            emit('playAudio', { //调用播报
              text:wholeText,
              type:0
            })
            console.log(currentTime,wholeText,wholeText.length,'playAudio方法调用-----')
            textBuffer.value = leftText
          }
        },
        onerror: (error:any) => {
          props.itemData.isLoading=false
          throw error
        },
        onclose: () => {
          props.itemData.isLoading=false
          emit('loadingEnd')
          if (textBuffer.value) {
            console.log(textBuffer.value,'textBuffer.value - playing')
            emit('playAudio', {
              text:textBuffer.value,
              type:1 // 播放完了 需要缓存队列
            })
            textBuffer.value = '';
          }
        }
    })
}

接口截图

network

在这里插入图片描述
在这里插入图片描述

### Vue3 实现流式接口对接 在现代Web开发中,服务器发送事件(Server-Sent Events, SSE)是一种允许服务器向客户端推送实时更新的技术。通过SSE可以在不刷新页面的情况下获取来自服务器的数据流。 #### 使用 `fetchEventSource` 插件实现流式接口对接 为了简化处理逻辑,在Vue3项目里可以借助于微软提供的 `@microsoft/fetch-event-source` 来方便地管理与后端服务之间的连接以及数据传输过程[^2]。 安装依赖: ```bash npm install @microsoft/fetch-event-source ``` 配置入口文件(main.js): ```javascript import { createApp } from 'vue' import App from './App.vue' // 导入 fetchEventSource 函数用于发起请求 import { fetchEventSource } from '@microsoft/fetch-event-source' const app = createApp(App) app.config.globalProperties.$fetchEventSource = fetchEventSource app.mount('#app') ``` 创建组件 (StreamComponent.vue) 来展示聊天记录并模拟打字效果: ```html <template> <div class="chat-container"> <!-- 显示消息列表 --> <ul id="message-list"></ul> <!-- 用户输入框 --> <input type="text" v-model="newMessageText"/> <!-- 发送按钮触发新对话 --> <button @click="sendMessage">Send</button> </div> </template> <script setup lang="ts"> import { ref } from 'vue'; import axios from "axios"; let newMessageText = ref('') function sendMessage() { const url = '/api/chat'; // 假设这是API路径 $fetchEventSource(url, { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({ message: newMessageText.value }), onopen(response){ console.log(`Connection opened with status ${response.status}`); }, onmessage(event){ let data = event.data; if(data === "[DONE]") return; try{ data = JSON.parse(data); addMessageToUI(data); // 将收到的消息添加到界面中显示出来 } catch(e){ console.error("Failed to parse incoming data as JSON", e); } }, async onclose(){ console.warn('The connection was closed by the server.'); } }); } function addMessageToUI(messageObject){ var ul = document.getElementById("message-list"); setTimeout(() => { var li = document.createElement("li"); li.textContent += `${messageObject.content}`; ul.appendChild(li); // Simulate typing effect here... }, Math.random()*800 + 200); // 随机延迟时间以模仿真实场景下的响应速度差异 } </script> ``` 此示例展示了如何利用 `fetchEventSource` 方法建立持久化的HTTP连接,并监听从服务器传来的增量更新。每当有新的文本片段到达时就会被追加至当前视图底部形成连续滚动的效果。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值