共计 3150 个字符,预计需要花费 8 分钟才能阅读完成。
传统轮询方案的性能瓶颈
在传统的前后端交互中,很多开发者会采用轮询(Polling)的方式获取最新数据。这种方式虽然实现简单,但存在几个明显缺陷:

- 频繁的 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) 如何处理流式请求中的异常?
这些问题的探索将帮助我们打造更完善的用户体验。欢迎在评论区分享你的见解和实践经验!
正文完
