uniapp在微信小程序中实现 SSE 流式响应

前言

最近需要使用uniapp开发一个智能对话页面,其中就需要使用SSE进行通信。

本文介绍下在uniapp中如何基于uni.request实现SSE流式处理。

在线体验

#小程序:yinuosnowball

SSE传输格式

返回输出的流式块:

  1. Content-Type为text/event-stream

  2. 每个流式块均为 data: 开头,块之间以 \n\n 即两个换行符分隔, 如下所示:
    在这里插入图片描述

  3. 后端接口定义的数据如下: - event为message,开始接收数据,answer为返回的结果 - event为message_end结束

在这里插入图片描述

接口数据已约定完成,下一步使用uniapp开始接收处理数据。

uniapp处理数据

客户端实现在微信小程序中接收 SSE 流式响应,需要以下步骤:

  1. 配置 HTTP 请求:设置适当的请求头和参数,以确保服务器返回流式响应。
  2. 处理分块数据:由于SSE是分块传输的,我们需要监听每个数据块,并解析它们。
  3. 错误处理:当每一次返回的最后出现不是完整的响应时,需要进行特殊处理。
  4. 完成时:可以进行追问等额外处理

下面使用 uni.request实现SSE的案例:

基本框架:

const requestTask = uni.request({url,method: 'POST',header: {Accept: 'text/event-stream',Authorization,},data,enableChunked: true,responseType: 'arraybuffer',success: (res) => {console.log('Data received 数据接受完毕:', res.data)},fail: (error) => {console.log('打印***error 错误处理', error)},complete: (complete) => {console.log('打印***complete 完成接收', complete)}
})requestTask.onChunkReceived((res)=>{// 处理数据
})

通过对requestTaskonChunkReceived监听就可以得到数据块,通过打印我们可以看到数据返回是ArrayBuffer,我们需要进行处理。

在这里插入图片描述

 const uint8Array = new Uint8Array(res.data);let text = String.fromCharCode.apply(null, uint8Array);

解析后得到以data:data:返回的格式,

此处需要注意解析后是data: 还是 data:data:格式

在这里插入图片描述

进一步处理:由于返回的数据块不是一段一段,而是很多段都返回,因此我们需要进行\n\n进行拆分,然后逐个解析:

在这里插入图片描述

const arr = text.split('\n\n').filter(Boolean)
arr.forEach(msg => {const jsonStr = msg.substring(11); // 去掉 'data:data: ' 前缀const data = JSON.parse(jsonStr);switch (data.event) {case 'message': {// 拼接返回文本this.dialogueList[existingMessageIndex].answer += data.answer;break;}case 'message_end':// 消息结束break;}
});

至此,我们就可以接收到消息,如果就这样那就最好,但对接的过程发现,每一次返回的文本最后一段不是完整的,导致解析出现失败,如下图所示:

在这里插入图片描述

解决方案是: 定义一个变量,当解析出现失败时,肯定是最后一段,进行存储,下一次接收到数据将上一次存储的进行拼接,然后解析:

在这里插入图片描述

具体代码:

// 每次发送存储数据
const msgObj = {query,answer: "",conversationId: null,isDone: false
}
this.dialogueList.push(msgObj)
// 保存上一次失败的text
let lastText = ''
requestTask.onChunkReceived((res) => {// 第一步:获取 字符串 数组const uint8Array = new Uint8Array(res.data);let text = lastText + String.fromCharCode.apply(null, uint8Array);lastText = '';let arr = text.split('\n\n').filter(Boolean)let lastIndex = arr.length - 1// 第二步:是否可以直接进行解析try {// 判断是否可以全部解析完成arr.every(item => JSON.parse(item.substring(11)))} catch (error) {// 如果报错截取最后一项lastText = arr[lastIndex]arr = arr.filter((_, i) => i !== lastIndex)}// 处理数据块if (arr.length) {try {arr.forEach(msg => {const jsonStr = msg.substring(11); // 去掉 'data: ' 前缀const data = JSON.parse(jsonStr);const existingMessageIndex = this.dialogueList.findIndex(item => item === msgObj);switch (data.event) {case 'message': {// 查找是否存在相同ID的消息this.dialogueList[existingMessageIndex].answer += data.answer;break;}case 'message_end':// 消息结束break;}});} catch (error) {console.error('解析数据失败:', error);}}
});

这样就完成了对不连续返回的错误处理。

如果需要直接结束请求,可以直接使用requestTask.abort()

完整代码

function sendMsg(query) {const msgObj = {query, // 问题answer: "", // 回答的结果conversationId: null,feedback: null,isDone: false // 自定义格式,用于加载处理}this.dialogueList.push(msgObj)// 请求参数const data = {}this.requestTask = uni.request({url,method: 'POST',header: {Accept: 'text/event-stream',Authorization: getStorage(tokenKeyEnum.zhonglv),},data,enableChunked: true,responseType: 'arraybuffer',success: (res) => {console.log('Data received 数据接受完毕:', res.data)},fail: (error) => {console.log('打印***error 错误处理', error)},complete: (complete) => { console.log('打印***complete 完成接收', complete)}})let lastText = ''this.requestTask.onChunkReceived((res) => {// 第一步:获取 字符串 数组const uint8Array = new Uint8Array(res.data);let text = lastText + String.fromCharCode.apply(null, uint8Array);lastText = '';let arr = text.split('\n\n').filter(Boolean)let lastIndex = arr.length - 1// 第二步:是否可以直接进行解析try {let isCanResolve = arr.every(item => JSON.parse(item.substring(11)))console.log('打印***isCanResolve', isCanResolve)} catch (error) {// 如果报错截取最后一项lastText = arr[lastIndex]arr = arr.filter((_, i) => i !== lastIndex)console.log('打印***error', error)}// 处理数据块if (arr.length) {try {arr.forEach(msg => {const jsonStr = msg.substring(11); // 去掉 'data:data: ' 前缀const data = JSON.parse(jsonStr);const existingMessageIndex = this.dialogueList.findIndex(item => item === msgObj);switch (data.event) {case 'message': {// 查找是否存在相同ID的消息this.dialogueList[existingMessageIndex].answer += data.answer;break;}case 'message_end':// 消息结束this.dialogueList[existingMessageIndex].isDone = true;break;}});} catch (error) {console.error('解析数据失败:', error);}}});
}

总结

最后总结一下,在uniapp中使用uni.request处理流式响应,主要步骤有:

  • 开启:enableChunked: true
  • 设置请求Header:Accept: 'text/event-stream'
  • 注册数据接收响应函数: requestTask.onChunkReceived(onChunkReceived)
  • 分块数据解析 String.fromCharCode
  • 处理不连续返回问题
  • 结束:requestTask.abort()

希望对你有所帮助,如有错误,请指正 O^O!


参考文档

  • uni.request文档: uniapp.dcloud.net.cn/api/request…

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:http://www.pswp.cn/news/922460.shtml
繁体地址,请注明出处:http://hk.pswp.cn/news/922460.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

STM32N6AI资料汇总

文章目录前言一、STM32N6硬件资源1.1 NUCLEO-N657X0-Q1.2 STM32N6570-DK1.3 正点原子STM32N647二、STM32N6软件资源2.1 STM32CubeN6例程资源包2.2 STM32图像信号处理器(ISP)调优软件2.3 正点原子N6开发板配套软件三、AI软件资源3.1 STM32N6 AI软件包总结…

Flask学习笔记(一)

1、环境准备pip install Flask使用Flask开发第1个入门程序:from flask import Flask app Flask(__name__) app.route(/) def hello_world():return Hello, World!if __name__ __main__:app.run()Flask构造函数将当前模块的名称(__name__)作为参数。2、route函数ap…

CSP认证练习题目推荐(4)

思维、贪心、综合 排队打水 这道题目不算难,但是不注意还是会出现很多错误,比如结构体的书写。以及自定义结构体排序。还有这里做的优化,使用前缀和记录打水的等待时间,但是这里很容易出错的点在于等待时间是应该是记录的前一个…

MySQL 视图的更新与删除:从操作规范到风险防控

MySQL 视图的更新与删除:从操作规范到风险防控 视图作为 “虚拟表”,其更新与删除操作常常让开发者困惑 ——“为什么更新视图会报错?”“删除视图会不会弄丢数据?” 实际上,80% 的视图操作问题都源于对 “视图依赖基表…

C 语言实现 I.MX6ULL 点灯(续上一篇)、SDK、deep及bsp工程管理

目录 一、汇编点灯转 C 语言实现 1. 关键字:volatile 2. 寄存器地址定义(两种方式) (1)直接宏定义地址 (2)结构体封装寄存器(优化访问) 3. 核心功能代码 &#xff…

DevOps实战(7) - 使用Arbess+GitPuk+sourcefare实现Node.js项目自动化部署

Arbess 是一款国产开源免费的 CI/CD 工具,工具支持一键部署,页面简洁易用。本文将详细介绍如何安装配置使用GitPuk、sourcefare、Arbess系统,使用流水线拉取GitPuk源码、使用sourcefare代码扫描、构建安装包并进行主机部署。 1、GitPuk 安装…

算法,蒜鸟蒜鸟-P1-理解“双指针”

欢迎来到啾啾的博客🐱。 记录学习点滴。分享工作思考和实用技巧,偶尔也分享一些杂谈💬。 有很多很多不足的地方,欢迎评论交流,感谢您的阅读和评论😄。 目录引言1 双指针:Two Pointers1.1 左右指…

使用cookiecutter创建python项目

一、关于Python项目结构Python 项目并没有完全统一的 “固定结构”,但行业内有一些广泛遵循的约定俗成的目录结构(尤其针对可分发的包或大型项目)。同时,确实有工具可以快速生成这些标准化结构,提高开发效率&#xff0…

台积电生态工程深度解析:从晶圆厂到蜂巢的系统架构迁移

当半导体巨头将工厂视为生态系统,用工程思维解决环境问题概述:生态系统的工程化再造台积电近日开展的"积蜜"项目绝非简单的企业CSR行为,而是一场将生态系统视为复杂系统进行工程化改造的技术实践。本文将从系统架构、数据监控、循环…

从零实现一个简易计算器

最近在刷算法题时,遇到了实现计算器的问题。一开始觉得很简单,但真正动手实现时才发现其中有很多细节需要考虑。今天就来分享一下我的实现思路和学到的经验。问题分析我们需要实现一个能够处理加减乘除四则运算的计算器,要正确处理运算符的优…

Actix-webRust Web框架入门教程

文章目录引言Actix-web是什么?准备工作你的第一个Actix-web应用理解代码结构处理请求和响应接收请求数据返回响应中间件 - 增强你的应用状态管理和依赖注入实用示例:构建RESTful API测试你的Actix-web应用部署Actix-web应用结语额外资源引言 嘿&#xf…

若依框架前端通过 nginx docker 镜像本地运行

1. 前言 项目运行过程图:对于前端项目通过命令 npm run build 打包后,无法直接运行。存在如下错误:可以通过配置 nginx 服务器运行前端项目解决如上问题。 2. Nginx 运行 采用 docker 镜像的方式运行,docker-compose.yml 文件内容…

浅聊一下HTTP协议

在日常上网浏览网页、刷视频时,背后都离不开 HTTP 协议的支持。作为 Web 世界的 “交通规则”,它负责服务器和客户端浏览器之间的数据传输。这篇文章就带大家全面了解 HTTP 协议,从基本概念到通信细节,再到安全相关的 HTTPS&#…

机器人控制器开发(定位——cartographer ros2 使用2)

文章总览 1 纯定位模式 当完成建图后,会生成pbstream格式的地图文件 配置纯定位模式的lua脚本 backpack_2d_localization.lua include "backpack_2d.lua"TRAJECTORY_BUILDER.pure_localization_trimmer {max_submaps_to_keep 3, } POSE_GRAPH.optimi…

《大数据之路1》笔记3:数据管理

一 元数据 1.1 元数据概述 定义: 元数据是关于数据的数据,元数据打通了源数据、数据仓库、数据应用,记录了数据从生产到消费的全部过程。元数据主要记录数据仓库中模型的定义、各层级间的映射关系、监控数据仓库的数据状态和ETL的任务运行状态…

排序实现java

排序算法概述Java中实现排序可以通过多种方式,包括内置方法、自定义算法或使用第三方库。常见的排序算法有冒泡排序、选择排序、插入排序、快速排序、归并排序等。使用Arrays.sort()方法对于数组排序,Java提供了Arrays.sort()方法,支持对基本…

51c大模型~合集182

我自己的原文哦~ https://blog.51cto.com/whaosoft/14174587 #LaV-CoT 超越GPT-4o,蚂蚁集团与南洋理工大学提出:首个语言感知的视觉思维链 随着大型视觉语言模型(VLM)的飞速发展,它们在处理复杂的视…

C++ STL之deque的使用和模拟实现

目录 deque 核心本质与定位 与stack和queue的关系: deque的使用 deque的底层实现 deque的原理介绍 deque的缺陷 总结: deque deque文档 : deque 翻译: 双端队列 deque(通常发音类似“deck”)是“double-ended queue”(双端队列&…

布草洗涤厂设备租赁押金原路退回系统—东方仙盟

设备租赁状态设备管理添加设备设备收押金设备退押金在布草洗涤行业的运营版图中,设备租赁是连接厂商与客户的重要纽带,而押金的收取与退还则是这一环节中关乎信任与效率的关键节点。未来之窗布草洗涤厂深谙此道,专为设备租赁业务打造的 “押金…

换源rocklinux和centos

一、Rockylinux换源,国外的源换成国内的源#nmcli connection modify ens33 ipv4.addresses 192.168.121.11 ipv4.gateway 192.168.121.2 ipv4.method manual ipv4.dns 114.114.114.114 connection.autoconnect yes修改地址#systemctl stop firewalld#systemctl diab…