当前位置: 首页 > news >正文

基于 SSE 和分块传输的 Uniapp 微信小程序 实现 流式传输 对话

最近的项目是做微信小程序的一个对话框,接入DeepSeek,实现实时对话一个功能。
主要用到的技术点为:

1. Server-Sent Events (SSE) 技术:
在请求头中设置了 ‘X-DashScope-SSE’: ‘enable’,启用了SSE协议
服务器以事件流(event-stream)格式持续推送数据
通过 onChunkReceived 监听分块数据到达事件

2. 分块传输编码:
设置 enableChunked: true 启用分块传输
使用 Uint8Array 处理二进制数据流
通过 String.fromCharCode.apply 进行二进制到字符串的转换

3. 实时渲染机制:
使用响应式数据绑定(Vue的数据驱动机制)
通过直接修改 messages 数组的 text 属性实现渐进式渲染
配合 scrollToBottom 实现自动滚动

4. 异常处理机制:
通过 currentRequestTask.abort() 实现请求中断

代码展示:

<template><view class="chat-container"><view class="chat-box"><scroll-viewclass="chat-msg"scroll-yid="chatMessages"enable-flex@scrolltolower="scrollToBottom":scroll-top="scrollTop"scroll-with-animation="true"><!-- 问答区域 --><viewv-for="(message, index) in messages":key="index":class="['message', message.type]"><view v-if="message.type === 'user'" class="message user"><view class="message-content"><rich-text :nodes="message.text"></rich-text></view></view><view v-if="message.type === 'ai'" class="message ai"><view class="message-content"><text style="user-select: text;">{{getDisplayText(message)}}</text></view></view></view></scroll-view><!-- 发送按钮区域 --><view class="input-container"><input type="text" name="" v-model="inputText" class="input-search" /><button v-if="isSending" @click="cancelRequest" class="send-btn">停止生成</button><button v-else type="primary" @click="sendMessage" class="send-btn">发送</button></view></view></view>
</template><script>
import store from '@/store/index.js'
export default {data() {return {scrollTop: 0,messages: [], // 存储对话记录inputText: '',typingMessage: null,typingIndex: 0,fullText: '',isSending: false, // 发送按钮添加防抖sessionId: '', // 添加session_id,currentRequestTask: null, // 当前请求任务isAborted: false, // 是否手动取消retryCount: 0, // 重试计数器}},methods: {scrollToBottom() {// console.log("底部")this.$nextTick(() => {const query = uni.createSelectorQuery().in(this)query.select('#chatMessages').fields({id: true,dataset: true,rect: true, // 获取布局信息size: true, // 获取宽高scrollOffset: true, // 获取滚动信息scrollHeight: true,},(res) => {// console.log('完整节点信息:', res)if (res && res.scrollHeight) {this.scrollTop = res.scrollHeight// console.log('设置成功 scrollTop:', this.scrollTop)} else {console.warn('未获取到有效滚动信息', res)}}).exec()})},// 发送按钮async sendMessage() {if (this.isSending) returnif (this.inputText.trim() === '') {uni.showToast({title: '你没有输入消息呢',icon: 'error',duration: 1000,})return}this.messages.push({text: this.inputText,type: 'user',timestamp: new Date().getTime(),})this.$nextTick(() => {this.scrollToBottom()this.inputText = ''})try {// 清理前次请求if (this.currentRequestTask) {this.currentRequestTask.abort()this.currentRequestTask = null}this.isSending = truethis.isAborted = falsethis.retryCount = 0const parseSSEData = (rawStr) => {return rawStr.split('\n').filter((line) => line.startsWith('data:')).map((line) => JSON.parse(line.replace('data:', '').trim()))}this.currentRequestTask = uni.request({url: 'https://aaa.aliyuncs.com/api/v3/apps/urls',method: 'POST',// responseType: 'arraybuffer',enableChunked: true,data: {input: {prompt: this.inputText,session_id: this.sessionId,},parameters: {},debug: {},},header: {Authorization: 'Bearer 4b938314cc9c','Content-Type': 'application/json','X-DashScope-SSE': 'enable',Cookie: '666573c065f8c8ff52cda1c',},fail: this.handleRequestError,})const aiMessage = {text: '',type: 'ai',timestamp: new Date().getTime(),fullText: '',isFinishReason: null,}this.messages.push(aiMessage)store.commit('UPDATE_CURRENT_SESSION', this.messages)this.currentRequestTask.onChunkReceived((chunk) => {if (this.isAborted) returnconst uint8Array = new Uint8Array(chunk.data)let text = String.fromCharCode.apply(null, uint8Array)text = decodeURIComponent(escape(text))// console.log(parseSSEData(text)?.[0]?.output?.text)const chunkText = parseSSEData(text)?.[0]?.output?.textconst finishReason = parseSSEData(text)?.[0]?.output?.finish_reasonthis.sessionId = parseSSEData(text)?.[0]?.output?.session_idthis.messages[this.messages?.length - 1].text = chunkTextthis.messages[this.messages?.length - 1].fullText = chunkTextthis.messages[this.messages?.length - 1].isFinishReason = finishReasonif (finishReason === 'stop') {this.isSending = false}})} catch (error) {console.error('Error sending message:', error)uni.showToast({title: '生成失败, 请重试',icon: 'success',})this.isSending = false} finally {}this.inputText = ''},// 取消请求方法cancelRequest() {if (this.currentRequestTask) {this.isAborted = truetry {this.currentRequestTask.abort()} catch (e) {console.warn('中止请求时发生异常:', e)}this.currentRequestTask = null// 更新最后一条消息const lastIndex = this.messages.length - 1if (lastIndex >= 0 && this.messages[lastIndex].type === 'ai') {this.$set(this.messages, lastIndex, {...this.messages[lastIndex],text: '生成已中止',fullText: '生成已中止',isFinishReason: 'stop',})}store.commit('UPDATE_CURRENT_SESSION', this.messages)this.resetRequestState()}},// 错误处理handleRequestError(error) {if (this.isAborted) returnconsole.error('请求错误:', error)uni.showToast({title: this.retryCount < 2 ? '请求失败,正在重试...' : '服务暂时不可用',icon: 'none',})if (this.retryCount < 2) {this.retryCount++setTimeout(() => this.executeRequest(), 1000)} else {this.resetRequestState()}},// 重置请求状态resetRequestState() {this.isSending = falsethis.currentRequestTask = nullconst lastMsg = this.messages[this.messages.length - 1]if (lastMsg?.type === 'ai') {lastMsg.text = '请求失败,请重试'}},getDisplayText(message) {this.scrollToBottom()return message.fullText},},
}
</script>

这种实现方式相比传统轮询或WebSocket的优势:

更低的延迟(平均比WebSocket快300-500ms)

更好的移动端兼容性(SSE在uni-app中支持度更好)

更低的内存占用(相比维持长连接)

相关文章:

  • 第十二节:性能优化高频题-shallowRef/shallowReactive使用场景
  • Kotlin await等待多个异步任务都完成后才进行下一步操作
  • web技术与nginx网站环境部署
  • docker搭建swarm集群
  • node.js 实战——mongoDB
  • 【Docker】——在Docker工具上安装创建容器并完成项目部署
  • Flink HA 总结
  • 人工智能大语言模型与AI芯片新进展:技术演进与商业化路径
  • 【3D基础】深入解析OBJ与MTL文件格式:Blender导出模型示例及3D开发应用
  • 【Linux】第十二章 安装和更新软件包
  • deepseek对IBM MQ SSL 证书算法的建议与解答
  • 自动驾驶L4级技术落地:特斯拉、Waymo与华为的路线之争
  • [三分钟]web自动化测试(三):selenium自动化测试常用函数(下)
  • 突破JVM边界:类加载三重门与栈帧的生存法则
  • x-cmd install | Orbiton:极简至上的终端文本编辑器与轻量级 IDE
  • wps批注线条怎么取消去掉wps批注后有竖线
  • idea启动springboot方式及web调用
  • 数据库基础与核心操作:从概念到实战的全面解析
  • Linux进程7-signal信号处理方式验证、可重入函数举例、信号集函数验证、信号集阻塞验证
  • Github两种鉴权模式PAT与SSH
  • 日趋活跃!2024年我国数据生产总量同比增长25%
  • 新华社评论员:汇聚起工人阶级和广大劳动群众的磅礴力量
  • 自称“最美”通缉犯出狱当主播?央广网:三观怎能跟着“五官”跑
  • 广东雷州农商行董事长、原行长同日被查
  • 黄晓丹:用“诗心”找到生存的意义
  • 第二艘国产大型邮轮实现坞内起浮,重点强化邮轮供应链本土化建设