SSE原理

250 阅读8分钟

Server-Sent Events (SSE) 前端开发指南

1. SSE 基本原理

1.1 什么是 SSE

Server-Sent Events (SSE) 是一种Web标准,允许服务器向客户端推送实时数据。它是基于HTTP协议的单向通信机制,服务器可以持续向客户端发送数据流。

1.2 工作原理

  • 客户端通过 EventSource API 建立与服务器的连接
  • 服务器保持连接开放,使用 text/event-stream 内容类型
  • 服务器可以持续发送数据,客户端实时接收
  • 连接断开时自动重连

1.3 与 WebSocket 的区别

特性SSEWebSocket
通信方向单向(服务器→客户端)双向
协议HTTPWebSocket
自动重连支持需要手动实现
浏览器支持现代浏览器都支持现代浏览器都支持
实现复杂度简单相对复杂

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 的对比

特性原生 EventSourcefetch-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 关键要点

  1. 理解SSE的优缺点和适用场景
  2. 实现完善的错误处理和重连机制
  3. 考虑浏览器兼容性和降级方案
  4. 合理选择技术栈和实现策略
  5. 建立完善的监控和调试体系

在选择SSE时,需要权衡其单向通信的限制和实现的简单性,确保它符合具体的业务需求。同时,也要考虑备选方案,确保在各种情况下都能提供良好的用户体验。