共计 4507 个字符,预计需要花费 12 分钟才能阅读完成。
背景痛点
在传统的对话框组件中,我们经常遇到以下几个问题:

- 状态同步延迟 :使用轮询或短轮询方式获取新消息时,会有明显的延迟感
- 消息堆积渲染卡顿 :当消息数量增多时,DOM 节点过多会导致页面卡顿
- 双向通信实现复杂 :传统的 HTTP 请求难以实现真正的实时双向通信
技术选型
在构建 ChatGPT 风格的对话框时,我们主要面临状态管理的选择:
- Vuex vs Pinia
- Vuex 更适合大型复杂应用,但学习曲线较陡
-
Pinia 更轻量,API 更简单,但生态系统不如 Vuex 成熟
-
最终选择 Composition API 的原因
- 更灵活的状态管理方式
- 更好的 TypeScript 支持
- 逻辑复用更方便
- 更细粒度的响应式控制
核心实现
WebSocket 实现双向通信
-
首先创建 WebSocket 连接
const socket = new WebSocket('wss://your-api-endpoint') -
实现消息收发
// 发送消息 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)
})
内存泄漏预防
- 清除所有定时器
- 取消所有事件监听
- 清除所有观察者
避坑指南
处理消息乱序
为每条消息添加序列号,并在客户端进行排序:
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)
// 其他错误处理逻辑
}
延伸思考
- 消息撤回功能
- 添加撤回按钮
- 发送撤回请求到服务器
-
更新本地消息状态
-
对话持久化
- 使用 IndexedDB 存储历史对话
- 实现分页加载
-
添加搜索功能
-
多语言支持
- 使用 vue-i18n
- 实现自动语言检测
-
添加翻译功能
-
消息已读回执
- 跟踪消息状态
- 发送已读确认
- 更新 UI 显示
通过本文的实现,我们构建了一个高效、可靠的 ChatGPT 风格对话框组件。这个组件不仅解决了传统对话框的痛点,还具备了良好的扩展性,可以方便地添加更多高级功能。
正文完
