创新实训(十) 项目开发——sse打字机效果前端实现

plalap 2024-10-01 09:03:11 阅读 94

后端的同学已经做好了后端的SSE,并给出了我们选型的原因:报告。我就不再赘述,主要还是从前端的实现来阐述。

EventSource

<code>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) { // 定义收到消息时的回调函数

// 当服务器发送消息时,这里会接收到消息并处理

},

onerror(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>"]}code>

{"text": "\u60a8\u597d", "message_id": "148994c7260445e9a23736808658b4fd"}

但是EventStream中全部为空。

查看响应标头,content-type没有问题:

image-20240603195141384

使用onopen方法,控制台输出一切正常:

<code>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出来,结果如下:

image-20240603200027638

再查看EventStream,已经成功解析出来了:

image-20240603200049358

聊天打字机输出

可以看到后端返回的消息中,其中的 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>"]}}code>

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;

}



声明

本文内容仅代表作者观点,或转载于其他网站,本站不以此文作为商业用途
如有涉及侵权,请联系本站进行删除
转载本站原创文章,请注明来源及作者。