Vue + ChatGPT 对话框前端实战:从零构建智能对话组件

11次阅读
没有评论

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

image.webp

背景痛点

在传统的对话框组件中,我们经常遇到以下几个问题:

Vue + ChatGPT 对话框前端实战:从零构建智能对话组件

  • 状态同步延迟 :使用轮询或短轮询方式获取新消息时,会有明显的延迟感
  • 消息堆积渲染卡顿 :当消息数量增多时,DOM 节点过多会导致页面卡顿
  • 双向通信实现复杂 :传统的 HTTP 请求难以实现真正的实时双向通信

技术选型

在构建 ChatGPT 风格的对话框时,我们主要面临状态管理的选择:

  1. Vuex vs Pinia
  2. Vuex 更适合大型复杂应用,但学习曲线较陡
  3. Pinia 更轻量,API 更简单,但生态系统不如 Vuex 成熟

  4. 最终选择 Composition API 的原因

  5. 更灵活的状态管理方式
  6. 更好的 TypeScript 支持
  7. 逻辑复用更方便
  8. 更细粒度的响应式控制

核心实现

WebSocket 实现双向通信

  1. 首先创建 WebSocket 连接

    const socket = new WebSocket('wss://your-api-endpoint')

  2. 实现消息收发

    // 发送消息
    const sendMessage = (content: string) => {if (socket.readyState === WebSocket.OPEN) {
        socket.send(JSON.stringify({
          type: 'message',
          content
        }))
      }
    }
    
    // 接收消息
    socket.onmessage = (event) => {const data = JSON.parse(event.data)
      // 处理接收到的消息
    }

基于 reactive 的对话状态管理

interface Message {
  id: string
  content: string
  sender: 'user' | 'bot'
  timestamp: number
}

const chatState = reactive({messages: [] as Message[],
  isLoading: false,
  error: null as string | null
})

虚拟滚动优化

使用 vue-virtual-scroller 组件实现长列表优化:

<template>
  <RecycleScroller
    :items="chatState.messages"
    :item-size="60"
    key-field="id"
  >
    <template #default="{item}">
      <MessageBubble :message="item" />
    </template>
  </RecycleScroller>
</template>

完整组件代码示例

以下是完整的单文件组件实现:

<template>
  <div class="chat-container">
    <div class="messages-container">
      <RecycleScroller
        :items="messages"
        :item-size="60"
        key-field="id"
      >
        <template #default="{item}">
          <MessageBubble 
            :message="item" 
            :is-typing="isTyping && item.sender ==='bot'"
          />
        </template>
      </RecycleScroller>
    </div>

    <div class="input-area">
      <input 
        v-model="inputMessage" 
        @keyup.enter="sendMessage"
        placeholder="Type your message..."
      />
      <button @click="sendMessage">Send</button>
    </div>
  </div>
</template>

<script lang="ts" setup>
import {ref, reactive, onMounted, onUnmounted} from 'vue'
import {RecycleScroller} from 'vue-virtual-scroller'
import 'vue-virtual-scroller/dist/vue-virtual-scroller.css'

interface Message {
  id: string
  content: string
  sender: 'user' | 'bot'
  timestamp: number
}

const inputMessage = ref('')
const isTyping = ref(false)

const chatState = reactive({messages: [] as Message[],
  isLoading: false,
  error: null as string | null,
  socket: null as WebSocket | null
})

// 初始化 WebSocket 连接
const initSocket = () => {chatState.socket = new WebSocket('wss://your-api-endpoint')

  chatState.socket.onopen = () => {console.log('WebSocket connected')
  }

  chatState.socket.onmessage = (event) => {const data = JSON.parse(event.data)

    if (data.type === 'message') {
      chatState.messages.push({id: Date.now().toString(),
        content: data.content,
        sender: 'bot',
        timestamp: Date.now()})
    } else if (data.type === 'typing') {isTyping.value = data.status}
  }

  chatState.socket.onerror = (error) => {console.error('WebSocket error:', error)
    chatState.error = 'Connection error'
  }

  chatState.socket.onclose = () => {console.log('WebSocket disconnected')
  }
}

// 发送消息
const sendMessage = () => {if (!inputMessage.value.trim()) return

  const message = inputMessage.value

  // 添加到本地消息列表
  chatState.messages.push({id: Date.now().toString(),
    content: message,
    sender: 'user',
    timestamp: Date.now()})

  // 发送到服务器
  if (chatState.socket && chatState.socket.readyState === WebSocket.OPEN) {
    chatState.socket.send(JSON.stringify({
      type: 'message',
      content: message
    }))
  } else {console.error('WebSocket is not connected')
    chatState.error = 'Connection lost. Trying to reconnect...'
    // 实现重连逻辑
  }

  inputMessage.value = ''
}

// 组件挂载时初始化
onMounted(() => {initSocket()
})

// 组件卸载时清理
onUnmounted(() => {if (chatState.socket) {chatState.socket.close()
  }
})
</script>

<style scoped>
.chat-container {
  display: flex;
  flex-direction: column;
  height: 100vh;
}

.messages-container {
  flex: 1;
  overflow: auto;
}

.input-area {
  padding: 10px;
  display: flex;
  border-top: 1px solid #eee;
}

.input-area input {
  flex: 1;
  padding: 8px;
  margin-right: 8px;
}
</style>

性能优化

消息分块渲染策略

对于长消息,我们可以分块渲染:

const chunkSize = 50
const visibleMessages = computed(() => {return chatState.messages.slice(-chunkSize)
})

WebSocket 连接保活

实现心跳机制保持连接:

let heartbeatInterval: number

const startHeartbeat = () => {heartbeatInterval = window.setInterval(() => {if (chatState.socket?.readyState === WebSocket.OPEN) {chatState.socket.send(JSON.stringify({ type: 'ping'}))
    }
  }, 30000) // 每 30 秒发送一次心跳
}

// 在连接成功后调用
startHeartbeat()

// 组件卸载时清除
onUnmounted(() => {clearInterval(heartbeatInterval)
})

内存泄漏预防

  1. 清除所有定时器
  2. 取消所有事件监听
  3. 清除所有观察者

避坑指南

处理消息乱序

为每条消息添加序列号,并在客户端进行排序:

interface Message {
  id: string
  seq: number
  // 其他字段
}

const orderedMessages = computed(() => {return [...chatState.messages].sort((a, b) => a.seq - b.seq)
})

敏感词过滤

实现简单的敏感词过滤:

const sensitiveWords = ['badword1', 'badword2']

const filterSensitiveWords = (text: string) => {return sensitiveWords.reduce((result, word) => {return result.replace(new RegExp(word, 'gi'), '***')
  }, text)
}

生产环境日志

使用 Sentry 或类似工具监控错误:

import * as Sentry from '@sentry/vue'

// 在 WebSocket 错误处理中
chatState.socket.onerror = (error) => {Sentry.captureException(error)
  // 其他错误处理逻辑
}

延伸思考

  1. 消息撤回功能
  2. 添加撤回按钮
  3. 发送撤回请求到服务器
  4. 更新本地消息状态

  5. 对话持久化

  6. 使用 IndexedDB 存储历史对话
  7. 实现分页加载
  8. 添加搜索功能

  9. 多语言支持

  10. 使用 vue-i18n
  11. 实现自动语言检测
  12. 添加翻译功能

  13. 消息已读回执

  14. 跟踪消息状态
  15. 发送已读确认
  16. 更新 UI 显示

通过本文的实现,我们构建了一个高效、可靠的 ChatGPT 风格对话框组件。这个组件不仅解决了传统对话框的痛点,还具备了良好的扩展性,可以方便地添加更多高级功能。

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