vue前端处理流式数据

我们平常使用的ai 也好 还是聊天也好 消息的处理当然重要不管是 通过webscoket 还是http 没有区别都是为了获取数据 只不过方式不一样

但是我们平常使用的deepseek 豆包 这些 回答的时候 文字不是一下全出来的 当然 这个流式数据 本身是需要切割的

我做了一个简单的前后端程序

我本地布置了一套大模型 llm studio

自己写了部分简单接口

复制代码
http://192.168.110.45:8001/ai/chat?input=%E4%BD%A0%E8%AF%B4%E4%BA%BA%E8%BF%99%E4%B8%80%E8%BE%88%E5%AD%90%E6%98%AF%E6%B3%A8%E5%AE%9A%E7%9A%84%E5%90%97

我发了一个简单的问答

你说人这一辈子是注定的吗

后端返回数据

复制代码
data: {"token":"这"}

data: {"token":"个"}

data: {"token":"问"}

data: {"token":"题"}

data: {"token":"很"}

data: {"token":"复"}

data: {"token":"杂"}

data: {"token":","}

data: {"token":"因"}

data: {"token":"为"}

data: {"token":"它"}

data: {"token":"涉"}

data: {"token":"及"}

data: {"token":"哲"}

data: {"token":"学"}

data: {"token":"、"}

data: {"token":"心"}

data: {"token":"理"}

data: {"token":"学"}

data: {"token":"和"}

data: {"token":"科"}

data: {"token":"学"}

data: {"token":"的"}

data: {"token":"多"}

data: {"token":"个"}

data: {"token":"方"}

data: {"token":"面"}

data: {"token":"。"}

data: {"token":"从"}

data: {"token":"哲"}

data: {"token":"学"}

data: {"token":"角"}

data: {"token":"度"}

data: {"token":"来"}

data: {"token":"看"}

data: {"token":","}

data: {"token":"有"}

data: {"token":"些"}

data: {"token":"人"}

data: {"token":"认"}

data: {"token":"为"}

data: {"token":"人"}

data: {"token":"生"}

data: {"token":"中"}

data: {"token":"的"}

data: {"token":"每"}

data: {"token":"一"}

data: {"token":"步"}

data: {"token":"都"}

data: {"token":"是"}

data: {"token":"由"}

data: {"token":"命"}

data: {"token":"运"}

data: {"token":"或"}

data: {"token":"上"}

data: {"token":"帝"}

data: {"token":"决"}

data: {"token":"定"}

data: {"token":"的"}

data: {"token":","}

data: {"token":"而"}

data: {"token":"我"}

data: {"token":"们"}

data: {"token":"只"}

data: {"token":"是"}

data: {"token":"被"}

data: {"token":"动"}

data: {"token":"地"}

data: {"token":"接"}

data: {"token":"受"}

data: {"token":"着"}

data: {"token":"。"}

data: {"token":"然"}

data: {"token":"而"}

data: {"token":","}

data: {"token":"另"}

data: {"token":"一"}

data: {"token":"些"}

data: {"token":"人"}

data: {"token":"则"}

data: {"token":"认"}

data: {"token":"为"}

data: {"token":"人"}

data: {"token":"生"}

data: {"token":"的"}

data: {"token":"选"}

data: {"token":"择"}

data: {"token":"和"}

data: {"token":"结"}

data: {"token":"果"}

data: {"token":"是"}

data: {"token":"由"}

data: {"token":"我"}

data: {"token":"们"}

data: {"token":"的"}

data: {"token":"自"}

data: {"token":"由"}

data: {"token":"意"}

data: {"token":"志"}

data: {"token":"决"}

data: {"token":"定"}

data: {"token":"的"}

data: {"token":"。"}

data: {"token":"\n"}

data: {"token":"\n"}

data: {"token":"从"}

data: {"token":"心"}

data: {"token":"理"}

data: {"token":"学"}

data: {"token":"角"}

data: {"token":"度"}

data: {"token":"来"}

data: {"token":"看"}

data: {"token":","}

data: {"token":"人"}

data: {"token":"的"}

data: {"token":"行"}

data: {"token":"为"}

data: {"token":"和"}

data: {"token":"决"}

data: {"token":"策"}

data: {"token":"受"}

data: {"token":"到"}

data: {"token":"各"}

data: {"token":"种"}

data: {"token":"因"}

data: {"token":"素"}

data: {"token":"的"}

data: {"token":"影"}

data: {"token":"响"}

data: {"token":","}

data: {"token":"如"}

data: {"token":"遗"}

data: {"token":"传"}

data: {"token":"、"}

data: {"token":"环"}

data: {"token":"境"}

data: {"token":"、"}

data: {"token":"经"}

data: {"token":"历"}

data: {"token":"和"}

data: {"token":"个"}

data: {"token":"性"}

data: {"token":"等"}

data: {"token":"。"}

data: {"token":"虽"}

data: {"token":"然"}

data: {"token":"这"}

data: {"token":"些"}

data: {"token":"因"}

data: {"token":"素"}

data: {"token":"可"}

data: {"token":"以"}

data: {"token":"影"}

data: {"token":"响"}

data: {"token":"我"}

data: {"token":"们"}

data: {"token":"的"}

data: {"token":"选"}

data: {"token":"择"}

data: {"token":","}

data: {"token":"但"}

data: {"token":"它"}

data: {"token":"们"}

data: {"token":"并"}

data: {"token":"不"}

data: {"token":"决"}

data: {"token":"定"}

data: {"token":"我"}

data: {"token":"们"}

data: {"token":"的"}

data: {"token":"人"}

data: {"token":"生"}

data: {"token":"道"}

data: {"token":"路"}

data: {"token":"。"}

data: {"token":"\n"}

data: {"token":"\n"}

data: {"token":"科"}

data: {"token":"学"}

data: {"token":"上"}

data: {"token":"来"}

data: {"token":"说"}

data: {"token":","}

data: {"token":"人"}

data: {"token":"生"}

data: {"token":"的"}

data: {"token":"发"}

data: {"token":"展"}

data: {"token":"也"}

data: {"token":"受"}

data: {"token":"到"}

data: {"token":"生"}

data: {"token":"物"}

data: {"token":"学"}

data: {"token":"和"}

data: {"token":"神"}

data: {"token":"经"}

data: {"token":"科"}

data: {"token":"学"}

data: {"token":"的"}

data: {"token":"影"}

data: {"token":"响"}

data: {"token":"。"}

data: {"token":"例"}

data: {"token":"如"}

data: {"token":","}

data: {"token":"基"}

data: {"token":"因"}

data: {"token":"、"}

data: {"token":"脑"}

data: {"token":"结"}

data: {"token":"构"}

data: {"token":"和"}

data: {"token":"功"}

data: {"token":"能"}

data: {"token":"都"}

data: {"token":"可"}

data: {"token":"能"}

data: {"token":"影"}

data: {"token":"响"}

data: {"token":"我"}

data: {"token":"们"}

data: {"token":"的"}

data: {"token":"行"}

data: {"token":"为"}

data: {"token":"和"}

data: {"token":"决"}

data: {"token":"策"}

data: {"token":"能"}

data: {"token":"力"}

data: {"token":"。"}

data: {"token":"但"}

data: {"token":"是"}

data: {"token":","}

data: {"token":"这"}

data: {"token":"些"}

data: {"token":"因"}

data: {"token":"素"}

data: {"token":"也"}

data: {"token":"不"}

data: {"token":"足"}

data: {"token":"以"}

data: {"token":"完"}

data: {"token":"全"}

data: {"token":"决"}

data: {"token":"定"}

data: {"token":"我"}

data: {"token":"们"}

data: {"token":"的"}

data: {"token":"命"}

data: {"token":"运"}

data: {"token":"。"}

data: {"token":"\n"}

data: {"token":"\n"}

data: {"token":"因"}

data: {"token":"此"}

data: {"token":","}

data: {"token":"我"}

data: {"token":"认"}

data: {"token":"为"}

data: {"token":"人"}

data: {"token":"这"}

data: {"token":"一"}

data: {"token":"辈"}

data: {"token":"子"}

data: {"token":"并"}

data: {"token":"不"}

data: {"token":"是"}

data: {"token":"完"}

data: {"token":"全"}

data: {"token":"注"}

data: {"token":"定"}

data: {"token":"的"}

data: {"token":"。"}

data: {"token":"虽"}

data: {"token":"然"}

data: {"token":"我"}

data: {"token":"们"}

data: {"token":"的"}

data: {"token":"人"}

data: {"token":"生"}

data: {"token":"道"}

data: {"token":"路"}

data: {"token":"会"}

data: {"token":"受"}

data: {"token":"到"}

data: {"token":"各"}

data: {"token":"种"}

data: {"token":"因"}

data: {"token":"素"}

data: {"token":"的"}

data: {"token":"影"}

data: {"token":"响"}

data: {"token":","}

data: {"token":"但"}

data: {"token":"我"}

data: {"token":"们"}

data: {"token":"仍"}

data: {"token":"然"}

data: {"token":"有"}

data: {"token":"自"}

data: {"token":"由"}

data: {"token":"选"}

data: {"token":"择"}

data: {"token":"和"}

data: {"token":"决"}

data: {"token":"定"}

data: {"token":"自"}

data: {"token":"己"}

data: {"token":"的"}

data: {"token":"生"}

data: {"token":"活"}

data: {"token":"方"}

data: {"token":"向"}

data: {"token":"。"}

data: [DONE]

当然可能我这个数据写的不标准 不应该使用token字段 先忽略

他是这样返回的

我前端代码处理文字流

复制代码
// 发送消息
async function sendMessage() {
  const question = inputText.value.trim();
  if (!question || isLoading.value) return;

  // 添加用户消息
  messages.value.push({ role: 'user', content: question });
  inputText.value = '';
  scrollToBottom();

  // 显示加载状态
  isLoading.value = true;

  // 创建一个临时的 AI 消息占位,用于流式追加
  const assistantMsgIndex = messages.value.length;
  messages.value.push({ role: 'assistant', content: '' });

  let fullText = '';

  try {
    // 注意:uni-app 的 H5 端支持 fetch,但 App 端可能需要使用 uni.request 并自行处理流式
    // 这里以 H5 为例,使用 fetch 读取 ReadableStream
    const response = await fetch(`${API_URL}?input=${encodeURIComponent(question)}`);

    // if (!response.ok) {
    //   throw new Error(`HTTP ${response.status}`);
    // }

    const reader = response.body.getReader();
    const decoder = new TextDecoder();

    while (true) {
      const { done, value } = await reader.read();
      if (done) break;
      const chunk = decoder.decode(value, { stream: true });
      const lines = chunk.split('\n');
      for (const line of lines) {
        if (line.startsWith('data: ')) {
          const data = line.slice(6);
          if (data === '[DONE]') continue;
          try {
            const json = JSON.parse(data);
            if (json.token) {
              fullText += json.token;
              // 更新占位消息的内容
              messages.value[assistantMsgIndex].content = fullText;
              scrollToBottom();
            }
          } catch (e) {
            console.warn('JSON parse error', e);
          }
        }
      }
    }

    if (!fullText) {
      messages.value[assistantMsgIndex].content = '(无响应内容)';
    }
  } catch (err) {
    console.error('Request error:', err);
    messages.value[assistantMsgIndex].content = '连接失败,请稍后重试。';
  } finally {
    isLoading.value = false;
    scrollToBottom();
  }
}

这里主要其实 也是很简单的 是把文字拼接起来了 然后我们可能就看到 文字一部分一部分出来 交互效果就会特别好

后端我使用node 服务写的

我也贴下

复制代码
import { Controller, Get, Query, Inject } from '@midwayjs/core';
import { Context } from '@midwayjs/koa';
import { BusinessChatService } from '../service/agent';

@Controller('/ai')
export class ChatController {
  @Inject()
  businessChatService: BusinessChatService;

  @Inject()
  ctx: Context;

  @Get('/chat')
  async chatStream(@Query('input') input: string) {
    if (!input) {
      this.ctx.status = 400;
      this.ctx.body = { error: '请输入问题' };
      return;
    }

    // 设置 SSE 响应头
    this.ctx.set({
      'Content-Type': 'text/event-stream',
      'Cache-Control': 'no-cache',
      Connection: 'keep-alive',
    });

    const res = this.ctx.res;
    // 监听客户端断开连接
    let isClosed = false;
    const onClose = () => {
      isClosed = true;
    };
    res.on('close', onClose);

    try {
      await this.businessChatService.chatStream(input, (token) => {
        if (!isClosed) {
          res.write(`data: ${JSON.stringify({ token })}\n\n`);
        }
      });
      if (!isClosed) {
        res.write(`data: [DONE]\n\n`);
        res.end();
      }
    } catch (err) {
      console.error('Stream error:', err);
      if (!isClosed) {
        res.write(`data: ${JSON.stringify({ token: '处理出错,请重试' })}\n\n`);
        res.write(`data: [DONE]\n\n`);
        res.end();
      }
    } finally {
      res.removeListener('close', onClose);
    }
  }
}

这个controller

我把前端代码 全部贴一下

复制代码
<template>
  <view class="chat-container">
    <!-- 消息列表 -->
    <view class="message-list">
      <view
        v-for="(msg, idx) in messages"
        :key="idx"
        :class="['message', msg.role]"
      >
        <view class="bubble">{{ msg.content }}</view>
      </view>
      <!-- 加载中提示 -->
      <view v-if="isLoading" class="loading">思考中...</view>
    </view>

    <!-- 底部输入区 -->
    <view class="input-area">
      <input
        type="text"
        v-model="inputText"
        placeholder="输入你的问题..."
        @confirm="sendMessage"
        :disabled="isLoading"
      />
      <button @click="sendMessage" :disabled="isLoading">发送</button>
    </view>
  </view>
</template>

<script setup>
import { ref, reactive, nextTick } from 'vue';

// 消息列表
const messages = ref([
  {
    role: 'assistant',
    content: '你好!我是 AI 助手,可以查询天气、回答你的问题。试试说"北京天气怎么样?"',
  },
]);

// 输入框内容
const inputText = ref('');
// 是否正在请求(显示加载)
const isLoading = ref(false);

// 后端接口地址(请根据实际部署修改)
const API_URL = 'http://192.168.110.45:8001/ai/chat';

// 滚动到底部
function scrollToBottom() {
  nextTick(() => {
    const query = uni.createSelectorQuery();
    query.select('.message-list').boundingClientRect();
    query.exec((res) => {
      if (res[0]) {
        uni.pageScrollTo({
          scrollTop: res[0].height,
          duration: 100,
        });
      }
    });
  });
}

// 发送消息
async function sendMessage() {
  const question = inputText.value.trim();
  if (!question || isLoading.value) return;

  // 添加用户消息
  messages.value.push({ role: 'user', content: question });
  inputText.value = '';
  scrollToBottom();

  // 显示加载状态
  isLoading.value = true;

  // 创建一个临时的 AI 消息占位,用于流式追加
  const assistantMsgIndex = messages.value.length;
  messages.value.push({ role: 'assistant', content: '' });

  let fullText = '';

  try {
    // 注意:uni-app 的 H5 端支持 fetch,但 App 端可能需要使用 uni.request 并自行处理流式
    // 这里以 H5 为例,使用 fetch 读取 ReadableStream
    const response = await fetch(`${API_URL}?input=${encodeURIComponent(question)}`);

    // if (!response.ok) {
    //   throw new Error(`HTTP ${response.status}`);
    // }

    const reader = response.body.getReader();
    const decoder = new TextDecoder();

    while (true) {
      const { done, value } = await reader.read();
      if (done) break;
      const chunk = decoder.decode(value, { stream: true });
      const lines = chunk.split('\n');
      for (const line of lines) {
        if (line.startsWith('data: ')) {
          const data = line.slice(6);
          if (data === '[DONE]') continue;
          try {
            const json = JSON.parse(data);
            if (json.token) {
              fullText += json.token;
              // 更新占位消息的内容
              messages.value[assistantMsgIndex].content = fullText;
              scrollToBottom();
            }
          } catch (e) {
            console.warn('JSON parse error', e);
          }
        }
      }
    }

    if (!fullText) {
      messages.value[assistantMsgIndex].content = '(无响应内容)';
    }
  } catch (err) {
    console.error('Request error:', err);
    messages.value[assistantMsgIndex].content = '连接失败,请稍后重试。';
  } finally {
    isLoading.value = false;
    scrollToBottom();
  }
}
</script>

<style scoped>
/* 全局样式,使用 rpx 适配移动端 */
.chat-container {
  display: flex;
  flex-direction: column;
  height: 100vh;
  background-color: #f5f5f5;
}

.message-list {
  flex: 1;
  overflow-y: auto;
  padding: 20rpx 30rpx;
}

.message {
  margin-bottom: 20rpx;
  display: flex;
}

.message.user {
  justify-content: flex-end;
}

.message.assistant {
  justify-content: flex-start;
}

.bubble {
  max-width: 80%;
  padding: 16rpx 24rpx;
  border-radius: 36rpx;
  font-size: 28rpx;
  line-height: 1.4;
  word-break: break-word;
}

.user .bubble {
  background-color: #007aff;
  color: white;
}

.assistant .bubble {
  background-color: #e5e5ea;
  color: black;
}

.loading {
  padding: 20rpx 30rpx;
  color: #666;
  font-style: italic;
  font-size: 26rpx;
}

.input-area {
  position: fixed;
  bottom: 0;
  left: 0;
  right: 0;
  background-color: white;
  padding: 16rpx 30rpx;
  border-top: 1px solid #ddd;
  display: flex;
  gap: 20rpx;
  align-items: center;
  box-sizing: border-box;
}

.input-area input {
  flex: 1;
  height: 72rpx;
  padding: 0 24rpx;
  border: 1px solid #ccc;
  border-radius: 36rpx;
  font-size: 28rpx;
  background: white;
}

.input-area button {
  background-color: #007aff;
  color: white;
  border: none;
  padding: 0 32rpx;
  height: 72rpx;
  line-height: 72rpx;
  border-radius: 36rpx;
  font-size: 28rpx;
  font-weight: normal;
}

.input-area button[disabled] {
  background-color: #aaa;
}
</style>
相关推荐
问道飞鱼3 小时前
【技术方案】面向 Web 系统的《全栈灰度部署方案设计》
前端·全栈·灰度发布
꧁꫞꯭零꯭点꯭꫞꧂3 小时前
前端面试题3
开发语言·前端·javascript
企业架构师老王3 小时前
企业级AI Agent工具功能差异深度对比:架构师视角的选型逻辑与提效实战
人工智能·ai
真心喜欢你吖3 小时前
OpenClaw安装部署Mac操作系统版 - 打造你的专属AI助理
java·人工智能·macos·ai·语言模型·智能体·openclaw
chools3 小时前
Java后端拥抱AI开发之个人学习路线 - - Spring AI【第二期】
java·人工智能·学习·spring·ai
ZC跨境爬虫3 小时前
Base64编码详解(含JS_Python实现+实战逆向案例)
前端·javascript·python
程序员夏末3 小时前
【AI Agent基础 | 第五篇】简析MCP(模型上下文协议)
人工智能·ai·ai agent
FuckPatience3 小时前
Halcon 寻找方形Mark
前端·javascript·数据库