写过一篇 发表于 2025-3-30 10:13:22

uniapp中的流式输出

一、完整代码展示



[*]目前大多数的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.replace('data:', '').trim();
      if (chunk) {
          this.messages.text += chunk;
          this.$forceUpdate();
      }
      chunkIndex++;
      }, 50);
    },

    serializeParams(params) {
      return Object.entries(params)
      .map(() => `${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.replace('data:', '').trim();
    if (chunk) {
      this.messages.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(() => `${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>

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
页: [1]
查看完整版本: uniapp中的流式输出