避开WebSocket和H5嵌套!用uni-app的enableChunked,5步实现小程序ChatGPT打字机效果
·
避开WebSocket和H5嵌套!用uni-app的enableChunked,5步实现小程序ChatGPT打字机效果
在小程序开发中,实现类似ChatGPT的实时打字机效果一直是个技术难点。传统方案往往依赖WebSocket或H5嵌套,但这些方法要么实现复杂,要么体验不佳。本文将介绍一种纯前端解决方案,利用uni-app的enableChunked特性,只需5个步骤就能实现流畅的实时回复效果。
1. 理解分块传输的基本原理
分块传输编码(Chunked Transfer Encoding)是HTTP协议中的一种数据传输方式,它允许服务器将响应分成多个"块"发送。对于小程序中的实时交互场景,这种技术有以下优势:
- 实时性:数据可以分段到达,无需等待完整响应
- 低延迟:每个数据块到达后可以立即处理
- 资源友好:不需要维持长连接,节省服务器资源
在uni-app中,enableChunked: true配置会启用对这种分块传输的支持。当与responseType: 'text'配合使用时,可以监听onChunkReceived事件来实时获取数据片段。
提示:分块传输与WebSocket不同,它仍然基于HTTP协议,不需要额外的握手过程或连接维护。
2. 配置请求参数
正确的请求配置是实现分块传输的基础。以下是一个完整的请求配置示例:
const requestTask = uni.request({
url: '你的API地址',
method: 'POST', // 根据后端要求选择GET或POST
responseType: 'text',
enableChunked: true,
header: {
'Content-Type': 'application/json',
// 其他必要的请求头
},
data: {
// 你的请求参数
}
})
关键参数说明:
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| responseType | string | 是 | 必须设置为'text' |
| enableChunked | boolean | 是 | 启用分块传输,设为true |
| method | string | 是 | 根据后端接口要求设置 |
3. 处理分块数据
配置好请求后,需要通过onChunkReceived事件监听器来处理到达的数据块。以下是数据处理的核心代码:
requestTask.onChunkReceived(function(response) {
// 将ArrayBuffer转换为Base64
const arrayBuffer = response.data
const uint8Array = new Uint8Array(arrayBuffer)
let text = uni.arrayBufferToBase64(uint8Array)
// 将Base64解码为UTF-8文本
text = new Buffer(text, 'base64').toString('utf8')
// 处理特殊结束标记
if (text.trim() === '0') {
console.log('数据传输完成')
return
}
// 处理实际内容数据
if (text.includes('content')) {
try {
const jsonStr = text.split('success: ')[1]
const data = JSON.parse(jsonStr)
// 更新UI显示
this.updateChatContent(data.content)
} catch (e) {
console.error('数据解析错误:', e)
}
}
})
数据转换流程说明:
- 接收到的数据是ArrayBuffer格式
- 先转换为Uint8Array
- 使用uni-app的
arrayBufferToBase64转为Base64 - 再将Base64解码为UTF-8文本
- 最后解析JSON内容
4. 实现流畅的"打字机"效果
要让文本像打字机一样逐个字符显示,需要一些前端技巧。以下是实现方案:
// 在页面data中定义
data() {
return {
chatContent: '',
typingSpeed: 30, // 打字速度(毫秒/字符)
typingTimer: null
}
},
methods: {
updateChatContent(newText) {
// 清除之前的定时器
if (this.typingTimer) clearTimeout(this.typingTimer)
const targetContent = this.chatContent + newText
let currentIndex = this.chatContent.length
const typeNextChar = () => {
if (currentIndex < targetContent.length) {
this.chatContent = targetContent.substring(0, currentIndex + 1)
currentIndex++
this.typingTimer = setTimeout(typeNextChar, this.typingSpeed)
// 自动滚动到底部
this.scrollToBottom()
}
}
typeNextChar()
},
scrollToBottom() {
uni.createSelectorQuery()
.select('#chat-container')
.boundingClientRect(rect => {
uni.pageScrollTo({
scrollTop: rect.bottom,
duration: 300
})
})
.exec()
}
}
优化技巧:
- 速度控制:通过调整
typingSpeed可以改变打字速度 - 平滑滚动:在添加新内容时自动滚动到最新消息
- 性能优化:使用
substring而非逐个字符拼接
5. 错误处理与性能优化
在实际应用中,健壮的错误处理和性能优化必不可少。以下是关键点:
常见错误处理:
requestTask.onChunkReceived(function(response) {
// ...数据转换代码...
// 错误处理
if (text.includes('error')) {
const errorMsg = text.replace("event: error\ndata: ", "").replace("\r\n", "")
try {
const error = JSON.parse(errorMsg)
this.showErrorToast(error.msg)
return
} catch (e) {
console.error('错误信息解析失败:', e)
}
}
})
methods: {
showErrorToast(message) {
uni.showToast({
title: message,
icon: 'none',
duration: 3000
})
}
}
性能优化建议:
- 节流处理:对于高频更新的内容,可以适当缓冲几个数据块再更新UI
- 内存管理:长时间对话时,注意清理不再需要的历史数据
- 网络中断处理:添加超时检测和重试机制
- 加载状态:显示清晰的加载指示器
// 示例:添加加载状态
data() {
return {
isLoading: false
}
},
methods: {
startRequest() {
this.isLoading = true
const requestTask = uni.request({
// ...配置...
complete: () => {
this.isLoading = false
}
})
}
}
完整示例代码
以下是一个完整的uni-app页面示例,整合了所有关键功能:
<template>
<view class="container">
<scroll-view id="chat-container" scroll-y class="chat-area">
<view class="message" v-for="(msg, index) in messages" :key="index">
{{ msg.content }}
</view>
<view v-if="isTyping" class="typing-indicator">...</view>
</scroll-view>
<view class="input-area">
<input v-model="inputText" placeholder="输入消息..." />
<button @click="sendMessage" :disabled="isLoading">发送</button>
</view>
</view>
</template>
<script>
export default {
data() {
return {
inputText: '',
messages: [],
isTyping: false,
isLoading: false,
currentResponse: '',
typingSpeed: 30
}
},
methods: {
sendMessage() {
if (!this.inputText.trim()) return
// 添加用户消息
this.messages.push({
role: 'user',
content: this.inputText
})
// 添加空的AI回复占位
this.messages.push({
role: 'assistant',
content: ''
})
const userMessage = this.inputText
this.inputText = ''
this.isLoading = true
this.isTyping = true
const requestTask = uni.request({
url: '你的API地址',
method: 'POST',
responseType: 'text',
enableChunked: true,
header: {
'Content-Type': 'application/json'
},
data: {
message: userMessage
},
complete: () => {
this.isLoading = false
this.isTyping = false
}
})
requestTask.onChunkReceived(response => {
const arrayBuffer = response.data
const uint8Array = new Uint8Array(arrayBuffer)
let text = uni.arrayBufferToBase64(uint8Array)
text = new Buffer(text, 'base64').toString('utf8')
if (text.trim() === '0') {
return
}
if (text.includes('error')) {
const errorMsg = text.replace("event: error\ndata: ", "").replace("\r\n", "")
try {
const error = JSON.parse(errorMsg)
this.showErrorToast(error.msg)
this.messages.pop() // 移除空的AI回复
} catch (e) {
console.error('错误解析失败:', e)
}
return
}
if (text.includes('content')) {
try {
const jsonStr = text.split('success: ')[1]
const data = JSON.parse(jsonStr)
this.currentResponse += data.content
this.typeResponse()
} catch (e) {
console.error('数据解析错误:', e)
}
}
})
},
typeResponse() {
const aiMessageIndex = this.messages.length - 1
let currentLength = this.messages[aiMessageIndex].content.length
if (currentLength < this.currentResponse.length) {
this.messages[aiMessageIndex].content =
this.currentResponse.substring(0, currentLength + 1)
this.$nextTick(() => {
this.scrollToBottom()
})
setTimeout(this.typeResponse, this.typingSpeed)
}
},
scrollToBottom() {
uni.createSelectorQuery()
.select('#chat-container')
.boundingClientRect(rect => {
uni.pageScrollTo({
scrollTop: rect.bottom,
duration: 100
})
})
.exec()
},
showErrorToast(message) {
uni.showToast({
title: message,
icon: 'none',
duration: 3000
})
}
}
}
</script>
<style>
.container {
display: flex;
flex-direction: column;
height: 100vh;
}
.chat-area {
flex: 1;
padding: 20rpx;
}
.input-area {
display: flex;
padding: 20rpx;
border-top: 1px solid #eee;
}
.input-area input {
flex: 1;
margin-right: 20rpx;
padding: 10rpx 20rpx;
border: 1px solid #ddd;
border-radius: 8rpx;
}
.typing-indicator {
padding: 10rpx;
color: #999;
}
</style>
在实际项目中,根据后端API的具体响应格式,可能需要对数据处理部分进行相应调整。这个方案避免了WebSocket的复杂性,也不需要使用H5嵌套,完全基于uni-app的原生能力实现,具有更好的性能和兼容性。
更多推荐



所有评论(0)