一、完整代码展示
- 目前大多数的ai对话都是流式输出,也就是对话是一个字或者多个字逐一进行显示的
- 下面是一个完整的流式显示程序,包含的用户的消息发出和ai的消息回复
<template>
<view class="chat-container">
<view class="messages">
<!-- 对话气泡 -->
<view
v-for="(message, index) in messages"
:key="index"
:class="['message', message.sender]"
>
<text selectable="true">{{ message.text }}</text>
</view>
<!-- 加载状态 -->
<view v-if="isLoading" class="loading-spinner"></view>
</view>
<!-- 消息输入和发送按钮 -->
<view class="input-area">
<textarea
v-model="inputMessage"
placeholder="输入消息"
@input="adjustInputHeight"
></textarea>
<button @click="sendMessage">发送</button>
</view>
</view>
</template>
<script>
export default {
data() {
return {
messages: [],
inputMessage: '',
isLoading: false,
inputHeight: 48
};
},
methods: {
sendMessage() {
//如果输出消息为空直接返回
if (!this.inputMessage.trim()) return;
// 添加用户消息
this.messages.push({
text: this.inputMessage,
sender: 'user'
});
// 初始化AI消息
const aiIndex = this.messages.length;
this.messages.push({
text: '',
sender: 'ai'
});
// 重置输入
this.inputMessage = '';
this.isLoading = true;
// 发起流式请求
const url = 'http://localhost:8081/chat';
const params = {
session_id: 'token',
content: this.inputMessage
};
uni.request({
url: url + '?' + this.serializeParams(params),
method: 'GET',
header: {
'Accept': 'text/event-stream',
},
success: (res) => {
this.processStreamResponse(res.data, aiIndex);
},
fail: (err) => {
console.error('请求失败:', err);
this.isLoading = false;
}
});
},
processStreamResponse(data, aiIndex) {
const chunks = data.split('\n');
let chunkIndex = 0;
const interval = setInterval(() => {
if (chunkIndex >= chunks.length) {
clearInterval(interval);
this.isLoading = false;
return;
}
const chunk = chunks[chunkIndex].replace('data:', '').trim();
if (chunk) {
this.messages[aiIndex].text += chunk;
this.$forceUpdate();
}
chunkIndex++;
}, 50);
},
serializeParams(params) {
return Object.entries(params)
.map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`)
.join('&');
},
adjustInputHeight(e) {
const textarea = e.target;
textarea.style.height = 'auto';
textarea.style.height = textarea.scrollHeight + 'px';
this.inputHeight = textarea.scrollHeight;
}
}
};
</script>
<style>
.chat-container {
height: 100vh;
display: flex;
flex-direction: column;
padding: 20px;
background-color: #f5f5f7;
}
.messages {
flex: 1;
overflow-y: auto;
padding: 20px;
}
.message {
margin: 10px 0;
padding: 12px 16px;
border-radius: 16px;
max-width: 70%;
word-wrap: break-word;
}
.message.user {
background: linear-gradient(135deg, #cbe7ff, #cfe9ff);
align-self: flex-end;
}
.message.ai {
background: linear-gradient(135deg, #f0f0f0, #e0e0e0);
align-self: flex-start;
}
.loading-spinner {
border: 4px solid #f3f3f3;
border-top: 4px solid #007aff;
border-radius: 50%;
width: 24px;
height: 24px;
animation: spin 1s linear infinite;
margin: 20px auto;
}
.input-area {
display: flex;
gap: 10px;
margin-top: 20px;
}
textarea {
flex: 1;
padding: 12px;
border: 1px solid #ddd;
border-radius: 8px;
resize: none;
}
button {
padding: 12px 24px;
background-color: #007aff;
color: white;
border: none;
border-radius: 8px;
cursor: pointer;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
</style>
二、流式传输核心代码讲解
1、请求发起
- 设置
Accept: text/event-stream
告知服务器需要流式响应 - 通过
session_id
传递认证信息 - 使用
GET
请求发送消息内容
uni.request({
url: url + '?' + this.serializeParams(params),
method: 'GET',
header: {
'Accept': 'text/event-stream',
},
success: (res) => {
this.processStreamResponse(res.data, aiIndex);
}
});
2、流式响应处理
- 将响应数据按换行符分割成块
- 使用
setInterval
控制显示速度(这里设置为 50ms / 块) - 逐块追加到 AI 消息中
- 使用
$forceUpdate
强制刷新视图
processStreamResponse(data, aiIndex) {
const chunks = data.split('\n');
let chunkIndex = 0;
const interval = setInterval(() => {
if (chunkIndex >= chunks.length) {
clearInterval(interval);
this.isLoading = false;
return;
}
const chunk = chunks[chunkIndex].replace('data:', '').trim();
if (chunk) {
this.messages[aiIndex].text += chunk;
this.$forceUpdate();
}
chunkIndex++;
}, 50);
}
3、加载状态管理
- 在请求发起时显示加载状态
- 响应处理完成后隐藏加载状态
// 发送消息时
this.isLoading = true;
// 响应处理完成
clearInterval(interval);
this.isLoading = false;
4、数据格式处理
- 将参数对象序列化为 URL 查询字符串
- 使用
encodeURIComponent
处理特殊字符
serializeParams(params) {
return Object.entries(params)
.map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`)
.join('&');
}
5、消息显示
- 使用 flex 布局实现消息气泡
- 通过
selectable="true"
实现文本选中 - 根据 sender 添加不同样式
<view v-for="(message, index) in messages" :class="['message', message.sender]">
<text selectable="true">{{ message.text }}</text>
</view>