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