后端的同学已经做好了后端的SSE,并给出了我们选型的原因:报告。我就不再赘述,主要还是从前端的实现来阐述。
EventSource:
SSE(Server-Sent Events,服务器发送事件)是一种实现服务器到客户端单向实时通信的技术。它允许服务器在有新数据可用的任何时候向浏览器推送信息,而不需要浏览器不断地询问服务器是否有新数据。这是通过在客户端和服务器之间建立一个持久的连接来实现的,服务器通过这个连接推送数据。
EventSource 是一个JavaScript接口,它为服务器发送事件提供了一种简单的实现方式。使用 EventSource,开发者可以非常容易地创建一个连接到服务器的客户端,并监听服务器推送的事件。当服务器发送一个事件时,EventSource 对象会触发一个事件,开发者可以监听这个事件并相应地处理。
在SSE中,浏览发送一个请求给服务端,通过响应头中的Content-Type:text/event-stream等向客户端声名这是一个长连接,发送的是流数据,这样客户端就不会关闭连接,一直等待服务端发送数据。
注意,sse本身是不支持post的方式,可以通过fetch的方式完成post相关操作。
post方式
使用post方式实现的大概格式如下:
// 创建一个AbortController实例,用于在未来某个时刻取消fetch请求
const controller = new AbortController();
// 获取由AbortController实例控制的信号对象
const signal = controller.signal;
// 使用fetchEventSource发起一个到指定URL的POST请求,用于建立SSE连接
fetchEventSource(`${url}`, {
method: 'POST', // 请求方法为POST
signal: signal, // 将信号对象传递给请求,以便能够取消请求
headers: { // 设置请求头
'Content-Type': 'application/json', // 请求内容类型为JSON
},
body: JSON.stringify(message), // 请求体,将message对象转换为JSON字符串
onmessage(msg) { // 定义收到消息时的回调函数
// 当服务器发送消息时,这里会接收到消息并处理
},
one rror(err) { // 定义出错时的回调函数
throw err; // 如果发生错误,抛出这个错误
// 抛出错误将中断fetchEventSource的处理流程
}
});
onmessage回调函数不触发
具体实现时,我发现log的onmessage消息全部为空。但是使用apipost测试后端接口可以得到流式输出。
进一步查看network,我发现其中的response中有响应,格式如下:(因为返回的是字符串且默认解码方式为utf-8所以没有成功解码)
{"docs": ["<span style='color:red'>\u672a\u627e\u5230\u76f8\u5173\u6587\u6863,\u8be5\u56de\u7b54\u4e3a\u5927\u6a21\u578b\u81ea\u8eab\u80fd\u529b\u89e3\u7b54\uff01</span>"]}
{"text": "\u60a8\u597d", "message_id": "148994c7260445e9a23736808658b4fd"}
但是EventStream中全部为空。
使用onopen方法,控制台输出一切正常:
async onopen(response) {
console.log(response);
if (response.ok && response.headers.get('content-type') === 'text/event-stream') {
console.log(response);
return;
} else if (response.status >= 400 && response.status < 500 && response.status !== 429) {
// client-side errors are usually non-retriable:
console.log("回应错误")
} else {
// throw new RetriableError();
}
},
说明只有onmessage的实现有问题。
查阅资料,我发现原因是因为SSE
规定了schema
,如果不符合下面的格式,前端无法解析数据
SSE
规定的schema
event: message\n
data: {数据}\n\n
以上为纯字符串,同时message
也可以是其他消息,不会影响onmessage
回调函数。
可以发现后端的返回格式并不如我们所愿,更改后端返回的代码如下:
# event_data = json.dumps(data)
event_data = json.dumps({"data":data})
# yield f"{event_data}\n\n"
yield f"event: message\ndata: {event_data}\n\n"
重启后端,重新运行,将onmessage中的msg log出来,结果如下:
再查看EventStream,已经成功解析出来了:
聊天打字机输出
可以看到后端返回的消息中,其中的 Unicode字符未被正确转义,首先需要使用JSON.parse()
方法来解码它。
const parsedData = JSON.parse(msg.data);
查看data的格式,分为两种,一种里面为完整的docs文本,另一种是需要拼接的text文本。
event: message
data: {"data": {"docs": ["<span style='color:red'>\u672a\u627e\u5230\u76f8\u5173\u6587\u6863,\u8be5\u56de\u7b54\u4e3a\u5927\u6a21\u578b\u81ea\u8eab\u80fd\u529b\u89e3\u7b54\uff01</span>"]}}
event: message
data: {"data": {"text": "\u5468", "message_id": "d67aac633ac0456f9945d3d0c5f20f45"}}
对于docs,我直接将其添加到aichat中,但是注意要将其中的换行符切换为<br>
let aiCurrentChatDocs = JSON.stringify(parsedData.data.docs);
aiCurrentChatDocs = aiCurrentChatDocs.replace(/\n/g, '<br>');
// 将ai回复加入list
AIReplay('参考:'+ aiCurrentChatDocs);
对于text,我使用resultAnswer拼接目前为止得到的text的字符串。
然后找到msgList中最后一条ai的消息(也就是消息内容和resultAnswer相等的消息),如果没有的话,说明当前是接收到的第一个token,于是创建AIReplay;如果可以找到的话,将当前的lastAI的content直接更改为resultAnswer.value。
因为msg是reactive创建的,所以可以动态修改。
// 将ai回复加入list
let lastAI = msgList.find((msg) => msg.content === resultAnswer.value);
resultAnswer.value +=parsedData.data.text;
//如果没有回答就创建新的:
if(!lastAI){
AIReplay(resultAnswer.value);
}
else{
lastAI.content=resultAnswer.value;
}
标签:后端,text,response,打字机,SSE,message,data,event
From: https://blog.csdn.net/qq_38040571/article/details/141352461