Vue3实战:如何优雅实现ChatGPT流式输出(附完整代码)

6次阅读
没有评论

共计 3150 个字符,预计需要花费 8 分钟才能阅读完成。

image.webp

传统轮询方案的性能瓶颈

在传统的前后端交互中,很多开发者会采用轮询(Polling)的方式获取最新数据。这种方式虽然实现简单,但存在几个明显缺陷:

Vue3 实战:如何优雅实现 ChatGPT 流式输出(附完整代码)

  • 频繁的 HTTP 请求会增加服务器压力
  • 数据更新存在延迟,无法实现真正的实时性
  • 在移动网络环境下会显著增加耗电量

SSE 与 WebSocket 技术选型

Server-Sent Events (SSE)

SSE 是一种服务器向浏览器推送更新的技术,特别适合文本数据流场景:

  • 基于 HTTP 协议,不需要额外的协议升级
  • 自动重连机制,连接中断后会尝试重新连接
  • 浏览器原生支持 EventSource API
  • 单向通信(服务器→客户端)

WebSocket

WebSocket 提供了全双工通信能力:

  • 需要专门的 WebSocket 服务器
  • 适合需要频繁双向通信的场景
  • 协议握手过程稍复杂

对于 ChatGPT 这类以接收服务器消息为主的场景,SSE 通常是更轻量级的选择。

核心实现方案

1. 使用 axios 拦截器处理分块流数据

// 创建自定义 axios 实例
const streamClient = axios.create({
  baseURL: 'https://api.openai.com',
  responseType: 'stream' // 关键配置
})

// 添加响应拦截器
streamClient.interceptors.response.use(response => {
  const stream = response.data
  return new Promise((resolve) => {
    let fullResponse = ''stream.on('data', chunk => {const chunkStr = chunk.toString()
      fullResponse += chunkStr
      // 这里可以 emit 自定义事件或调用回调
    })

    stream.on('end', () => {resolve(fullResponse)
    })
  })
})

2. Composition API 封装 useChatStream hook

// types.ts
type Message = {
  id: string
  content: string
  role: 'user' | 'assistant'
}

// useChatStream.ts
export function useChatStream() {const messages = ref<Message[]>([])
  const isLoading = ref(false)
  const error = ref<Error | null>(null)

  const sendMessage = async (prompt: string) => {
    try {
      isLoading.value = true
      const messageId = Date.now().toString()

      // 添加用户消息
      messages.value.push({
        id: messageId,
        content: prompt,
        role: 'user'
      })

      // 添加占位回复
      messages.value.push({id: `temp-${messageId}`,
        content: '',
        role: 'assistant'
      })

      const response = await streamClient.post('/v1/chat/completions', {
        model: 'gpt-3.5-turbo',
        messages: [{role: 'user', content: prompt}
        ],
        stream: true
      })

      // 流式更新消息内容
      response.on('data', (chunk) => {
        const assistantMessageIndex = messages.value.findIndex(m => m.id === `temp-${messageId}`
        )
        if (assistantMessageIndex !== -1) {messages.value[assistantMessageIndex].content += chunk
        }
      })

      response.on('end', () => {
        // 更新为最终消息 ID
        messages.value[assistantMessageIndex].id = `msg-${Date.now()}`
      })

    } catch (err) {error.value = err} finally {isLoading.value = false}
  }

  return {messages, isLoading, error, sendMessage}
}

3. 基于 v -for 的动态消息渲染优化

<template>
  <div class="chat-container">
    <div 
      v-for="message in messages" 
      :key="message.id"
      :class="['message', message.role]"
    >
      <div class="content">
        {{message.content}}
      </div>
    </div>

    <div v-if="isLoading" class="loading-indicator">
      AI 正在思考...
    </div>
  </div>
</template>

<style scoped>
.message {
  margin: 10px 0;
  padding: 12px;
  border-radius: 8px;
}

.message.user {
  background: #e3f2fd;
  align-self: flex-end;
}

.message.assistant {
  background: #f5f5f5;
  align-self: flex-start;
}

.loading-indicator {
  color: #666;
  padding: 8px;
  font-style: italic;
}
</style>

性能优化策略

1. 大文本分片策略

当处理大型文本流时,建议:

  • 设置合理的 chunk 大小(如 1024 字节)
  • 使用 TextDecoder 处理二进制流
  • 实现缓冲区管理,避免内存堆积

2. 浏览器内存管理

长时间运行的流式连接可能导致内存增长:

  • 定期检查 window.performance.memory
  • 考虑实现自动垃圾回收机制
  • 对于超长对话,建议分页或存档历史消息

3. 连接中断重试机制

const MAX_RETRIES = 3
let retryCount = 0

function connectWithRetry() {const eventSource = new EventSource('/api/stream')

  eventSource.onerror = () => {if (retryCount < MAX_RETRIES) {setTimeout(() => {
        retryCount++
        connectWithRetry()}, 1000 * retryCount)
    }
  }

  return eventSource
}

生产环境检查清单

CORS 配置要点

  • 确保服务器设置正确的 Access-Control-Allow-Origin
  • 对于 credentialed 请求,需要设置 Access-Control-Allow-Credentials
  • 预检请求 (OPTIONS) 的正确处理

鉴权 Token 的安全存储

  • 避免将 API 密钥直接存储在客户端代码中
  • 考虑使用 HttpOnly Cookies 或后端代理
  • 实现 token 刷新机制

流量控制策略

  • 客户端限流(如 debounce 用户输入)
  • 服务器端速率限制处理
  • 实现优雅降级方案

开放性问题

Vue3 的 Suspense 特性为异步加载提供了更优雅的解决方案。我们可以思考:

  • 如何将流式输出与 Suspense 结合?
  • 能否实现基于 Suspense 的逐字显示动画?
  • 错误边界 (Error Boundary) 如何处理流式请求中的异常?

这些问题的探索将帮助我们打造更完善的用户体验。欢迎在评论区分享你的见解和实践经验!

正文完
 0
评论(没有评论)