最近的项目是做微信小程序的一个对话框,接入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() 实现请求中断
代码展示:
javascript
<template>
<view class="chat-container">
<view class="chat-box">
<scroll-view
class="chat-msg"
scroll-y
id="chatMessages"
enable-flex
@scrolltolower="scrollToBottom"
:scroll-top="scrollTop"
scroll-with-animation="true"
>
<!-- 问答区域 -->
<view
v-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) return
if (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 = true
this.isAborted = false
this.retryCount = 0
const 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) return
const 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?.text
const finishReason = parseSSEData(text)?.[0]?.output?.finish_reason
this.sessionId = parseSSEData(text)?.[0]?.output?.session_id
this.messages[this.messages?.length - 1].text = chunkText
this.messages[this.messages?.length - 1].fullText = chunkText
this.messages[this.messages?.length - 1].isFinishReason = finishReason
if (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 = true
try {
this.currentRequestTask.abort()
} catch (e) {
console.warn('中止请求时发生异常:', e)
}
this.currentRequestTask = null
// 更新最后一条消息
const lastIndex = this.messages.length - 1
if (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) return
console.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 = false
this.currentRequestTask = null
const 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中支持度更好)
更低的内存占用(相比维持长连接)