一、完整代碼展示
- 目前大多數的ai對話都是流式輸出,也就是對話是一個字或者多個字逐一進行顯示的
- 下面是一個完整的流式顯示程序,包含的用戶的消息發出和ai的消息回復
<template><view class="chat-container"><view class="messages"><!-- 對話氣泡 --><viewv-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"><textareav-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>