在uniapp里面處理流式請求相對于web端來說有點麻煩,下面我將講述幾種處理流式請求的方式。
1.websocket
WebSocket 是處理實時數據流的最佳選擇之一,UniApp 提供了原生的 WebSocket 支持:
<template><view class="container"><scroll-view scroll-y class="data-container" :scroll-top="scrollTop"><view v-for="(item, index) in messages" :key="index" class="message-item">{{ item }}</view></scroll-view><view class="control-panel"><button @click="connectSocket" type="primary" :disabled="isConnected">連接</button><button @click="closeSocket" type="warn" :disabled="!isConnected">斷開</button></view></view>
</template><script>
export default {data() {return {socketTask: null,isConnected: false,messages: [],scrollTop: 0}},methods: {connectSocket() {this.socketTask = uni.connectSocket({url: 'wss://your-websocket-server.com/stream',success: () => {console.log('準備連接...')}})this.socketTask.onOpen(() => {this.isConnected = truethis.addMessage('連接已建立')// 訂閱數據流this.socketTask.send({data: JSON.stringify({action: 'subscribe'})})})this.socketTask.onMessage((res) => {this.addMessage(res.data)})this.socketTask.onClose(() => {this.isConnected = falsethis.addMessage('連接已關閉')})this.socketTask.onError((res) => {this.addMessage('錯誤: ' + JSON.stringify(res))})},closeSocket() {if (this.socketTask && this.isConnected) {this.socketTask.close()}},addMessage(msg) {this.messages.push(typeof msg === 'string' ? msg : JSON.stringify(msg))this.$nextTick(() => {this.scrollTop = 99999 // 滾動到底部})}},onUnload() {// 頁面卸載時關閉連接this.closeSocket()}
}
</script><style>
.container {display: flex;flex-direction: column;height: 100vh;padding: 20rpx;
}
.data-container {flex: 1;border: 1px solid #eee;padding: 20rpx;margin-bottom: 20rpx;background-color: #f9f9f9;
}
.message-item {padding: 10rpx;border-bottom: 1px solid #eee;word-break: break-all;
}
.control-panel {display: flex;justify-content: space-around;padding: 20rpx 0;
}
</style>
2.uni.request 處理流式請求
這種方式適用于h5頁面和小程序,不適用于app
<template><view class="container"><view class="stream-container"><view v-for="(item, index) in streamData" :key="index" class="stream-item">{{ item }}</view></view><button @click="startStream" type="primary">開始接收流數據</button></view>
</template><script>
export default {data() {return {streamData: [],dataBuffer: ''}},methods: {startStream() {uni.request({url: 'https://your-stream-api.com/stream',method: 'GET',enableChunked: true,dataType: 'text',onChunkReceived: (res) => {this.handleChunk(res.data);},success: (res) => {uni.showToast({title: '流數據接收完成',icon: 'success'});},fail: (err) => {uni.showModal({title: '錯誤',content: '流數據接收失敗: ' + JSON.stringify(err),showCancel: false});}});},handleChunk(chunk) {// 將接收到的數據添加到緩沖區this.dataBuffer += chunk;// 處理可能的換行符分隔的數據const lines = this.dataBuffer.split('\n');// 保留最后一個可能不完整的行this.dataBuffer = lines.pop() || '';// 處理完整的行for (const line of lines) {if (line.trim()) {try {// 嘗試解析JSONconst data = JSON.parse(line);this.streamData.push(JSON.stringify(data));} catch (e) {// 非JSON數據直接顯示this.streamData.push(line);}}}}}
}
</script><style>
.container {padding: 20px;
}
.stream-container {border: 1px solid #eee;padding: 10px;margin-bottom: 20px;max-height: 300px;overflow-y: auto;
}
.stream-item {padding: 5px 0;border-bottom: 1px solid #f5f5f5;
}
</style>
3.使用 SSE (Server-Sent Events)
雖然 UniApp 沒有原生的 SSE API,但可以通過封裝 XMLHttpRequest 來實現 SSE:
function createSSEConnection(url) {// 創建一個標準的XMLHttpRequest對象const xhr = new XMLHttpRequest()xhr.open('GET', url, true)xhr.setRequestHeader('Accept', 'text/event-stream')xhr.setRequestHeader('Cache-Control', 'no-cache')// 設置響應類型為文本xhr.responseType = 'text'// 數據緩沖區let buffer = ''// 處理進度事件xhr.onprogress = function(e) {// 獲取新數據const newData = xhr.responseText.substring(buffer.length)if (newData) {buffer += newData// 按行分割數據const lines = newData.split('\n')for (const line of lines) {if (line.startsWith('data:')) {const eventData = line.substring(5).trim()// 觸發數據處理handleSSEData(eventData)}}}}xhr.onerror = function(e) {console.error('SSE連接錯誤:', e)}xhr.onreadystatechange = function() {if (xhr.readyState === 4) {if (xhr.status === 200) {console.log('SSE連接完成')} else {console.error('SSE連接失敗:', xhr.status)}}}// 發送請求xhr.send()return xhr
}// 處理SSE數據
function handleSSEData(data) {try {const parsedData = JSON.parse(data)console.log('收到SSE數據:', parsedData)// 處理數據...} catch (e) {console.log('收到SSE文本:', data)// 處理非JSON數據...}
}// 使用方法
const sseConnection = createSSEConnection('https://your-sse-endpoint.com/events')// 關閉連接
function closeSSE() {if (sseConnection) {sseConnection.abort()}
}
4.使用分頁和輪詢模擬流
對于不支持真正的流式請求的場景,可以使用分頁和輪詢來模擬流式體驗:
// 輪詢獲取數據
let lastId = 0
let isPolling = falsefunction startPolling() {isPolling = truepoll()
}function stopPolling() {isPolling = false
}function poll() {if (!isPolling) returnuni.request({url: 'https://your-api.com/data',data: {last_id: lastId,limit: 10},success: (res) => {const data = res.dataif (data && data.items && data.items.length > 0) {// 處理接收到的數據processItems(data.items)// 更新最后ID用于下次請求lastId = data.items[data.items.length - 1].id}// 如果還有更多數據,繼續輪詢if (data.has_more) {setTimeout(poll, 1000) // 1秒后再次輪詢} else {console.log('所有數據接收完畢')isPolling = false}},fail: (err) => {console.error('輪詢失敗:', err)// 錯誤后延遲重試setTimeout(() => {if (isPolling) poll()}, 3000)}})
}
5.app端適用renderjs
該方式適用于app端進行流式請求,在app端上面的幾種方式我都嘗試過除了websocket沒有一個能用的,經過反復的查詢才找到適用renderjs這種方式。
RenderJS 是 UniApp 提供的一個運行在視圖層的 JavaScript 引擎,允許開發者直接操作 DOM 和使用瀏覽器特有的 API。
<viewclass="":sseValue="sseValue":change:sseValue="renderScript.getSseValue":messagesRenderjs="messagesRenderjs":change:messagesRenderjs="renderScript.getMessage":downSend="downSend":change:downSend="renderScript.getDownSend":modelchangeValue="modelchangeValue":change:modelchangeValue="renderScript.getModel"></view>
通過這種方式來調用renderjs
<script module="renderScript" lang="renderjs">
export default {data() {return {VITE_AIR14B_url: '',VITE_AILOCAL:'',VITE_ANYTHING:'',modelValue: 'qwen2.5:14b',messages: [],downValue: false,loading: false,downSentValue: false,biaoshi: false,model:'qianwen'};},methods: {getMessage(val) {// console.log(val,'message')this.messages = val;},async getSseValue(val) {console.log(val,'val')if(this.model=='qianwen'){await this.changeModel(val);}else if(this.model=='locel'){await this.changeModelLocal(val)}else if(this.model=='water'){await this.changeModelAnything(val)}else if(this.model=='sxyd'){console.log('sxyd')}},// 暫停生成getDownSend(val) {console.log(val, 'valvalval');if (!this.biaoshi) {this.biaoshi = true;} else {this.downSentValue = true;}//停止生成的標志// this.downSentValue=true},// 獲取模型getModel(val) {console.log(val);this.model=val},// 接收流式數據qianwenasync changeModel(val) {let params = {model: this.modelValue,messages: this.messages,stream: true};try {// 發送請求到 APIconst response = await fetch(this.VITE_AIR14B_url + '/api/chat', {method: 'POST',headers: {'Content-Type': 'application/json'},body: JSON.stringify(params)});// 獲取流式響應體const reader = response.body.getReader();const decoder = new TextDecoder('utf-8'); // 解碼器,將字節流轉換為字符串this.downValue = false;let value = ''; // 用于拼接收到的每一部分 response 內容// 獲取響應之后關閉loading// this.$ownerInstance.callMethod('closeLoading', null);this.messages.push({role: 'assistant',content: ''});// 逐塊讀取數據while (!this.downValue) {// 讀取數據塊const { done: isDone, value: chunk } = await reader.read();// if (this.onDown.value) {// this.downValue = true;// break;// }if (this.downSentValue) {this.downValue = true;this.downSentValue = false;this.$ownerInstance.callMethod('setSend', true);this.messages[this.messages.length - 1].content += '\n\n[已取消生成]';this.$ownerInstance.callMethod('setMessageValue', this.messages);break;}this.downValue = isDone;if (this.downValue) {this.$ownerInstance.callMethod('setSend', true);} else {this.$ownerInstance.callMethod('setSend', false);}let chunkString = decoder.decode(chunk, { stream: true });// 將字節流轉換為字符串并追加到 valuevalue += chunkString;// 打印已接收的部分數據// console.log('Received chunk:', chunkString);if (chunkString && this.messages[this.messages.length - 1].role === 'assistant') {try {const parsedChunk = JSON.parse(chunkString);const content = parsedChunk?.message?.content; // 使用可選鏈避免屬性訪問錯誤if (content === undefined) {throw new Error('Invalid chunk structure: Missing message.content');}// 確保 messages 數組非空if (this.messages.length === 0) {throw new Error('Messages array is empty');}this.messages[this.messages.length - 1].content += content;if(this.messages[this.messages.length - 1].content){// 獲取響應之后關閉loadingthis.$ownerInstance.callMethod('closeLoading', null);}// 將值傳遞出去this.$ownerInstance.callMethod('setMessageValue', this.messages);} catch (error) {console.error('處理 chunk 時發生錯誤:', error);}}}// console.log('Final response:', value);} catch (error) {console.error('Error during fetch request:', error);this.loading = false;// 顯示錯誤消息if (this.messages.length > 0 && this.messages[this.messages.length - 1].role === 'assistant') {this.messages[this.messages.length - 1].content += '\n\n[生成回答時出現錯誤]';}}},}
};
</script>