Server-Sent Events (SSE) 前端开发指南
1. SSE 基本原理
1.1 什么是 SSE
Server-Sent Events (SSE) 是一种Web标准,允许服务器向客户端推送实时数据。它是基于HTTP协议的单向通信机制,服务器可以持续向客户端发送数据流。
1.2 工作原理
- 客户端通过
EventSourceAPI 建立与服务器的连接 - 服务器保持连接开放,使用
text/event-stream内容类型 - 服务器可以持续发送数据,客户端实时接收
- 连接断开时自动重连
1.3 与 WebSocket 的区别
| 特性 | SSE | WebSocket |
|---|---|---|
| 通信方向 | 单向(服务器→客户端) | 双向 |
| 协议 | HTTP | WebSocket |
| 自动重连 | 支持 | 需要手动实现 |
| 浏览器支持 | 现代浏览器都支持 | 现代浏览器都支持 |
| 实现复杂度 | 简单 | 相对复杂 |
2. SSE 优缺点分析
2.1 优点
- 简单易用: 基于标准HTTP协议,实现简单
- 自动重连: 内置重连机制,无需手动处理
- 浏览器原生支持: 现代浏览器都支持,无需额外库
- HTTP兼容性: 可以通过代理、防火墙,支持认证
- 轻量级: 相比WebSocket更轻量,适合简单推送场景
2.2 缺点
- 单向通信: 只能服务器向客户端推送,无法客户端向服务器发送数据
- 连接限制: 浏览器对同一域名的连接数有限制(通常6个)
- 数据格式限制: 只能发送文本数据,需要手动解析
- 连接稳定性: 长时间连接可能因为网络波动而断开
- 错误处理: 错误处理机制相对简单,难以区分不同类型的错误
- 调试困难: 网络层面的问题难以调试
2.3 适用场景
- 实时通知推送
- 数据流更新
- 日志流传输
- 简单的实时数据展示
- 不需要双向通信的场景
3. 替代方案和解决方案
3.1 Fetch API + 轮询
class FetchPollingManager {
constructor(url, options = {}) {
this.url = url;
this.options = {
interval: 1000,
maxRetries: 3,
...options
};
this.isPolling = false;
this.retryCount = 0;
this.controller = null;
}
start() {
if (this.isPolling) return;
this.isPolling = true;
this.poll();
}
async poll() {
while (this.isPolling) {
try {
this.controller = new AbortController();
const response = await fetch(this.url, {
signal: this.controller.signal,
headers: {
'Cache-Control': 'no-cache',
'Pragma': 'no-cache'
}
});
if (response.ok) {
const data = await response.json();
this.handleData(data);
this.retryCount = 0;
} else {
throw new Error(`HTTP ${response.status}`);
}
} catch (error) {
if (error.name === 'AbortError') {
break;
}
this.handleError(error);
}
await this.delay(this.options.interval);
}
}
stop() {
this.isPolling = false;
if (this.controller) {
this.controller.abort();
}
}
handleData(data) {
// 触发自定义事件
window.dispatchEvent(new CustomEvent('fetch-data', { detail: data }));
}
handleError(error) {
this.retryCount++;
if (this.retryCount >= this.options.maxRetries) {
console.error('达到最大重试次数,停止轮询');
this.stop();
} else {
console.warn(`轮询错误,${this.options.interval}ms后重试:`, error);
}
}
delay(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
}
// 使用示例
const pollingManager = new FetchPollingManager('/api/updates', { interval: 2000 });
pollingManager.start();
window.addEventListener('fetch-data', (event) => {
console.log('收到数据:', event.detail);
});
3.2 Fetch API + 长轮询 (Long Polling)
class LongPollingManager {
constructor(url, options = {}) {
this.url = url;
this.options = {
timeout: 30000,
maxRetries: 3,
...options
};
this.isPolling = false;
this.retryCount = 0;
this.controller = null;
}
async start() {
if (this.isPolling) return;
this.isPolling = true;
await this.longPoll();
}
async longPoll() {
while (this.isPolling) {
try {
this.controller = new AbortController();
const timeoutId = setTimeout(() => {
this.controller.abort();
}, this.options.timeout);
const response = await fetch(this.url, {
signal: this.controller.signal,
headers: {
'Cache-Control': 'no-cache',
'Pragma': 'no-cache'
}
});
clearTimeout(timeoutId);
if (response.ok) {
const data = await response.json();
this.handleData(data);
this.retryCount = 0;
} else {
throw new Error(`HTTP ${response.status}`);
}
} catch (error) {
if (error.name === 'AbortError') {
console.log('长轮询超时,重新开始');
continue;
}
this.handleError(error);
}
}
}
stop() {
this.isPolling = false;
if (this.controller) {
this.controller.abort();
}
}
handleData(data) {
window.dispatchEvent(new CustomEvent('long-polling-data', { detail: data }));
}
handleError(error) {
this.retryCount++;
if (this.retryCount >= this.options.maxRetries) {
console.error('达到最大重试次数,停止长轮询');
this.stop();
} else {
console.warn(`长轮询错误,重试中:`, error);
// 指数退避重试
setTimeout(() => this.start(), Math.pow(2, this.retryCount) * 1000);
}
}
}
3.3 Server-Sent Events Polyfill
// 为不支持SSE的浏览器提供polyfill
class EventSourcePolyfill {
constructor(url, options = {}) {
this.url = url;
this.options = options;
this.readyState = EventSource.CONNECTING;
this.onopen = null;
this.onmessage = null;
this.onerror = null;
this.listeners = new Map();
this.connect();
}
addEventListener(type, listener) {
if (!this.listeners.has(type)) {
this.listeners.set(type, []);
}
this.listeners.get(type).push(listener);
}
removeEventListener(type, listener) {
if (this.listeners.has(type)) {
const listeners = this.listeners.get(type);
const index = listeners.indexOf(listener);
if (index > -1) {
listeners.splice(index, 1);
}
}
}
async connect() {
try {
const response = await fetch(this.url, {
headers: {
'Accept': 'text/event-stream',
'Cache-Control': 'no-cache'
}
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
this.readyState = EventSource.OPEN;
this.triggerEvent('open', {});
const reader = response.body.getReader();
const decoder = new TextDecoder();
while (true) {
const { done, value } = await reader.read();
if (done) break;
const chunk = decoder.decode(value);
this.parseChunk(chunk);
}
} catch (error) {
this.readyState = EventSource.CLOSED;
this.triggerEvent('error', { error });
}
}
parseChunk(chunk) {
const lines = chunk.split('\n');
let eventType = 'message';
let data = '';
for (const line of lines) {
if (line.startsWith('event:')) {
eventType = line.slice(6).trim();
} else if (line.startsWith('data:')) {
data = line.slice(5).trim();
} else if (line === '') {
if (data) {
this.triggerEvent(eventType, { data });
eventType = 'message';
data = '';
}
}
}
}
triggerEvent(type, eventData) {
const event = new Event(type);
Object.assign(event, eventData);
if (this[`on${type}`]) {
this[`on${type}`](event);
}
if (this.listeners.has(type)) {
this.listeners.get(type).forEach(listener => {
listener(event);
});
}
}
close() {
this.readyState = EventSource.CLOSED;
}
}
// 检测浏览器支持
if (typeof EventSource === 'undefined') {
window.EventSource = EventSourcePolyfill;
}
4. Microsoft Fetch Event Source
4.1 基本原理
@microsoft/fetch-event-source 是一个基于 Fetch API 的 SSE 实现库,它解决了原生 EventSource 的一些限制:
- 更好的错误处理: 提供详细的错误信息和重试机制
- 请求头支持: 可以添加自定义请求头,支持认证
- 请求体支持: 支持 POST 请求和请求体
- 更灵活的重连策略: 可配置的重连逻辑
- TypeScript 支持: 完整的类型定义
4.2 安装和基本使用
npm install @microsoft/fetch-event-source
import { fetchEventSource } from '@microsoft/fetch-event-source';
class FetchEventSourceManager {
constructor(url, options = {}) {
this.url = url;
this.options = {
method: 'GET',
headers: {},
body: null,
onopen: null,
onmessage: null,
onclose: null,
onerror: null,
retry: 3000,
maxRetries: 5,
...options
};
}
async start() {
try {
await fetchEventSource(this.url, {
method: this.options.method,
headers: this.options.headers,
body: this.options.body,
onopen(response) {
console.log('连接已建立:', response.status);
if (this.options.onopen) {
this.options.onopen(response);
}
},
onmessage(event) {
try {
const data = JSON.parse(event.data);
if (this.options.onmessage) {
this.options.onmessage(data, event);
}
} catch (error) {
console.error('解析消息失败:', error);
}
},
onclose() {
console.log('连接已关闭');
if (this.options.onclose) {
this.options.onclose();
}
},
onerror(err) {
console.error('连接错误:', err);
if (this.options.onerror) {
this.options.onerror(err);
}
// 返回 false 停止重连
return false;
},
// 重连配置
retry: this.options.retry,
maxRetries: this.options.maxRetries,
});
} catch (error) {
console.error('启动失败:', error);
}
}
}
// 使用示例
const manager = new FetchEventSourceManager('/api/events', {
headers: {
'Authorization': 'Bearer ' + token,
'Content-Type': 'application/json'
},
onmessage: (data, event) => {
console.log('收到消息:', data);
// 处理不同类型的消息
switch (data.type) {
case 'notification':
showNotification(data.message);
break;
case 'update':
updateUI(data);
break;
}
},
onerror: (error) => {
if (error.status === 401) {
// 认证失败,跳转登录
window.location.href = '/login';
}
}
});
manager.start();
4.3 高级配置和最佳实践
// 带认证和自定义重连逻辑的配置
const advancedConfig = {
method: 'POST',
headers: {
'Authorization': `Bearer ${getAuthToken()}`,
'Content-Type': 'application/json',
'X-Client-Version': '1.0.0'
},
body: JSON.stringify({
userId: getCurrentUserId(),
preferences: getUserPreferences()
}),
// 自定义重连逻辑
async onopen(response) {
if (response.status === 200) {
console.log('连接成功建立');
// 重置重连计数
this.retryCount = 0;
} else if (response.status === 401) {
// 认证失败,刷新token
await refreshAuthToken();
return false; // 停止重连
} else {
console.error('连接失败:', response.status);
return false;
}
},
onmessage(event) {
try {
const data = JSON.parse(event.data);
this.handleMessage(data);
} catch (error) {
console.error('消息解析失败:', error);
}
},
onclose() {
console.log('连接关闭');
// 可以在这里执行清理操作
},
onerror(err) {
console.error('连接错误:', err);
// 根据错误类型决定是否重连
if (err.status === 0) {
// 网络错误,重连
return true;
} else if (err.status >= 500) {
// 服务器错误,重连
return true;
} else if (err.status === 429) {
// 限流,延迟重连
setTimeout(() => this.start(), 60000);
return false;
} else {
// 其他错误,不重连
return false;
}
},
// 重连配置
retry: 1000,
maxRetries: 10,
// 指数退避重连
async onretry(err, attempt) {
if (attempt > this.options.maxRetries) {
console.error('达到最大重连次数');
return false;
}
// 指数退避延迟
const delay = Math.min(1000 * Math.pow(2, attempt), 30000);
console.log(`${delay}ms后重连 (第${attempt}次)`);
await new Promise(resolve => setTimeout(resolve, delay));
return true;
}
};
4.4 与原生 EventSource 的对比
| 特性 | 原生 EventSource | fetch-event-source |
|---|---|---|
| 请求头支持 | 有限 | 完整支持 |
| 请求体支持 | 不支持 | 支持 |
| 错误处理 | 简单 | 详细 |
| 重连控制 | 自动 | 可配置 |
| 认证支持 | 有限 | 完整 |
| 浏览器兼容性 | 现代浏览器 | 需要fetch支持 |
| 包大小 | 0KB | ~2KB |
| 类型支持 | 无 | TypeScript |
4.5 关键源码解析
4.5.1 核心实现原理
fetch-event-source 的核心思想是使用 Fetch API 的 ReadableStream 来模拟 SSE 的数据流处理。以下是关键源码的简化版本:
// 核心函数实现原理
async function fetchEventSource(url, options) {
const {
method = 'GET',
headers = {},
body = null,
onopen,
onmessage,
onclose,
onerror,
retry = 1000,
maxRetries = 5
} = options;
let attempt = 0;
while (attempt <= maxRetries) {
try {
// 发起fetch请求
const response = await fetch(url, {
method,
headers: {
'Accept': 'text/event-stream',
'Cache-Control': 'no-cache',
...headers
},
body
});
// 检查响应状态
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
// 触发连接建立事件
if (onopen) {
onopen(response);
}
// 获取响应流
const reader = response.body.getReader();
const decoder = new TextDecoder();
try {
// 处理数据流
while (true) {
const { done, value } = await reader.read();
if (done) break;
// 解码数据块
const chunk = decoder.decode(value, { stream: true });
// 解析SSE格式
const events = parseSSEChunk(chunk);
// 触发消息事件
for (const event of events) {
if (onmessage) {
onmessage(event);
}
}
}
} finally {
reader.releaseLock();
}
// 连接正常关闭
if (onclose) {
onclose();
}
break;
} catch (error) {
attempt++;
// 触发错误事件
if (onerror) {
const shouldRetry = onerror(error, attempt);
if (shouldRetry === false) {
break;
}
}
// 重连逻辑
if (attempt <= maxRetries) {
await delay(retry * Math.pow(2, attempt - 1));
}
}
}
}
// SSE数据解析函数
function parseSSEChunk(chunk) {
const lines = chunk.split('\n');
const events = [];
let currentEvent = { type: 'message', data: '', id: '', retry: null };
for (const line of lines) {
if (line.startsWith('event:')) {
currentEvent.type = line.slice(6).trim();
} else if (line.startsWith('data:')) {
currentEvent.data += line.slice(5).trim() + '\n';
} else if (line.startsWith('id:')) {
currentEvent.id = line.slice(3).trim();
} else if (line.startsWith('retry:')) {
currentEvent.retry = parseInt(line.slice(6).trim(), 10);
} else if (line === '') {
if (currentEvent.data) {
// 移除最后一个换行符
currentEvent.data = currentEvent.data.slice(0, -1);
events.push({ ...currentEvent });
currentEvent = { type: 'message', data: '', id: '', retry: null };
}
}
}
return events;
}
4.5.2 重连机制实现
重连机制是 fetch-event-source 的重要特性,以下是其实现原理:
class RetryManager {
constructor(options) {
this.retry = options.retry || 1000;
this.maxRetries = options.maxRetries || 5;
this.attempt = 0;
this.backoffMultiplier = options.backoffMultiplier || 2;
this.maxBackoff = options.maxBackoff || 30000;
}
async shouldRetry(error, attempt) {
// 检查是否达到最大重试次数
if (attempt > this.maxRetries) {
return false;
}
// 根据错误类型决定是否重连
if (error.status === 0) {
// 网络错误,应该重连
return true;
} else if (error.status >= 500) {
// 服务器错误,应该重连
return true;
} else if (error.status === 429) {
// 限流,延迟重连
return true;
} else if (error.status >= 400 && error.status < 500) {
// 客户端错误,不重连
return false;
}
return true;
}
async getRetryDelay(attempt) {
// 指数退避算法
const delay = Math.min(
this.retry * Math.pow(this.backoffMultiplier, attempt - 1),
this.maxBackoff
);
// 添加随机抖动,避免多个客户端同时重连
const jitter = delay * 0.1 * Math.random();
return delay + jitter;
}
async waitBeforeRetry(attempt) {
const delay = await this.getRetryDelay(attempt);
console.log(`等待 ${delay}ms 后重连 (第${attempt}次)`);
await new Promise(resolve => setTimeout(resolve, delay));
}
}
4.5.3 流式数据处理
fetch-event-source 使用 ReadableStream 来处理服务器发送的数据流:
class StreamProcessor {
constructor(response, options) {
this.response = response;
this.options = options;
this.decoder = new TextDecoder();
this.buffer = '';
this.events = [];
}
async processStream() {
const reader = this.response.body.getReader();
try {
while (true) {
const { done, value } = await reader.read();
if (done) {
// 处理缓冲区中的剩余数据
this.processBuffer();
break;
}
// 解码数据块
const chunk = this.decoder.decode(value, { stream: true });
this.buffer += chunk;
// 处理完整的事件
this.processBuffer();
}
} finally {
reader.releaseLock();
}
}
processBuffer() {
const lines = this.buffer.split('\n');
// 保留最后一行(可能不完整)
this.buffer = lines.pop() || '';
// 处理完整的行
for (const line of lines) {
this.processLine(line);
}
// 触发完整的事件
this.triggerEvents();
}
processLine(line) {
if (line.startsWith('event:')) {
this.currentEvent = { ...this.currentEvent, type: line.slice(6).trim() };
} else if (line.startsWith('data:')) {
this.currentEvent = { ...this.currentEvent, data: line.slice(5).trim() };
} else if (line.startsWith('id:')) {
this.currentEvent = { ...this.currentEvent, id: line.slice(3).trim() };
} else if (line === '') {
if (this.currentEvent.data) {
this.events.push({ ...this.currentEvent });
this.currentEvent = { type: 'message', data: '', id: '' };
}
}
}
triggerEvents() {
for (const event of this.events) {
if (this.options.onmessage) {
this.options.onmessage(event);
}
}
this.events = [];
}
}
4.5.4 错误处理和恢复
错误处理是 fetch-event-source 的核心特性之一:
class ErrorHandler {
constructor(options) {
this.options = options;
this.errorCount = 0;
this.lastErrorTime = 0;
}
handleError(error, attempt) {
this.errorCount++;
this.lastErrorTime = Date.now();
// 记录错误信息
console.error(`SSE错误 (第${attempt}次):`, {
status: error.status,
statusText: error.statusText,
message: error.message,
timestamp: new Date().toISOString()
});
// 根据错误类型决定处理策略
switch (error.status) {
case 0:
return this.handleNetworkError(error, attempt);
case 401:
return this.handleAuthError(error, attempt);
case 403:
return this.handleForbiddenError(error, attempt);
case 429:
return this.handleRateLimitError(error, attempt);
case 500:
case 502:
case 503:
case 504:
return this.handleServerError(error, attempt);
default:
return this.handleUnknownError(error, attempt);
}
}
handleNetworkError(error, attempt) {
// 网络错误,应该重连
return true;
}
async handleAuthError(error, attempt) {
// 认证错误,尝试刷新token
try {
await this.options.refreshToken?.();
return true; // 刷新成功,重连
} catch (refreshError) {
// 刷新失败,停止重连
this.options.onAuthFailure?.(error);
return false;
}
}
handleRateLimitError(error, attempt) {
// 限流错误,延迟重连
const retryAfter = this.getRetryAfterHeader(error);
if (retryAfter) {
setTimeout(() => this.options.retry(), retryAfter * 1000);
}
return false;
}
handleServerError(error, attempt) {
// 服务器错误,指数退避重连
return attempt < this.options.maxRetries;
}
handleUnknownError(error, attempt) {
// 未知错误,根据配置决定是否重连
return this.options.retryOnUnknownError !== false &&
attempt < this.options.maxRetries;
}
getRetryAfterHeader(error) {
const retryAfter = error.headers?.get('Retry-After');
return retryAfter ? parseInt(retryAfter, 10) : null;
}
}
4.5.5 性能优化技巧
fetch-event-source 包含多种性能优化:
class PerformanceOptimizer {
constructor() {
this.messageQueue = [];
this.processing = false;
this.batchSize = 10;
this.batchTimeout = 16; // 约60fps
}
// 批量处理消息,避免频繁的DOM更新
queueMessage(event) {
this.messageQueue.push(event);
if (!this.processing) {
this.processing = true;
this.processBatch();
}
}
async processBatch() {
while (this.messageQueue.length > 0) {
const batch = this.messageQueue.splice(0, this.batchSize);
// 批量处理消息
await this.processMessages(batch);
// 如果还有消息,等待下一帧
if (this.messageQueue.length > 0) {
await this.waitForNextFrame();
}
}
this.processing = false;
}
async processMessages(messages) {
// 使用 requestAnimationFrame 确保在合适的时机更新UI
return new Promise(resolve => {
requestAnimationFrame(() => {
for (const message of messages) {
this.options.onmessage(message);
}
resolve();
});
});
}
waitForNextFrame() {
return new Promise(resolve => setTimeout(resolve, this.batchTimeout));
}
// 内存泄漏防护
cleanup() {
this.messageQueue = [];
this.processing = false;
}
}
5. 前端实现
5.1 基本用法
// 创建 EventSource 实例
const eventSource = new EventSource('/api/events');
// 监听连接打开
eventSource.onopen = function(event) {
console.log('连接已建立');
};
// 监听消息
eventSource.onmessage = function(event) {
console.log('收到消息:', event.data);
const data = JSON.parse(event.data);
// 处理数据
};
// 监听特定事件
eventSource.addEventListener('custom-event', function(event) {
console.log('自定义事件:', event.data);
});
// 监听错误
eventSource.onerror = function(event) {
console.error('连接错误:', event);
};
// 关闭连接
eventSource.close();
5.2 高级用法
class SSEManager {
constructor(url, options = {}) {
this.url = url;
this.options = {
retry: 3000,
...options
};
this.eventSource = null;
this.isConnected = false;
this.reconnectAttempts = 0;
this.maxReconnectAttempts = 5;
}
connect() {
try {
this.eventSource = new EventSource(this.url);
this.setupEventListeners();
} catch (error) {
console.error('SSE连接失败:', error);
this.scheduleReconnect();
}
}
setupEventListeners() {
this.eventSource.onopen = () => {
this.isConnected = true;
this.reconnectAttempts = 0;
console.log('SSE连接已建立');
};
this.eventSource.onmessage = (event) => {
this.handleMessage(event);
};
this.eventSource.onerror = (event) => {
this.isConnected = false;
console.error('SSE连接错误:', event);
this.scheduleReconnect();
};
}
handleMessage(event) {
try {
const data = JSON.parse(event.data);
// 触发自定义事件
this.dispatchEvent('message', data);
} catch (error) {
console.error('解析SSE消息失败:', error);
}
}
scheduleReconnect() {
if (this.reconnectAttempts < this.maxReconnectAttempts) {
this.reconnectAttempts++;
setTimeout(() => {
console.log(`尝试重连 (${this.reconnectAttempts}/${this.maxReconnectAttempts})`);
this.connect();
}, this.options.retry);
} else {
console.error('达到最大重连次数,停止重连');
}
}
close() {
if (this.eventSource) {
this.eventSource.close();
this.isConnected = false;
}
}
dispatchEvent(type, data) {
const event = new CustomEvent(type, { detail: data });
window.dispatchEvent(event);
}
}
// 使用示例
const sseManager = new SSEManager('/api/events', { retry: 5000 });
sseManager.connect();
// 监听消息
window.addEventListener('message', (event) => {
console.log('收到SSE消息:', event.detail);
});
6. 服务器端实现
6.1 Node.js Express 示例
const express = require('express');
const app = express();
app.get('/api/events', (req, res) => {
// 设置SSE头部
res.writeHead(200, {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive',
'Access-Control-Allow-Origin': '*'
});
// 发送初始连接消息
res.write('data: {"type": "connected", "message": "连接已建立"}\n\n');
// 模拟数据推送
const interval = setInterval(() => {
const data = {
type: 'update',
timestamp: new Date().toISOString(),
message: '服务器推送消息'
};
res.write(`data: ${JSON.stringify(data)}\n\n`);
}, 1000);
// 客户端断开连接时清理
req.on('close', () => {
clearInterval(interval);
res.end();
});
});
app.listen(3000, () => {
console.log('服务器运行在端口3000');
});
6.2 Python Flask 示例
from flask import Flask, Response, request
import json
import time
app = Flask(__name__)
@app.route('/api/events')
def events():
def generate():
# 发送连接确认
yield f"data: {json.dumps({'type': 'connected'})}\n\n"
# 持续发送数据
while True:
data = {
'type': 'update',
'timestamp': time.time(),
'message': '服务器推送消息'
}
yield f"data: {json.dumps(data)}\n\n"
time.sleep(1)
return Response(generate(), mimetype='text/event-stream')
if __name__ == '__main__':
app.run(debug=True)
7. 使用注意事项
7.1 浏览器兼容性
- 现代浏览器都支持 SSE
- IE 不支持,需要使用 polyfill 或降级方案
- 移动端浏览器支持良好
7.2 连接管理
// 页面可见性变化时管理连接
document.addEventListener('visibilitychange', () => {
if (document.hidden) {
// 页面隐藏时关闭连接
eventSource.close();
} else {
// 页面显示时重新连接
eventSource = new EventSource('/api/events');
}
});
// 页面卸载时清理
window.addEventListener('beforeunload', () => {
if (eventSource) {
eventSource.close();
}
});
7.3 错误处理
eventSource.onerror = function(event) {
if (event.target.readyState === EventSource.CLOSED) {
console.log('连接已关闭');
} else if (event.target.readyState === EventSource.CONNECTING) {
console.log('正在尝试重连...');
} else {
console.error('连接状态异常:', event.target.readyState);
}
};
7.4 性能优化
- 合理设置重连间隔,避免频繁重连
- 及时清理不需要的事件监听器
- 考虑使用连接池管理多个SSE连接
- 监控连接状态和性能指标
8. 实际应用场景
8.1 实时通知
// 实时通知系统
const notificationSSE = new EventSource('/api/notifications');
notificationSSE.addEventListener('notification', (event) => {
const notification = JSON.parse(event.data);
showNotification(notification);
});
function showNotification(notification) {
// 显示通知UI
const toast = document.createElement('div');
toast.className = 'toast';
toast.textContent = notification.message;
document.body.appendChild(toast);
setTimeout(() => {
toast.remove();
}, 3000);
}
8.2 实时数据更新
// 实时数据仪表板
const dashboardSSE = new EventSource('/api/dashboard');
dashboardSSE.addEventListener('metrics', (event) => {
const metrics = JSON.parse(event.data);
updateDashboard(metrics);
});
function updateDashboard(metrics) {
// 更新各种指标显示
document.getElementById('cpu-usage').textContent = metrics.cpu + '%';
document.getElementById('memory-usage').textContent = metrics.memory + '%';
document.getElementById('network-traffic').textContent = metrics.network + ' MB/s';
}
8.3 聊天应用
// 实时聊天
const chatSSE = new EventSource('/api/chat');
chatSSE.addEventListener('message', (event) => {
const message = JSON.parse(event.data);
appendMessage(message);
});
chatSSE.addEventListener('user-joined', (event) => {
const user = JSON.parse(event.data);
showUserJoined(user);
});
function appendMessage(message) {
const messageElement = document.createElement('div');
messageElement.className = 'message';
messageElement.innerHTML = `
<strong>${message.user}:</strong> ${message.text}
<small>${new Date(message.timestamp).toLocaleTimeString()}</small>
`;
document.getElementById('chat-messages').appendChild(messageElement);
}
9. 最佳实践
9.1 连接管理
- 实现智能重连机制
- 在页面不可见时暂停连接
- 合理设置重连次数和间隔
9.2 错误处理
- 实现完善的错误处理逻辑
- 提供用户友好的错误提示
- 记录错误日志用于调试
9.3 性能优化
- 避免在SSE回调中执行耗时操作
- 使用防抖/节流优化频繁更新
- 合理使用事件委托
9.4 安全性
- 验证服务器身份
- 实现适当的认证和授权
- 防止XSS攻击
10. 调试和监控
10.1 浏览器开发者工具
- Network 标签页查看SSE连接
- Console 查看连接状态和错误
- 监控内存使用情况
10.2 连接状态监控
// 监控连接状态
setInterval(() => {
if (eventSource) {
console.log('连接状态:', eventSource.readyState);
console.log('连接URL:', eventSource.url);
}
}, 5000);
10.3 性能指标
- 连接建立时间
- 消息接收频率
- 重连次数和成功率
- 内存使用情况
11. 常见问题和解决方案
11.1 连接频繁断开
- 检查服务器配置
- 实现指数退避重连
- 监控网络状况
11.2 内存泄漏
- 及时清理事件监听器
- 避免闭包引用
- 定期检查内存使用
11.3 跨域问题
- 配置正确的CORS头部
- 使用代理服务器
- 考虑使用相对路径
12. 工程实践建议
12.1 技术选型决策树
是否需要实时通信?
├─ 否 → 使用传统HTTP请求
└─ 是 → 需要双向通信?
├─ 是 → 使用WebSocket
└─ 否 → 服务器推送数据?
├─ 是 → 考虑SSE
│ ├─ 需要认证/自定义请求头?
│ │ ├─ 是 → 使用fetch-event-source
│ │ └─ 否 → 使用原生EventSource
│ └─ 需要更好的错误处理?
│ ├─ 是 → 使用fetch-event-source
│ └─ 否 → 使用原生EventSource
└─ 否 → 使用轮询或长轮询
12.2 混合策略
class HybridRealTimeManager {
constructor(url, options = {}) {
this.url = url;
this.options = options;
this.currentStrategy = null;
this.strategies = {
sse: new SSEManager(url, options),
fetchEventSource: new FetchEventSourceManager(url, options),
polling: new FetchPollingManager(url, options)
};
}
async start() {
// 优先尝试SSE
try {
await this.tryStrategy('sse');
} catch (error) {
console.warn('SSE失败,尝试fetch-event-source:', error);
try {
await this.tryStrategy('fetchEventSource');
} catch (error) {
console.warn('fetch-event-source失败,降级到轮询:', error);
await this.tryStrategy('polling');
}
}
}
async tryStrategy(strategyName) {
if (this.currentStrategy) {
this.currentStrategy.stop();
}
this.currentStrategy = this.strategies[strategyName];
await this.currentStrategy.start();
console.log(`使用策略: ${strategyName}`);
}
stop() {
if (this.currentStrategy) {
this.currentStrategy.stop();
}
}
}
12.3 性能监控和指标
class RealTimeMetrics {
constructor() {
this.metrics = {
connectionTime: 0,
messageCount: 0,
errorCount: 0,
reconnectCount: 0,
lastMessageTime: 0
};
this.startTime = Date.now();
}
recordConnection() {
this.metrics.connectionTime = Date.now() - this.startTime;
}
recordMessage() {
this.metrics.messageCount++;
this.metrics.lastMessageTime = Date.now();
}
recordError() {
this.metrics.errorCount++;
}
recordReconnect() {
this.metrics.reconnectCount++;
}
getMetrics() {
const uptime = Date.now() - this.startTime;
return {
...this.metrics,
uptime,
messageRate: this.metrics.messageCount / (uptime / 1000),
errorRate: this.metrics.errorCount / (uptime / 1000)
};
}
exportMetrics() {
// 导出到监控系统
console.log('实时通信指标:', this.getMetrics());
}
}
13. 总结
SSE是一种简单有效的实时通信解决方案,特别适合服务器向客户端推送数据的场景。通过合理的设计和实现,可以构建稳定可靠的实时应用。
13.1 选择建议
- 简单推送场景: 使用原生EventSource
- 需要认证/自定义请求: 使用fetch-event-source
- 复杂实时应用: 考虑WebSocket
- 降级方案: 准备轮询或长轮询作为备选
13.2 关键要点
- 理解SSE的优缺点和适用场景
- 实现完善的错误处理和重连机制
- 考虑浏览器兼容性和降级方案
- 合理选择技术栈和实现策略
- 建立完善的监控和调试体系
在选择SSE时,需要权衡其单向通信的限制和实现的简单性,确保它符合具体的业务需求。同时,也要考虑备选方案,确保在各种情况下都能提供良好的用户体验。