Vue3+TS极简集成指南:10分钟实现ChatGPT式流式对话效果

2,904 阅读4分钟

COVER.png

前言:为什么选择流式响应?

现代AI聊天式交互的核心需求是实时性渐进呈现
相比传统接口的"请求-等待-完整响应"模式,流式响应(Streaming Response)能带来更自然的对话体验。
今天咱们就用Vue3+TypeScript手把手实现一个ChatGPT同款效果,让你在10分钟内get核心技术!


一、技术选型:为什么不是原生EventSource?

传统方案痛点:

const source = new EventSource('/api/chat')  // 这玩意用过的都懂...
source.onmessage = (e) => { /* 处理消息 */ }
能力EventSourcefetch-event-source
自定义请求头❌ 只能URL传参✅ 支持Authorization等
请求方法仅GET✅ 支持POST/PUT
中断控制❌ 关不掉✅ AbortController掌控全局
错误重试❌ 固定3秒重试✅ 自定义重试策略
数据格式❌ 仅文本✅ 支持JSON/二进制

小结: 要玩转生产级的流式交互,@microsoft/fetch-event-source才是真香选择!


二、环境准备

npm install @microsoft/fetch-event-source  # 这才是流式处理的灵魂

三、核心实现

3.1 类型定义

// types/chat.ts
export interface Message {
  id: string         // 消息唯一标识
  role: 'user' | 'assistant'  // 发送方身份
  content: string    // 消息内容
  timestamp: number  // 时间戳
}

export type MessageChunk = {
  content: string   // 当前片段内容
  done: boolean     // 是否结束标记
}

3.2 流式处理Hook

// hooks/useChatStream.ts
import { fetchEventSource } from '@microsoft/fetch-event-source'
import { ref, type Ref } from 'vue'

export const useChatStream = () => {
  // 创建中断控制器(用来随时关闭连接)
  const controller = new AbortController()
  
  // 响应式状态管理
  const isStreaming = ref(false)  // 是否正在流式传输
  const error = ref<Error | null>(null)  // 错误信息

  const streamChat = async (messages: Ref<Message[]>, input: string) => {
    // 添加用户消息
    const userMessage: Message = {
      id: Date.now().toString(),
      role: 'user',
      content: input,
      timestamp: Date.now()
    }
    messages.value = [...messages.value, userMessage]

    // 初始化AI消息(内容为空)
    const assistantMessage: Message = {
      id: `temp_${Date.now()}`,
      role: 'assistant',
      content: '',
      timestamp: Date.now()
    }
    messages.value = [...messages.value, assistantMessage]

    isStreaming.value = true  // 开启流式传输
    
    try {
      await fetchEventSource('/api/chat', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
          Authorization: `Bearer ${localStorage.getItem('token')}`  // 携带鉴权信息
        },
        body: JSON.stringify({ message: input }),
        signal: controller.signal,  // 绑定中断信号
        
        // 连接建立时的回调
        async onopen(response) {
          if (!response.ok) throw new Error('服务器开小差了,状态码:'+response.status)
        },
        
        // 收到消息时的处理
        onmessage(ev) {
          const chunk = JSON.parse(ev.data) as MessageChunk
          const lastMsg = messages.value[messages.value.length - 1]
          
          // 拼接消息内容(类似打字机效果)
          lastMsg.content += chunk.content
          
          // 触发响应式更新(Vue3的魔法就在这里!)
          messages.value = [...messages.value.slice(0, -1), lastMsg]
        },
        
        // 连接关闭时
        onclose() {
          // 生成正式ID替换临时ID
          const lastMsg = messages.value[messages.value.length - 1]
          lastMsg.id = `msg_${Date.now()}`
          isStreaming.value = false
        },
        
        // 错误处理
        onerror(err) {
          throw new Error(`流式传输异常:${err}`)
        }
      })
    } catch (err) {
      error.value = err as Error
      isStreaming.value = false
    }
  }

  return { 
    streamChat,   // 启动流式对话
    isStreaming,  // 当前状态
    error,        // 错误对象
    cancel: () => controller.abort()  // 紧急停止方法
  }
}

3.3 聊天组件(视图层)

<!-- components/ChatWindow.vue -->
<script setup lang="ts">
import { ref } from 'vue'
import { useChatStream } from '@/hooks/useChatStream'
import type { Message } from '@/types/chat'

const messages = ref<Message[]>([])  // 消息列表
const input = ref('')  // 输入框内容
const { streamChat, isStreaming, error } = useChatStream()

const handleSubmit = () => {
  if (!input.value.trim() || isStreaming.value) return
  streamChat(messages, input.value)  // 触发流式请求
  input.value = ''  // 清空输入框
}
</script>

<template>
  <div class="chat-container">
    <!-- 消息列表 -->
    <div v-for="msg in messages" 
         :key="msg.id"
         :class="`message ${msg.role}`">
      {{ msg.content }}
    </div>
    
    <!-- 输入区域 -->
    <form @submit.prevent="handleSubmit">
      <input
        v-model="input"
        :disabled="isStreaming"
        placeholder="输入你想问的..."
      />
      <button type="submit" :disabled="isStreaming">
        {{ isStreaming ? 'AI正在思考...' : '发送' }}
      </button>
    </form>
    
    <!-- 错误提示 -->
    <div v-if="error" class="error">
      {{ error.message }} 
      <button @click="error = null">×</button>
    </div>
  </div>
</template>

四、错误重试:打造健壮系统

4.1 指数退避重试策略

// 在useChatStream.ts中扩展
const MAX_RETRIES = 3  // 最大重试次数
const BASE_DELAY = 1000  // 基础延迟1秒

const streamChat = async (/* 参数 */) => {
  let retries = 0
  
  const retry = () => {
    if (retries >= MAX_RETRIES) {
      error.value = new Error('服务器连接失败,请稍后再试')
      return
    }
    
    const delay = BASE_DELAY * Math.pow(2, retries)  // 指数退避
    retries++
    
    setTimeout(() => {
      streamChat(messages, input)  // 重新发起请求
    }, delay)
  }

  await fetchEventSource(/* ... */ {
    // ...其他配置
    onerror(err) {
      if (!controller.signal.aborted) {
        retry()  // 触发重试
      }
    }
  })
}

4.2 断点续传机制

// 在消息处理中添加lastId追踪
let lastEventId = ''

// 请求配置中追加header
headers: {
  ...,
  'Last-Event-ID': lastEventId  // 告诉服务端从哪里继续
}

// 在onmessage回调中更新lastId
onmessage(ev) {
  if (ev.id) lastEventId = ev.id
  // ...其他处理
}

五、效果增强技巧

5.1 丝滑滚动(用户体验++)

<script setup>
// 在ChatWindow.vue中添加
import { nextTick, watch } from 'vue'

const chatContainer = ref<HTMLElement>()

watch(messages, async () => {
  await nextTick()  // 等待DOM更新
  if (chatContainer.value) {
    chatContainer.value.scrollTop = chatContainer.value.scrollHeight
  }
}, { deep: true })
</script>

<template>
  <div ref="chatContainer" class="chat-container">
    <!-- 消息列表 -->
  </div>
</template>

5.2 性能优化:消息分块缓冲

// 在useChatStream.ts中
let buffer = ''  // 缓冲池
let lastRender = 0

onmessage(ev) {
  buffer += ev.data
  
  // 每100ms渲染一次(减少DOM操作)
  if (Date.now() - lastRender > 100) {
    lastMsg.content += buffer
    buffer = ''
    lastRender = Date.now()
    messages.value = [...messages.value.slice(0, -1), lastMsg]
  }
}

onclose() {
  // 处理剩余缓冲
  if (buffer) {
    lastMsg.content += buffer
    messages.value = [...messages.value.slice(0, -1), lastMsg]
  }
}

六、避坑指南

  1. 内存泄漏
    务必在组件卸载时终止连接:

    onUnmounted(() => controller.abort())
    
  2. XSS防护
    如果渲染HTML内容,记得消毒:

    import DOMPurify from 'dompurify'
    lastMsg.content = DOMPurify.sanitize(content)
    
  3. 流量控制
    服务端配置速率限制:

    // 服务端示例(Node.js)
    app.post('/api/chat', (req, res) => {
      res.setHeader('X-RateLimit-Limit', '10')  // 每分钟10次
    })
    

总结

现在你已掌握:
✅ 流式交互核心原理
✅ Vue3响应式集成技巧
✅ 生产级错误处理方案
✅ 性能优化实战经验

下一步建议:

  1. 接入OpenAI API实现真实对话
  2. 添加Markdown渲染提升可读性
  3. 实现对话历史持久化存储
# 最后友情提醒:
# 上线前记得配置CORS和HTTPS!

技术雷达:

  • 核心库:Vue3.3+ / TypeScript5+
  • 流式传输:@microsoft/fetch-event-source
  • 样式方案:UnoCSS(推荐)
  • 状态管理:Pinia(复杂场景使用)

准备好迎接用户的"Wow Effect"了吗?现在就去打造属于你的智能对话应用吧!


(完) 以上完整代码已经分享到Gitee, 如有所需欢迎自取