一、SSE 基本概念与工作原理
1.1 什么是 SSE?
Server-Sent Events(SSE) 是一种基于 HTTP 协议的服务器推送技术,允许服务器向客户端单向发送实时数据流。它使用简单的文本协议,通过 EventSource API 在浏览器端实现。
1.2 工作原理
┌─────────────┐ ┌─────────────┐
│ 客户端 │ ◄───────────────── │ 服务器 │
│ (Browser) │ HTTP 长连接 │ (Server) │
│ │ ◄── data: 消息1 │ │
│ EventSource │ ◄── data: 消息2 │ 持续推送 │
│ │ ◄── data: 消息3 │ │
└─────────────┘ └─────────────┘
核心特点:
- 基于 HTTP/1.1 或 HTTP/2
- 单向通信(服务器 → 客户端)
- 自动重连机制
- 文本数据格式(支持 UTF-8)
1.3 SSE 消息格式
Content-Type: text/event-stream
Cache-Control: no-cache
Connection: keep-alive
data: 这是第一条消息
data: 这是第二条消息
id: 123
event: custom-event
: 这是注释(心跳检测)
data: {"type": "json", "content": "支持JSON格式"}
二、技术对比分析
| 特性 | SSE | WebSocket | 长轮询 | 短轮询 |
|---|---|---|---|---|
| 通信方向 | 单向(服务器→客户端) | 双向 | 单向 | 单向 |
| 协议 | HTTP | WebSocket (ws/wss) | HTTP | HTTP |
| 实时性 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ | ⭐ |
| 复杂度 | 低 | 高 | 中 | 低 |
| 自动重连 | ✅ 原生支持 | ❌ 需手动实现 | ❌ | ❌ |
| 浏览器支持 | 主流浏览器 | 主流浏览器 | 全部 | 全部 |
| 防火墙穿透 | ✅ 容易 | ⚠️ 可能受阻 | ✅ 容易 | ✅ 容易 |
| 二进制支持 | ❌ 仅文本 | ✅ 支持 | ✅ 支持 | ✅ 支持 |
| 连接数限制 | HTTP/1.1: 6个/域 | 无限制 | HTTP限制 | HTTP限制 |
选型建议
- 选择 SSE:AI 流式输出、股票行情、新闻推送、日志实时展示
- 选择 WebSocket:即时聊天、在线游戏、协同编辑、需要双向高频通信
- 选择轮询:兼容性要求极高、简单场景、数据更新频率低
三、React 中实现 SSE 客户端
3.1 基础 SSE Hook
在你的 Vite + React 项目中创建以下文件:
// src/hooks/useSSE.ts
import { useEffect, useRef, useCallback, useState } from 'react';
export interface SSEMessage {
id?: string;
event?: string;
data: string;
}
export interface UseSSEOptions {
url: string;
withCredentials?: boolean;
onMessage?: (message: SSEMessage) => void;
onOpen?: () => void;
onError?: (error: Event) => void;
onClose?: () => void;
reconnectInterval?: number;
maxReconnectAttempts?: number;
}
export interface UseSSEReturn {
isConnected: boolean;
lastMessage: SSEMessage | null;
error: Event | null;
connect: () => void;
disconnect: () => void;
}
export const useSSE = (options: UseSSEOptions): UseSSEReturn => {
const {
url,
withCredentials = false,
onMessage,
onOpen,
onError,
onClose,
reconnectInterval = 3000,
maxReconnectAttempts = 5,
} = options;
const [isConnected, setIsConnected] = useState(false);
const [lastMessage, setLastMessage] = useState<SSEMessage | null>(null);
const [error, setError] = useState<Event | null>(null);
const eventSourceRef = useRef<EventSource | null>(null);
const reconnectAttemptsRef = useRef(0);
const reconnectTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
// 清理函数
const cleanup = useCallback(() => {
if (reconnectTimerRef.current) {
clearTimeout(reconnectTimerRef.current);
reconnectTimerRef.current = null;
}
if (eventSourceRef.current) {
eventSourceRef.current.close();
eventSourceRef.current = null;
}
}, []);
// 建立连接
const connect = useCallback(() => {
cleanup();
try {
const es = new EventSource(url, { withCredentials });
eventSourceRef.current = es;
// 连接打开
es.onopen = () => {
console.log('[SSE] 连接已建立');
setIsConnected(true);
setError(null);
reconnectAttemptsRef.current = 0;
onOpen?.();
};
// 接收消息
es.onmessage = (event) => {
const message: SSEMessage = {
id: event.lastEventId || undefined,
data: event.data,
};
setLastMessage(message);
onMessage?.(message);
};
// 处理自定义事件
es.addEventListener('custom-event', (event: MessageEvent) => {
const message: SSEMessage = {
id: event.lastEventId || undefined,
event: event.type,
data: event.data,
};
setLastMessage(message);
onMessage?.(message);
});
// 错误处理
es.onerror = (err) => {
console.error('[SSE] 连接错误:', err);
setIsConnected(false);
setError(err);
onError?.(err);
// 自动重连逻辑
if (reconnectAttemptsRef.current < maxReconnectAttempts) {
reconnectAttemptsRef.current++;
console.log(`[SSE] ${reconnectInterval}ms 后尝试第 ${reconnectAttemptsRef.current} 次重连...`);
reconnectTimerRef.current = setTimeout(() => {
connect();
}, reconnectInterval);
} else {
console.error('[SSE] 达到最大重连次数,停止重连');
cleanup();
onClose?.();
}
};
} catch (err) {
console.error('[SSE] 创建连接失败:', err);
setError(err as Event);
}
}, [url, withCredentials, onMessage, onOpen, onError, onClose, reconnectInterval, maxReconnectAttempts, cleanup]);
// 断开连接
const disconnect = useCallback(() => {
console.log('[SSE] 手动断开连接');
cleanup();
setIsConnected(false);
reconnectAttemptsRef.current = maxReconnectAttempts; // 防止自动重连
onClose?.();
}, [cleanup, maxReconnectAttempts, onClose]);
// 组件卸载时清理
useEffect(() => {
return () => {
cleanup();
};
}, [cleanup]);
return {
isConnected,
lastMessage,
error,
connect,
disconnect,
};
};
3.2 使用示例组件
// src/components/SSEDemo.tsx
import { useSSE } from '../hooks/useSSE';
export const SSEDemo: React.FC = () => {
const { isConnected, lastMessage, error, connect, disconnect } = useSSE({
url: 'http://localhost:3000/api/sse',
onMessage: (msg) => {
console.log('收到消息:', msg.data);
},
onOpen: () => {
console.log('SSE 连接成功');
},
onError: (err) => {
console.error('SSE 错误:', err);
},
reconnectInterval: 3000,
maxReconnectAttempts: 5,
});
return (
<div className="p-6 max-w-2xl mx-auto">
<h2 className="text-2xl font-bold mb-4">SSE 连接演示</h2>
<div className="mb-4">
<span className="font-semibold">连接状态: </span>
<span className={isConnected ? 'text-green-600' : 'text-red-600'}>
{isConnected ? '🟢 已连接' : '🔴 未连接'}
</span>
</div>
{error && (
<div className="mb-4 p-3 bg-red-100 text-red-700 rounded">
连接错误: {error.type}
</div>
)}
<div className="mb-4">
<span className="font-semibold">最新消息: </span>
<div className="mt-2 p-3 bg-gray-100 rounded min-h-[60px]">
{lastMessage?.data || '等待消息...'}
</div>
</div>
<div className="space-x-4">
<button
onClick={connect}
disabled={isConnected}
className="px-4 py-2 bg-blue-600 text-white rounded disabled:bg-gray-400"
>
连接
</button>
<button
onClick={disconnect}
disabled={!isConnected}
className="px-4 py-2 bg-red-600 text-white rounded disabled:bg-gray-400"
>
断开
</button>
</div>
</div>
);
};
四、连接生命周期管理
4.1 完整生命周期图示
┌─────────┐ connect() ┌─────────┐
│ 初始状态 │ ─────────────► │ 连接中 │
│ (closed) │ │(connecting)
└─────────┘ └────┬────┘
▲ │
│ onopen │
│ ▼
│ ┌─────────┐
│ onerror + 重连超限 │ 已连接 │
└─────────────────── │ (open) │
└────┬────┘
│
┌────────────────┼────────────────┐
│ │ │
▼ ▼ ▼
onmessage 自定义事件 disconnect()
(接收数据) (custom-event) (手动断开)
│ │ │
└────────────────┘ │
│ ▼
│ ┌─────────┐
└──────────►│ 已关闭 │
│ (closed) │
└─────────┘
4.2 高级连接管理 Hook
// src/hooks/useSSEAdvanced.ts
import { useEffect, useRef, useCallback, useState, useReducer } from 'react';
// 连接状态机
type ConnectionState =
| 'idle'
| 'connecting'
| 'connected'
| 'reconnecting'
| 'closed'
| 'error';
interface SSEState {
status: ConnectionState;
messages: string[];
error: Error | null;
reconnectCount: number;
}
type SSEAction =
| { type: 'CONNECTING' }
| { type: 'CONNECTED' }
| { type: 'MESSAGE_RECEIVED'; payload: string }
| { type: 'ERROR'; payload: Error }
| { type: 'RECONNECTING' }
| { type: 'CLOSED' }
| { type: 'CLEAR_MESSAGES' };
const initialState: SSEState = {
status: 'idle',
messages: [],
error: null,
reconnectCount: 0,
};
function sseReducer(state: SSEState, action: SSEAction): SSEState {
switch (action.type) {
case 'CONNECTING':
return { ...state, status: 'connecting', error: null };
case 'CONNECTED':
return { ...state, status: 'connected', error: null, reconnectCount: 0 };
case 'MESSAGE_RECEIVED':
return { ...state, messages: [...state.messages, action.payload] };
case 'ERROR':
return { ...state, status: 'error', error: action.payload };
case 'RECONNECTING':
return {
...state,
status: 'reconnecting',
reconnectCount: state.reconnectCount + 1
};
case 'CLOSED':
return { ...state, status: 'closed' };
case 'CLEAR_MESSAGES':
return { ...state, messages: [] };
default:
return state;
}
}
export const useSSEAdvanced = (url: string) => {
const [state, dispatch] = useReducer(sseReducer, initialState);
const esRef = useRef<EventSource | null>(null);
const shouldReconnectRef = useRef(true);
// 清理连接
const cleanup = useCallback(() => {
if (esRef.current) {
// 移除所有事件监听器
esRef.current.onopen = null;
esRef.current.onmessage = null;
esRef.current.onerror = null;
esRef.current.close();
esRef.current = null;
}
}, []);
// 连接函数
const connect = useCallback(() => {
cleanup();
dispatch({ type: 'CONNECTING' });
try {
const es = new EventSource(url);
esRef.current = es;
es.onopen = () => {
dispatch({ type: 'CONNECTED' });
};
es.onmessage = (event) => {
// 处理心跳消息
if (event.data === 'ping') {
console.log('[SSE] 收到心跳');
return;
}
dispatch({ type: 'MESSAGE_RECEIVED', payload: event.data });
};
es.onerror = (error) => {
dispatch({
type: 'ERROR',
payload: new Error('SSE 连接错误')
});
if (shouldReconnectRef.current) {
dispatch({ type: 'RECONNECTING' });
// 指数退避重连
const delay = Math.min(1000 * Math.pow(2, state.reconnectCount), 30000);
setTimeout(() => {
if (shouldReconnectRef.current) {
connect();
}
}, delay);
}
};
} catch (err) {
dispatch({
type: 'ERROR',
payload: err instanceof Error ? err : new Error('未知错误')
});
}
}, [url, cleanup, state.reconnectCount]);
// 断开连接
const disconnect = useCallback(() => {
shouldReconnectRef.current = false;
cleanup();
dispatch({ type: 'CLOSED' });
}, [cleanup]);
// 清空消息
const clearMessages = useCallback(() => {
dispatch({ type: 'CLEAR_MESSAGES' });
}, []);
// 组件卸载清理
useEffect(() => {
return () => {
shouldReconnectRef.current = false;
cleanup();
};
}, [cleanup]);
return {
...state,
connect,
disconnect,
clearMessages,
isConnected: state.status === 'connected',
isConnecting: state.status === 'connecting',
isReconnecting: state.status === 'reconnecting',
};
};
五、AI 对话流式输出实战
5.1 核心 Hook:useChatStream
// src/hooks/useChatStream.ts
import { useState, useCallback, useRef } from 'react';
export interface ChatMessage {
id: string;
role: 'user' | 'assistant' | 'system';
content: string;
timestamp: number;
isStreaming?: boolean;
}
interface StreamChunk {
content?: string;
done?: boolean;
error?: string;
}
export const useChatStream = (apiEndpoint: string) => {
const [messages, setMessages] = useState<ChatMessage[]>([]);
const [isStreaming, setIsStreaming] = useState(false);
const [streamingContent, setStreamingContent] = useState('');
const abortControllerRef = useRef<AbortController | null>(null);
// 生成唯一 ID
const generateId = () => `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
// 发送消息并接收流式响应
const sendMessage = useCallback(async (content: string) => {
if (!content.trim() || isStreaming) return;
// 添加用户消息
const userMessage: ChatMessage = {
id: generateId(),
role: 'user',
content: content.trim(),
timestamp: Date.now(),
};
// 创建 AI 回复占位
const assistantMessageId = generateId();
const assistantMessage: ChatMessage = {
id: assistantMessageId,
role: 'assistant',
content: '',
timestamp: Date.now(),
isStreaming: true,
};
setMessages(prev => [...prev, userMessage, assistantMessage]);
setIsStreaming(true);
setStreamingContent('');
// 创建 AbortController 用于取消请求
abortControllerRef.current = new AbortController();
try {
const response = await fetch(apiEndpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': 'text/event-stream',
},
body: JSON.stringify({
messages: [...messages, userMessage].map(m => ({
role: m.role,
content: m.content,
})),
stream: true,
}),
signal: abortControllerRef.current.signal,
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
if (!response.body) {
throw new Error('Response body is null');
}
const reader = response.body.getReader();
const decoder = new TextDecoder();
let accumulatedContent = '';
// 读取流式数据
while (true) {
const { done, value } = await reader.read();
if (done) break;
// 解码数据块
const chunk = decoder.decode(value, { stream: true });
// 解析 SSE 格式的数据
const lines = chunk.split('\n');
for (const line of lines) {
if (line.startsWith('data: ')) {
const data = line.slice(6).trim();
// 处理结束标记
if (data === '[DONE]') {
continue;
}
try {
const parsed: StreamChunk = JSON.parse(data);
if (parsed.error) {
throw new Error(parsed.error);
}
if (parsed.content) {
accumulatedContent += parsed.content;
setStreamingContent(accumulatedContent);
// 实时更新消息列表
setMessages(prev =>
prev.map(msg =>
msg.id === assistantMessageId
? { ...msg, content: accumulatedContent }
: msg
)
);
}
if (parsed.done) {
break;
}
} catch (e) {
// 非 JSON 格式,直接追加
if (data && data !== '[DONE]') {
accumulatedContent += data;
setStreamingContent(accumulatedContent);
setMessages(prev =>
prev.map(msg =>
msg.id === assistantMessageId
? { ...msg, content: accumulatedContent }
: msg
)
);
}
}
}
}
}
// 标记流式传输完成
setMessages(prev =>
prev.map(msg =>
msg.id === assistantMessageId
? { ...msg, isStreaming: false }
: msg
)
);
} catch (error) {
if (error instanceof Error && error.name === 'AbortError') {
console.log('请求被取消');
} else {
console.error('流式请求错误:', error);
// 更新消息显示错误
setMessages(prev =>
prev.map(msg =>
msg.id === assistantMessageId
? { ...msg, content: '❌ 发送失败,请重试', isStreaming: false }
: msg
)
);
}
} finally {
setIsStreaming(false);
setStreamingContent('');
abortControllerRef.current = null;
}
}, [apiEndpoint, messages, isStreaming]);
// 停止生成
const stopGeneration = useCallback(() => {
if (abortControllerRef.current) {
abortControllerRef.current.abort();
abortControllerRef.current = null;
// 标记最后一条消息停止流式传输
setMessages(prev => {
const lastMsg = prev[prev.length - 1];
if (lastMsg?.role === 'assistant' && lastMsg.isStreaming) {
return prev.map((msg, idx) =>
idx === prev.length - 1
? { ...msg, isStreaming: false }
: msg
);
}
return prev;
});
setIsStreaming(false);
}
}, []);
// 清空对话
const clearChat = useCallback(() => {
stopGeneration();
setMessages([]);
}, [stopGeneration]);
return {
messages,
isStreaming,
streamingContent,
sendMessage,
stopGeneration,
clearChat,
};
};
5.2 聊天界面组件
// src/components/ChatInterface.tsx
import React, { useState, useRef, useEffect } from 'react';
import { useChatStream } from '../hooks/useChatStream';
import { Send, Square, Trash2, Bot, User } from 'lucide-react'; // 需要安装 lucide-react
export const ChatInterface: React.FC = () => {
const [input, setInput] = useState('');
const messagesEndRef = useRef<HTMLDivElement>(null);
const inputRef = useRef<HTMLTextAreaElement>(null);
const {
messages,
isStreaming,
sendMessage,
stopGeneration,
clearChat,
} = useChatStream('/api/chat');
// 自动滚动到底部
useEffect(() => {
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
}, [messages]);
// 处理发送
const handleSend = () => {
if (!input.trim() || isStreaming) return;
sendMessage(input);
setInput('');
// 重置输入框高度
if (inputRef.current) {
inputRef.current.style.height = 'auto';
}
};
// 处理键盘事件
const handleKeyDown = (e: React.KeyboardEvent) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
handleSend();
}
};
// 自动调整输入框高度
const handleInputChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
setInput(e.target.value);
e.target.style.height = 'auto';
e.target.style.height = `${Math.min(e.target.scrollHeight, 200)}px`;
};
return (
<div className="flex flex-col h-screen bg-gray-50">
{/* 头部 */}
<header className="bg-white border-b px-6 py-4 flex items-center justify-between">
<div className="flex items-center gap-3">
<Bot className="w-8 h-8 text-blue-600" />
<h1 className="text-xl font-semibold">AI 助手</h1>
</div>
<button
onClick={clearChat}
className="p-2 text-gray-500 hover:text-red-600 hover:bg-red-50 rounded-lg transition-colors"
title="清空对话"
>
<Trash2 className="w-5 h-5" />
</button>
</header>
{/* 消息列表 */}
<div className="flex-1 overflow-y-auto p-6 space-y-6">
{messages.length === 0 ? (
<div className="text-center text-gray-400 mt-20">
<Bot className="w-16 h-16 mx-auto mb-4 opacity-50" />
<p className="text-lg">开始与 AI 对话吧</p>
<p className="text-sm mt-2">输入问题,AI 将流式回复</p>
</div>
) : (
messages.map((msg) => (
<div
key={msg.id}
className={`flex gap-4 ${
msg.role === 'user' ? 'flex-row-reverse' : ''
}`}
>
{/* 头像 */}
<div
className={`w-10 h-10 rounded-full flex items-center justify-center flex-shrink-0 ${
msg.role === 'user'
? 'bg-blue-600 text-white'
: 'bg-gray-200 text-gray-600'
}`}
>
{msg.role === 'user' ? (
<User className="w-5 h-5" />
) : (
<Bot className="w-5 h-5" />
)}
</div>
{/* 消息内容 */}
<div
className={`max-w-[80%] rounded-2xl px-5 py-3 ${
msg.role === 'user'
? 'bg-blue-600 text-white'
: 'bg-white border shadow-sm'
}`}
>
<div className="whitespace-pre-wrap leading-relaxed">
{msg.content}
{msg.isStreaming && (
<span className="inline-block w-2 h-5 ml-1 bg-current animate-pulse" />
)}
</div>
<div
className={`text-xs mt-2 ${
msg.role === 'user' ? 'text-blue-200' : 'text-gray-400'
}`}
>
{new Date(msg.timestamp).toLocaleTimeString()}
</div>
</div>
</div>
))
)}
<div ref={messagesEndRef} />
</div>
{/* 输入区域 */}
<div className="bg-white border-t p-4">
<div className="max-w-4xl mx-auto flex gap-3">
<div className="flex-1 relative">
<textarea
ref={inputRef}
value={input}
onChange={handleInputChange}
onKeyDown={handleKeyDown}
placeholder="输入消息... (Shift+Enter 换行)"
disabled={isStreaming}
rows={1}
className="w-full px-4 py-3 pr-12 border rounded-xl resize-none focus:outline-none focus:ring-2 focus:ring-blue-500 disabled:bg-gray-100"
/>
<div className="absolute right-3 bottom-3 text-xs text-gray-400">
{input.length} 字
</div>
</div>
{isStreaming ? (
<button
onClick={stopGeneration}
className="px-6 py-3 bg-red-600 text-white rounded-xl hover:bg-red-700 transition-colors flex items-center gap-2"
>
<Square className="w-4 h-4 fill-current" />
停止
</button>
) : (
<button
onClick={handleSend}
disabled={!input.trim()}
className="px-6 py-3 bg-blue-600 text-white rounded-xl hover:bg-blue-700 disabled:bg-gray-300 disabled:cursor-not-allowed transition-colors flex items-center gap-2"
>
<Send className="w-4 h-4" />
发送
</button>
)}
</div>
<p className="text-center text-xs text-gray-400 mt-2">
AI 生成的内容仅供参考
</p>
</div>
</div>
);
};
5.3 模拟后端(用于测试)
// src/mocks/mockAIStream.ts
// 用于开发测试的模拟 SSE 服务端
export const mockAIStream = () => {
const mockResponses = [
"你好!我是 AI 助手。",
"我可以帮助你解答问题、编写代码、分析数据等。",
"这是一个模拟的流式响应,用于测试前端 SSE 功能。",
"在实际应用中,这里会是真实的 AI 模型返回的内容。",
"SSE 流式输出可以让用户实时看到 AI 的思考过程,提升用户体验。"
];
return new ReadableStream({
async start(controller) {
const encoder = new TextEncoder();
for (const text of mockResponses) {
// 模拟逐字输出
for (const char of text) {
const chunk = {
content: char,
done: false
};
controller.enqueue(
encoder.encode(`data: ${JSON.stringify(chunk)}\n\n`)
);
// 模拟打字延迟
await new Promise(resolve => setTimeout(resolve, 50));
}
// 句子间停顿
await new Promise(resolve => setTimeout(resolve, 300));
}
// 发送结束标记
controller.enqueue(encoder.encode('data: [DONE]\n\n'));
controller.close();
}
});
};
// 在开发环境中拦截 fetch
if (import.meta.env.DEV) {
const originalFetch = window.fetch;
window.fetch = async (input: RequestInfo | URL, init?: RequestInit) => {
const url = input.toString();
if (url.includes('/api/chat')) {
const stream = mockAIStream();
return new Response(stream, {
headers: {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive',
},
});
}
return originalFetch(input, init);
};
}
5.4 集成到 App
// src/App.tsx
import { ChatInterface } from './components/ChatInterface';
import './App.css';
function App() {
return (
<div className="min-h-screen bg-gray-50">
<ChatInterface />
</div>
);
}
export default App;
六、优势、局限性与最佳实践
6.1 SSE 的优势
| 优势 | 说明 |
|---|---|
| 简单易用 | 基于 HTTP,无需额外协议升级 |
| 自动重连 | 原生支持,无需手动实现 |
| 轻量级 | 单向通信,开销小于 WebSocket |
| 兼容性好 | 支持 HTTP/2 多路复用 |
| 调试方便 | 纯文本协议,易于抓包分析 |
6.2 SSE 的局限性
| 局限性 | 解决方案 |
|---|---|
| 单向通信 | 需要双向时用 WebSocket 或配合 HTTP POST |
| 仅支持文本 | 二进制数据用 Base64 编码 |
| 连接数限制 | HTTP/2 可解决,或使用多个域名 |
| 不支持自定义 Header | 使用 Cookie 或 URL 参数传递认证信息 |
6.3 最佳实践清单
✅ 应该做的
// 1. 始终处理连接错误
es.onerror = (error) => {
console.error('SSE 错误:', error);
// 实现重连逻辑
};
// 2. 使用 AbortController 支持取消
const controller = new AbortController();
fetch(url, { signal: controller.signal });
// 3. 实现心跳检测
setInterval(() => {
if (Date.now() - lastMessageTime > 30000) {
reconnect();
}
}, 10000);
// 4. 合理设置重连策略
const backoff = Math.min(1000 * Math.pow(2, attempts), 30000);
// 5. 组件卸载时清理
useEffect(() => {
return () => {
eventSource.close();
};
}, []);
❌ 不应该做的
// 1. 不要忽略错误处理
// ❌ 错误
es.onerror = null;
// 2. 不要在连接未关闭时创建新连接
// ❌ 错误
if (!es) {
es = new EventSource(url); // 可能已有连接
}
// 3. 不要忘记清理事件监听器
// ❌ 错误
// 直接关闭而不移除监听器可能导致内存泄漏
// 4. 不要在前一个请求未完成时发送新请求
// ❌ 错误
const send = () => {
fetch('/api/chat', { body: JSON.stringify({ msg }) });
// 没有检查是否正在流式传输
};
6.4 性能优化建议
// 1. 使用虚拟列表渲染大量消息
import { Virtuoso } from 'react-virtuoso';
<Virtuoso
data={messages}
itemContent={(index, message) => <MessageItem {...message} />}
/>
// 2. 防抖处理输入
import { useDebouncedCallback } from 'use-debounce';
const debouncedSend = useDebouncedCallback(sendMessage, 300);
// 3. 使用 Web Worker 处理消息解析
// worker.ts
self.onmessage = (e) => {
const parsed = parseSSEChunk(e.data);
self.postMessage(parsed);
};
// 4. 启用 HTTP/2 服务器推送(如果支持)
总结
SSE 是实现 AI 对话流式输出的理想选择,它简单、可靠且易于维护。在 React 项目中,通过自定义 Hook 可以有效管理 SSE 连接的生命周期,结合 TypeScript 可以获得完整的类型支持。
关键要点:
- 使用
EventSource处理简单场景 - 使用
fetch+ReadableStream处理需要自定义请求头的场景 - 始终实现错误处理和自动重连
- 注意组件卸载时的资源清理
- 针对 AI 场景优化用户体验(打字机效果、停止生成等)
希望这篇指南能帮助你顺利实现 SSE 流式请求功能!如有问题,欢迎继续讨论。