SpringAI打造自己的AI应用流式响应(前端篇)

继上一篇文章SpringAI打造自己的AI应用(入门篇),我们已经使用SpringAI完成了项目搭建,并完成了模型调用接口,接下来我们开始使用前端来实现调用接口。

1.环境装备

在开始项目之前我们先装备如下环境

- 1. Node.js 16或更高

- 2. VUE3.x

⚠️:后台需要跨域,前端使用devServer配置代理的话会影响流式效果。

2.功能实现与分析(主要是流式输出打字风格的实现)

方案1: SSE:

Server-Sent Events(SSE)是一种用于在Web应用程序中从服务器向浏览器推送实时更新的技术。与WebSockets相比,SSE是单向的,意味着数据流只能从服务器发送到客户端,而不是双向通信。因为AI对话刚好需要服务器推送到客户端的场景,所及这个方案比较适合。对于前端来说,使用SSE一般使用自带的EventSource或者微软提供的@microsoft/fetch-event-source库来实现。

EventSource 说明:
优点:
  • 简单易用,与 HTTP 协议兼容。
  • 只需要一个长连接,服务器可以推送任意数量的事件。
  • 适用于服务端向客户端发送频率较低的数据。
  • 可以自动重连,并且在连接断开时会触发 error 和 close 事件,方便处理异常情况。
缺点:
  • 不支持双向通信。
  • 不支持二进制数据传输。
  • 兼容性存在问题,不支持 IE 浏览器。
  • 不支持POST请求

使用示例

ini 复制代码
let source = new EventSource("url");  
 
source.onmessage = function (event) {
     //收到响应数据
}

⚠️:这里需要注意使用EventSource 需要我们后台接口返回一个结束标识符号,需要在前端判断结束断开,不然就是重复请求。 后台接口加标识比如:[done]

less 复制代码
@CrossOrigin  
@GetMapping(value = "/api/stream")  
public Flux<String> stream(String prompt){  
  
  
final Flux<String> content = ollamaChatModel.stream(prompt);  
  
return content.concatWith(Flux.just("[end]"));
 }

前端改造后

ini 复制代码
let source = new EventSource("url");  
 
source.onmessage = function (event) {
     //收到响应数据
     
     //收到结束标识主动断开
     if(event.data==='[done]'){
      
      source.close()
     }
}

fetch-event-source 微软提供的封装库:

优点:
  • 简化使用:fetch-event-source 封装了 SSE 的复杂性,提供了更简洁的 API 进行连接和处理。
  • 自动重连:库内置了自动重连机制,方便开发者处理网络不稳定的问题。
  • 灵活性:支持自定义事件和消息处理,适应不同应用场景。
  • 轻量级:相比 WebSocket,SSE 使用更少的资源和带宽,适合轻量级实时数据更新。
  • 兼容性:与标准 EventSource API 兼容,易于集成到现有项目中。
  • 支持POST

缺点:

  • 单向通信:和标准 SSE 一样,只能从服务器向客户端发送数据,不支持双向通信。
  • 仅支持文本数据:无法原生处理二进制数据,需要进行编码转换(如 Base64)。
  • 连接限制:浏览器对同一源的并发 SSE 连接数有限制(通常为 6 个)。
  • 跨域限制:需要正确配置 CORS 才能跨域使用。
  • 浏览器兼容性:依赖浏览器支持 EventSource,在不支持的浏览器上需要 polyfill。

安装: npm install @microsoft/fetch-event-source --save

示例代码

vbnet 复制代码
let data = new FormData()  
data.append("query", "输入的问题")  
fetchEventSource("url", {  
method:"POST",   
body: data,  
onmessage:(event=>{  
//结果
console.log(event)  
 
})  
})

方案2: fetch

fetch 是现代 JavaScript 中用于执行网络请求的内置 API,提供了一种简单、灵活且基于 Promise 的方式来替代传统的 XMLHttpRequest。它是现代 Web 应用开发中非常重要的工具。

优点
  • 现代化:fetch 是现代浏览器的标准 API,语法简洁。
  • Promise 支持:基于 Promise,方便链式处理异步操作。
  • 灵活性:支持各种 HTTP 方法和请求设置。
缺点
  • 不支持超时:原生不支持请求超时,需要自行实现。
  • 不支持上传进度监听。
  • 错误处理复杂:只对网络错误抛出异常,HTTP 错误需手动检查响应状态。
  • 浏览器兼容性:在较老的浏览器中需要 polyfill。

示例代码

javascript 复制代码
let response = await fetch("url")  
 
const reader = response.body.getReader();  
  
// 读取流数据  
const decoder = new TextDecoder('utf-8'); // 用于将 Uint8Array 转换为字符串  
let done, value;  
while ({ done, value } = await reader.read()) {  
   
const chunk = decoder.decode(value, { stream: true });  
  //获取流式数据
 console.log(chunk)
if(done){  
break  
}  
  
}

方案3 axios

Axios 是一个基于 Promise 的 JavaScript 库,用于在浏览器和 Node.js 中进行 HTTP 请求。它是一个简洁且易于使用的工具,被广泛应用于前端开发中,用于与服务器进行数据交互,使用Vue生态对这个就不陌生了。 网上搜索了很多实现流都是在node.js环境中,在前端使用response.data.on监听是不行的,那么还有什么办法实现了,可以使用下载进度条监听回调来实现。

安装: npm install axios --save

示列代码

csharp 复制代码
axios.get("url", {  
 onDownloadProgress:(e=>{  
      //流式响应结果
     console.log( e.event.target.responseText )
 })  
 });

虽然也可以实现类似效果,但体验不是那么好,返回的内容不是一个一个来的,而是自动拼接成一个结果的,如:

 你
 你好
 你好吗
 你好吗?

方案4 Websocket

WebSocket 是一种在单个 TCP 连接上提供全双工通信的协议,它使得客户端和服务器之间进行实时交互变得更加容易。它是一种标准化的通信协议,客户端和服务器都可以通过它发送消息。

优点:

  • 支持双向通信,客户端和服务端都可以发送和接收消息;
  • 可以发送二进制数据,支持大文件传输;
  • 协议比较轻量级,能够节省网络带宽和服务器资源;
  • 兼容性较好,大部分现代浏览器都支持 WebSocket。

缺点:

  • 需要在服务端实现 WebSocket 协议的支持;
  • 相对于 HTTP 请求来说,WebSocket 连接需要占用更多的服务端资源;
  • 安全性问题:需要注意防止 CSRF 和 XSS 攻击,避免恶意用户利用 WebSocket 劫持会话或注入脚本等。

这种方案会比较麻烦,需要后端实现websocket服务器,然后ai调用返回数据后使用后台给前端websocket推送数据,这种代码网上很多我就不去实现了。

完整演示代码

xml 复制代码
<script setup>  
import { ref } from "vue";  
import axios from "axios";  
  
import { fetchEventSource } from "@microsoft/fetch-event-source";  
  
const message = ref("");  
const result = ref("")  
  
  
function send(){  
  
result.value = "思考中...."  
  
// useFetch()  
  
  
useMFetch()  
  
}  
  
  
function useMFetch(){  
  
let data = new FormData()  
data.append("query", message.value)  
fetchEventSource("http://localhost:8050/api/responseStream", {  
method:"POST",  
mode:'cors',  
body: data,  
onmessage:(event=>{  
console.log(event)  
result.value += event.data  
})  
})  
  
  
}  
  
function useAxios(){  
  
axios.get("http://localhost:8050/api/stream?conversationId=1&query=" + message.value, {  
onDownloadProgress:(e=>{  
  
result.value = e.event.target.responseText  
  
})  
});  
  
  
}  
  
/* eslint-disable */  
async function useFetch(){  
  
let response = await fetch("http://localhost:8050/api/stream?conversationId=1&query=" + message.value)  
result.value=''  
message.value=''  
const reader = response.body.getReader();  
  
// 读取流数据  
const decoder = new TextDecoder('utf-8'); // 用于将 Uint8Array 转换为字符串  
let done, value;  
while ({ done, value } = await reader.read()) {  
// value 是一个 Uint8Array,表示数据  
const chunk = decoder.decode(value, { stream: true });  
  
result.value +=chunk  
if(done){  
break  
}  
  
}  
  
}  
  
/* eslint-disable */  
function sse(){  
let source = new EventSource("http://localhost:8050/api/stream?conversationId=1&query=" + message.value);  
message.value = ''  
source.onmessage = function (event) {  
  
if(event.data === "[end]"){  
console.log('结束')  
source.close()  
}else{  
if(result.value==='思考中....'){  
result.value = ''  
}  
result.value += event.data  
  
}  
  
}  
}  
  
  
  
</script>  
  
<template>  
  
<div style="padding: 50px">  
<div id="content">  
{{result}}  
</div>  
<div style="display: flex;gap: 10px;margin-top: 200px;">  
<input type="text" v-model="message" style="width: 100%" />  
<button @click="send" style="flex-shrink: 0">发送</button>  
</div>  
</div>  
</template>  
  
<style scoped>  
  
</style>

3.总结

通过本文,我们了解了如何实现流式响应效果以及常用的方案有哪些优缺点等。看似简单但必须要自己去实现了,才能理解里面的坑以及各种技术的优缺点。希望这篇文章可以帮到有需要的人吧,有什么不好的地方欢迎大家评判指正。

相关推荐
流烟默1 小时前
vue和微信小程序处理markdown格式数据
前端·vue.js·微信小程序
梨落秋溪、1 小时前
输入框元素覆盖冲突
java·服务器·前端
菲力蒲LY1 小时前
vue 手写分页
前端·javascript·vue.js
天下皆白_唯我独黑2 小时前
npm 安装扩展遇到证书失效解决方案
前端·npm·node.js
~欸嘿2 小时前
Could not download npm for node v14.21.3(nvm无法下载节点v14.21.3的npm)
前端·npm·node.js
化作繁星2 小时前
React 高阶组件的优缺点
前端·javascript·react.js
zpjing~.~3 小时前
vue 父组件和子组件中v-model和props的使用和区别
前端·javascript·vue.js
做一颗卷心菜3 小时前
Promise
开发语言·前端·javascript
bin91533 小时前
DeepSeek 助力 Vue 开发:打造丝滑的 键盘快捷键(Keyboard Shortcuts)
前端·javascript·vue.js·计算机外设·ecmascript·deepseek
格式化小拓3 小时前
在vue2中操作数组,如何保证其视图的响应式
前端·javascript·vue.js