pin_drop当前位置:知识文库 ❯ 图文

后端通信:SSE服务器推送 - 从入门到实践详解

教程简介

Server-Sent Events(SSE)是HTML5提供的服务器推送技术,允许服务器通过HTTP连接单向向客户端推送数据。与WebSocket的全双工通信不同,SSE专注于服务器到客户端的单向推送,具有更简单的API、内置的自动重连、事件类型支持等优势。SSE非常适合实时通知、数据监控、新闻推送等单向数据流场景。

本教程将全面讲解SSE原理、EventSource API、事件格式、连接管理、自动重连、事件ID、自定义事件类型、SSE与WebSocket对比,以及实时通知实现。

核心概念

1. SSE原理

SSE基于HTTP长连接实现服务器推送:

  • 客户端发起HTTP请求,请求头包含Accept: text/event-stream

  • 服务器保持连接打开,持续发送事件数据

  • 数据以text/event-stream格式传输

  • 连接断开时浏览器自动重连

2. EventSource API

代码示例

// 创建连接
const eventSource = new EventSource('/api/events');

// 监听消息
eventSource.onmessage = function(event) {
    console.log('收到消息:', event.data);
    console.log('事件ID:', event.lastEventId);
};

// 监听连接打开
eventSource.onopen = function() {
    console.log('连接已建立');
};

// 监听错误
eventSource.onerror = function(event) {
    if (eventSource.readyState === EventSource.CONNECTING) {
        console.log('正在重连...');
    } else if (eventSource.readyState === EventSource.CLOSED) {
        console.log('连接已关闭');
    }
};

// 关闭连接
eventSource.close();

EventSource属性

属性 说明
url 连接URL
readyState 连接状态
withCredentials 是否携带Cookie

readyState状态

常量 说明
0 CONNECTING 正在连接/重连
1 OPEN 连接已建立
2 CLOSED 连接已关闭

3. 事件格式

SSE事件流格式:

代码示例

field: value\n

每个事件以空行分隔:

代码示例

field1: value1\n
field2: value2\n
\n

字段类型

字段 说明 示例
data 消息数据 data: Hello World
event 事件类型 event: update
id 事件ID id: 123
retry 重连间隔(ms) retry: 5000

多行数据

代码示例

data: 第一行\n
data: 第二行\n
\n

客户端接收时两行用换行符连接:"第一行\n第二行"

JSON数据

代码示例

data: {"name":"张三","email":"zhangsan@example.com"}\n
\n

4. 自定义事件类型

代码示例

// 监听自定义事件
eventSource.addEventListener('update', function(event) {
    const data = JSON.parse(event.data);
    console.log('更新事件:', data);
});

eventSource.addEventListener('notification', function(event) {
    const data = JSON.parse(event.data);
    console.log('通知事件:', data);
});

eventSource.addEventListener('alert', function(event) {
    console.log('告警事件:', event.data);
});

服务器发送:

代码示例

event: update\n
data: {"price": 99.9}\n
\n

event: notification\n
data: {"message": "新消息"}\n
\n

event: alert\n
data: 系统告警!\n
\n

5. 事件ID与自动重连

SSE内置自动重连机制:

  • 连接断开时浏览器自动重连

  • 使用id字段标记事件,重连时通过Last-Event-ID头部发送最后一个事件ID

  • 服务器据此从断点续传

代码示例

// 客户端
const eventSource = new EventSource('/api/events');

// 服务器发送带ID的事件
// id: 100\n
// data: 消息内容\n
// \n

// 重连时浏览器自动发送
// Last-Event-ID: 100

retry字段:设置重连间隔

代码示例

retry: 5000\n
\n

6. SSE vs WebSocket

特性 SSE WebSocket
通信方向 服务器→客户端 双向
协议 HTTP ws/wss
数据格式 文本 文本/二进制
自动重连 内置 需手动实现
事件ID 内置
事件类型 支持 需自定义
浏览器支持 现代浏览器 所有浏览器
代理/防火墙 友好 可能被阻止
复杂度
适用场景 推送、通知 聊天、游戏

SSE的优势:

  • 基于HTTP,无需特殊协议

  • 内置自动重连和事件ID

  • 更简单的API

  • 天然兼容代理和CDN

SSE的劣势:

  • 单向通信

  • 不支持二进制数据

  • IE不支持(可用polyfill)

7. 使用Fetch实现SSE

当需要更多控制时,可以用Fetch API手动解析SSE流:

代码示例

async function connectSSE(url) {
    const response = await fetch(url, {
        headers: { 'Accept': 'text/event-stream' }
    });

    const reader = response.body.getReader();
    const decoder = new TextDecoder();
    let buffer = '';

    while (true) {
        const { done, value } = await reader.read();
        if (done) break;

        buffer += decoder.decode(value, { stream: true });
        const lines = buffer.split('\n\n');
        buffer = lines.pop();

        for (const event of lines) {
            if (!event.trim()) continue;
            let data = '', eventType = 'message', id = '';
            for (const line of event.split('\n')) {
                if (line.startsWith('data:')) data += line.slice(5).trim() + '\n';
                else if (line.startsWith('event:')) eventType = line.slice(6).trim();
                else if (line.startsWith('id:')) id = line.slice(3).trim();
            }
            data = data.trimEnd();
            if (data) {
                console.log({ eventType, data, id });
            }
        }
    }
}

代码示例

示例1:SSE事件流模拟器

代码示例

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>SSE事件流模拟器</title>
    <style>
        * { margin: 0; padding: 0; box-sizing: border-box; }
        body { font-family: 'Segoe UI', system-ui, sans-serif; background: #f0f2f5; padding: 20px; color: #333; }
        .container { max-width: 900px; margin: 0 auto; }
        h1 { text-align: center; color: #1a73e8; margin-bottom: 24px; font-size: 28px; }
        .card { background: #fff; border-radius: 12px; padding: 24px; margin-bottom: 20px; box-shadow: 0 2px 8px rgba(0,0,0,0.08); }
        .card h2 { font-size: 18px; color: #1a73e8; margin-bottom: 16px; padding-bottom: 8px; border-bottom: 2px solid #e8eaed; }
        .status-bar { display: flex; align-items: center; gap: 10px; margin-bottom: 16px; padding: 10px; background: #f8f9fa; border-radius: 8px; }
        .status-dot { width: 10px; height: 10px; border-radius: 50%; }
        .dot-connecting { background: #ff9800; animation: pulse 1s infinite; }
        .dot-open { background: #34a853; }
        .dot-closed { background: #ea4335; }
        @keyframes pulse { 0%,100% { opacity:1; } 50% { opacity:0.4; } }
        .btn-row { display: flex; gap: 8px; margin-bottom: 16px; flex-wrap: wrap; }
        button { padding: 8px 16px; border: none; border-radius: 6px; font-weight: 600; cursor: pointer; font-size: 13px; }
        .btn-connect { background: #34a853; color: #fff; }
        .btn-disconnect { background: #ea4335; color: #fff; }
        .btn-simulate { background: #1a73e8; color: #fff; }
        .event-log { background: #1e1e1e; color: #d4d4d4; padding: 16px; border-radius: 8px; font-family: 'Consolas', monospace; font-size: 12px; line-height: 1.8; max-height: 350px; overflow-y: auto; white-space: pre-wrap; }
        .log-event { color: #64b5f6; }
        .log-data { color: #81c784; }
        .log-system { color: #ffb74d; }
        .log-error { color: #e57373; }
        .stats { display: flex; gap: 16px; margin-top: 12px; }
        .stat { padding: 8px 14px; background: #f8f9fa; border-radius: 6px; font-size: 13px; }
        .stat b { color: #1a73e8; }
    </style>
</head>
<body>
    <div class="container">
        <h1>SSE事件流模拟器</h1>
        <div class="card">
            <h2>连接状态</h2>
            <div class="status-bar">
                <div class="status-dot dot-closed" id="statusDot"></div>
                <span id="statusText">未连接</span>
            </div>
            <div class="btn-row">
                <button class="btn-connect" onclick="startSimulation()">开始模拟</button>
                <button class="btn-disconnect" onclick="stopSimulation()">停止</button>
                <button class="btn-simulate" onclick="sendCustomEvent()">发送自定义事件</button>
            </div>
        </div>
        <div class="card">
            <h2>事件流</h2>
            <div class="event-log" id="eventLog">等待连接...</div>
            <div class="stats">
                <div class="stat">事件数: <b id="eventCount">0</b></div>
                <div class="stat">最后ID: <b id="lastId">-</b></div>
                <div class="stat">运行时间: <b id="runtime">0s</b></div>
            </div>
        </div>
    </div>
    <script>
        let simulationTimer = null;
        let eventId = 0;
        let eventCount = 0;
        let startTime = 0;
        let runtimeTimer = null;

        const eventTypes = [
            { type: 'message', data: () => JSON.stringify({ text: '系统消息', time: new Date().toISOString() }) },
            { type: 'update', data: () => JSON.stringify({ price: (Math.random() * 1000).toFixed(2), change: (Math.random() > 0.5 ? '+' : '-') + (Math.random() * 5).toFixed(2) }) },
            { type: 'notification', data: () => JSON.stringify({ title: '新通知', body: '您有一条新消息', type: ['info', 'warning', 'success'][Math.floor(Math.random() * 3)] }) },
            { type: 'heartbeat', data: () => 'ping' }
        ];

        function log(msg, type = 'system') {
            const time = new Date().toLocaleTimeString();
            document.getElementById('eventLog').innerHTML += `<span class="log-${type}">[${time}] ${msg}</span>\n`;
            document.getElementById('eventLog').scrollTop = document.getElementById('eventLog').scrollTop + 1000;
        }

        function updateStatus(state) {
            const dot = document.getElementById('statusDot');
            const text = document.getElementById('statusText');
            const states = {
                connecting: { dot: 'dot-connecting', text: '正在连接...' },
                open: { dot: 'dot-open', text: '已连接' },
                closed: { dot: 'dot-closed', text: '已断开' }
            };
            dot.className = 'status-dot ' + states[state].dot;
            text.textContent = states[state].text;
        }

        function startSimulation() {
            if (simulationTimer) return;
            eventId = 0;
            eventCount = 0;
            startTime = Date.now();

            updateStatus('connecting');
            log('正在建立SSE连接...', 'system');

            setTimeout(() => {
                updateStatus('open');
                log('SSE连接已建立', 'system');
                log('Content-Type: text/event-stream', 'system');
                log('retry: 5000', 'system');
                log('---', 'system');

                simulationTimer = setInterval(() => {
                    const eventType = eventTypes[Math.floor(Math.random() * eventTypes.length)];
                    if (eventType.type === 'heartbeat' && Math.random() > 0.3) return;

                    eventId++;
                    eventCount++;

                    log(`event: ${eventType.type}`, 'event');
                    log(`id: ${eventId}`, 'event');
                    log(`data: ${eventType.data()}`, 'data');
                    log('', 'system');

                    document.getElementById('eventCount').textContent = eventCount;
                    document.getElementById('lastId').textContent = eventId;
                }, 2000 + Math.random() * 3000);

                runtimeTimer = setInterval(() => {
                    document.getElementById('runtime').textContent = Math.floor((Date.now() - startTime) / 1000) + 's';
                }, 1000);
            }, 1000);
        }

        function stopSimulation() {
            clearInterval(simulationTimer);
            clearInterval(runtimeTimer);
            simulationTimer = null;
            runtimeTimer = null;
            updateStatus('closed');
            log('SSE连接已关闭', 'system');
        }

        function sendCustomEvent() {
            if (!simulationTimer) { log('请先建立连接', 'error'); return; }
            eventId++;
            eventCount++;
            const data = JSON.stringify({ message: '自定义事件', timestamp: Date.now() });
            log('event: custom', 'event');
            log(`id: ${eventId}`, 'event');
            log(`data: ${data}`, 'data');
            log('', 'system');
            document.getElementById('eventCount').textContent = eventCount;
            document.getElementById('lastId').textContent = eventId;
        }
    </script>
</body>
</html>

示例2:实时通知系统

代码示例

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>实时通知系统</title>
    <style>
        * { margin: 0; padding: 0; box-sizing: border-box; }
        body { font-family: 'Segoe UI', system-ui, sans-serif; background: #f0f2f5; padding: 20px; color: #333; }
        .container { max-width: 800px; margin: 0 auto; }
        h1 { text-align: center; color: #1a73e8; margin-bottom: 24px; font-size: 28px; }
        .card { background: #fff; border-radius: 12px; padding: 24px; margin-bottom: 20px; box-shadow: 0 2px 8px rgba(0,0,0,0.08); }
        .card h2 { font-size: 18px; color: #1a73e8; margin-bottom: 16px; padding-bottom: 8px; border-bottom: 2px solid #e8eaed; }
        .notification-list { list-style: none; max-height: 500px; overflow-y: auto; }
        .notification-item { padding: 14px; border: 1px solid #e8eaed; border-radius: 10px; margin-bottom: 10px; animation: slideIn 0.3s ease; }
        @keyframes slideIn { from { opacity:0; transform:translateY(-10px); } to { opacity:1; transform:translateY(0); } }
        .notif-header { display: flex; align-items: center; gap: 8px; margin-bottom: 6px; }
        .notif-icon { width: 28px; height: 28px; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 14px; }
        .notif-info { background: #e3f2fd; color: #1565c0; }
        .notif-success { background: #e8f5e9; color: #2e7d32; }
        .notif-warning { background: #fff3e0; color: #ef6c00; }
        .notif-error { background: #ffebee; color: #c62828; }
        .notif-title { font-weight: 700; font-size: 14px; flex: 1; }
        .notif-time { font-size: 11px; color: #999; }
        .notif-body { font-size: 13px; color: #555; line-height: 1.5; }
        .controls { display: flex; gap: 8px; margin-bottom: 16px; }
        button { padding: 8px 16px; border: none; border-radius: 6px; font-weight: 600; cursor: pointer; font-size: 13px; }
        .btn-start { background: #34a853; color: #fff; }
        .btn-stop { background: #ea4335; color: #fff; }
        .btn-clear { background: #5f6368; color: #fff; }
        .empty-state { text-align: center; padding: 40px; color: #bbb; }
    </style>
</head>
<body>
    <div class="container">
        <h1>实时通知系统</h1>
        <div class="card">
            <h2>通知控制</h2>
            <div class="controls">
                <button class="btn-start" onclick="startNotifications()">开始接收</button>
                <button class="btn-stop" onclick="stopNotifications()">停止</button>
                <button class="btn-clear" onclick="clearNotifications()">清空</button>
            </div>
        </div>
        <div class="card">
            <h2>通知列表</h2>
            <ul class="notification-list" id="notifList">
                <li class="empty-state">点击"开始接收"查看实时通知</li>
            </ul>
        </div>
    </div>
    <script>
        let notifTimer = null;

        const notifTemplates = [
            { type: 'info', title: '系统通知', body: '服务器维护将于今晚22:00开始' },
            { type: 'success', title: '操作成功', body: '文件上传完成,共3个文件' },
            { type: 'warning', title: '安全警告', body: '检测到异常登录尝试' },
            { type: 'error', title: '错误提示', body: '数据库连接超时,请稍后重试' },
            { type: 'info', title: '新消息', body: '张三给您发送了一条消息' },
            { type: 'success', title: '订单确认', body: '您的订单#1024已确认' },
            { type: 'warning', title: '存储空间', body: '存储空间使用率已达85%' },
            { type: 'info', title: '版本更新', body: '新版本v2.5.0已发布' }
        ];

        function addNotification(notif) {
            const list = document.getElementById('notifList');
            const empty = list.querySelector('.empty-state');
            if (empty) empty.remove();

            const time = new Date().toLocaleTimeString();
            const li = document.createElement('li');
            li.className = 'notification-item';
            li.innerHTML = `
                <div class="notif-header">
                    <div class="notif-icon notif-${notif.type}">${notif.type === 'info' ? 'i' : notif.type === 'success' ? '' : notif.type === 'warning' ? '!' : ''}</div>
                    <span class="notif-title">${notif.title}</span>
                    <span class="notif-time">${time}</span>
                </div>
                <div class="notif-body">${notif.body}</div>
            `;
            list.insertBefore(li, list.firstChild);

            // 限制最多显示20条
            while (list.children.length > 20) {
                list.removeChild(list.lastChild);
            }
        }

        function startNotifications() {
            if (notifTimer) return;
            notifTimer = setInterval(() => {
                const notif = notifTemplates[Math.floor(Math.random() * notifTemplates.length)];
                addNotification(notif);
            }, 3000 + Math.random() * 5000);
        }

        function stopNotifications() {
            clearInterval(notifTimer);
            notifTimer = null;
        }

        function clearNotifications() {
            document.getElementById('notifList').innerHTML = '<li class="empty-state">通知已清空</li>';
        }
    </script>
</body>
</html>

示例3:SSE与WebSocket对比工具

代码示例

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>SSE与WebSocket对比工具</title>
    <style>
        * { margin: 0; padding: 0; box-sizing: border-box; }
        body { font-family: 'Segoe UI', system-ui, sans-serif; background: #f0f2f5; padding: 20px; color: #333; }
        .container { max-width: 900px; margin: 0 auto; }
        h1 { text-align: center; color: #1a73e8; margin-bottom: 24px; font-size: 28px; }
        .card { background: #fff; border-radius: 12px; padding: 24px; margin-bottom: 20px; box-shadow: 0 2px 8px rgba(0,0,0,0.08); }
        .card h2 { font-size: 18px; color: #1a73e8; margin-bottom: 16px; padding-bottom: 8px; border-bottom: 2px solid #e8eaed; }
        .comparison-table { width: 100%; border-collapse: collapse; }
        .comparison-table th, .comparison-table td { padding: 12px 14px; text-align: left; border-bottom: 1px solid #e8eaed; font-size: 14px; }
        .comparison-table th { background: #f8f9fa; font-weight: 600; color: #555; }
        .comparison-table th:first-child { width: 150px; }
        .tag { display: inline-block; padding: 2px 8px; border-radius: 4px; font-size: 12px; font-weight: 600; }
        .tag-sse { background: #e3f2fd; color: #1565c0; }
        .tag-ws { background: #e8f5e9; color: #2e7d32; }
        .tag-both { background: #fff3e0; color: #ef6c00; }
        .scenario-cards { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
        .scenario-card { padding: 16px; border: 2px solid #e8eaed; border-radius: 10px; }
        .scenario-card h3 { font-size: 15px; margin-bottom: 8px; }
        .scenario-card p { font-size: 13px; color: #666; line-height: 1.6; }
        .recommend { font-weight: 700; font-size: 14px; margin-top: 8px; }
        .rec-sse { color: #1565c0; }
        .rec-ws { color: #2e7d32; }
        .rec-both { color: #ef6c00; }
    </style>
</head>
<body>
    <div class="container">
        <h1>SSE与WebSocket对比工具</h1>
        <div class="card">
            <h2>特性对比</h2>
            <table class="comparison-table">
                <thead><tr><th>特性</th><th><span class="tag tag-sse">SSE</span></th><th><span class="tag tag-ws">WebSocket</span></th></tr></thead>
                <tbody>
                    <tr><td>通信方向</td><td>服务器→客户端</td><td>双向</td></tr>
                    <tr><td>底层协议</td><td>HTTP</td><td>ws/wss</td></tr>
                    <tr><td>数据格式</td><td>文本</td><td>文本 + 二进制</td></tr>
                    <tr><td>自动重连</td><td>内置</td><td>需手动实现</td></tr>
                    <tr><td>事件ID</td><td>内置</td><td>无</td></tr>
                    <tr><td>事件类型</td><td>内置</td><td>需自定义</td></tr>
                    <tr><td>浏览器API</td><td>EventSource</td><td>WebSocket</td></tr>
                    <tr><td>代理兼容</td><td>好(基于HTTP)</td><td>可能被阻止</td></tr>
                    <tr><td>连接开销</td><td>低</td><td>中等</td></tr>
                    <tr><td>IE支持</td><td>不支持</td><td>支持</td></tr>
                    <tr><td>实现复杂度</td><td>低</td><td>中</td></tr>
                    <tr><td>服务端推送</td><td>原生支持</td><td>原生支持</td></tr>
                    <tr><td>客户端发送</td><td>需另开HTTP</td><td>直接发送</td></tr>
                </tbody>
            </table>
        </div>
        <div class="card">
            <h2>场景推荐</h2>
            <div class="scenario-cards">
                <div class="scenario-card">
                    <h3>实时通知</h3>
                    <p>服务器向客户端推送通知消息,客户端不需要向服务器发送数据。</p>
                    <div class="recommend rec-sse">推荐: SSE</div>
                </div>
                <div class="scenario-card">
                    <h3>聊天应用</h3>
                    <p>双向通信,客户端和服务器都需要发送消息。</p>
                    <div class="recommend rec-ws">推荐: WebSocket</div>
                </div>
                <div class="scenario-card">
                    <h3>数据监控</h3>
                    <p>服务器推送实时数据(股票、传感器等),客户端只接收。</p>
                    <div class="recommend rec-sse">推荐: SSE</div>
                </div>
                <div class="scenario-card">
                    <h3>在线协作</h3>
                    <p>多人实时编辑,需要双向同步状态。</p>
                    <div class="recommend rec-ws">推荐: WebSocket</div>
                </div>
                <div class="scenario-card">
                    <h3>新闻推送</h3>
                    <p>服务器推送最新新闻,客户端浏览阅读。</p>
                    <div class="recommend rec-sse">推荐: SSE</div>
                </div>
                <div class="scenario-card">
                    <h3>在线游戏</h3>
                    <p>高频双向通信,低延迟要求。</p>
                    <div class="recommend rec-ws">推荐: WebSocket</div>
                </div>
            </div>
        </div>
    </div>
</body>
</html>

浏览器兼容性

特性 Chrome Firefox Safari Edge 说明
EventSource 6+ 6+ 5+ 79+ 现代浏览器支持
自定义事件 6+ 6+ 5+ 79+ addEventListener
Last-Event-ID 6+ 6+ 5+ 79+ 自动重连
withCredentials 6+ 6+ 5+ 79+ 跨域Cookie
IE支持 不支持 不支持 不支持 不支持 需polyfill

Polyfill:eventsource polyfill库可在IE中使用SSE。

注意事项与最佳实践

1. 连接管理

  • 浏览器对同域SSE连接数有限制(通常6个)

  • 不需要时及时关闭连接

  • 页面隐藏时考虑断开连接

2. 事件格式

  • 确保每个事件以空行结尾

  • 多行data使用多个data字段

  • JSON数据放在单个data字段中

3. 错误处理

  • 监听onerror事件

  • 检查readyState判断连接状态

  • 实现自定义重连逻辑(如果需要)

4. 安全考虑

  • 使用HTTPS传输敏感数据

  • 验证事件数据格式

  • 实现认证(Cookie或URL参数)

代码规范示例

SSE客户端封装

代码示例

class SSEClient {
    constructor(url, options = {}) {
        this.url = url;
        this.options = { withCredentials: false, ...options };
        this.eventSource = null;
        this.listeners = new Map();
    }

    connect() {
        this.eventSource = new EventSource(this.url, {
            withCredentials: this.options.withCredentials
        });

        this.eventSource.onopen = () => this._emit('open');
        this.eventSource.onerror = (e) => {
            if (this.eventSource.readyState === EventSource.CLOSED) {
                this._emit('close');
            } else {
                this._emit('error', e);
            }
        };
        this.eventSource.onmessage = (e) => {
            this._emit('message', { data: e.data, id: e.lastEventId });
        };

        // 注册自定义事件监听
        for (const [event] of this.listeners) {
            if (event !== 'open' && event !== 'error' && event !== 'close' && event !== 'message') {
                this.eventSource.addEventListener(event, (e) => {
                    this._emit(event, { data: e.data, id: e.lastEventId });
                });
            }
        }
    }

    on(event, callback) {
        if (!this.listeners.has(event)) this.listeners.set(event, []);
        this.listeners.get(event).push(callback);
    }

    off(event, callback) {
        if (!this.listeners.has(event)) return;
        if (callback) {
            const cbs = this.listeners.get(event).filter(cb => cb !== callback);
            this.listeners.set(event, cbs);
        } else {
            this.listeners.delete(event);
        }
    }

    _emit(event, data) {
        (this.listeners.get(event) || []).forEach(cb => cb(data));
    }

    close() {
        if (this.eventSource) {
            this.eventSource.close();
            this.eventSource = null;
        }
    }
}

// 使用
const sse = new SSEClient('/api/events');
sse.on('open', () => console.log('SSE已连接'));
sse.on('update', (e) => console.log('更新:', e.data));
sse.on('notification', (e) => console.log('通知:', e.data));
sse.connect();

常见问题与解决方案

问题1:IE不支持EventSource

解决方案:使用eventsource polyfill库,或用Fetch API手动解析SSE流。

问题2:连接数限制

解决方案:合并多个SSE端点为一个,使用事件类型区分不同数据。

问题3:SSE连接被代理超时

解决方案:定期发送心跳注释(: heartbeat\n\n),保持连接活跃。

问题4:需要双向通信

解决方案:SSE + Fetch组合,SSE接收服务器推送,Fetch发送客户端请求。

总结

SSE是轻量级的服务器推送方案:

  1. 单向推送:服务器→客户端,适合推送和通知场景

  2. 简单API:EventSource接口,开箱即用

  3. 自动重连:内置重连机制和事件ID续传

  4. 事件类型:支持自定义事件类型,便于消息路由

  5. 基于HTTP:兼容代理、CDN,部署简单

  6. 与WebSocket对比:SSE更简单但只支持单向,WebSocket更强大但更复杂

选择SSE还是WebSocket取决于场景:单向推送选SSE,双向通信选WebSocket。

常见问题

什么是SSE服务器推送?

SSE服务器推送是HTML5开发中的重要技术,本教程详细介绍了其核心概念和实践方法。

如何学习SSE服务器推送的实际应用?

教程中提供了完整的代码示例和实践指导,建议结合示例代码动手练习。

SSE服务器推送有哪些注意事项?

常见注意事项包括兼容性、性能优化等,教程的注意事项与最佳实践部分有详细说明。

SSE服务器推送适合初学者吗?

本教程从基础概念讲起,循序渐进,适合有一定HTML和JavaScript基础的初学者。

SSE服务器推送的核心要点是什么?

核心要点包括教程简介等内容,建议按教程顺序逐步学习。

标签: Promise HTTP头部 OAuth Canvas 网络安全 async await 场景管理 JWT

本文由小确幸生活整理发布,转载请注明出处

本文涉及AI创作

内容由AI创作,请仔细甄别

list快速访问

上一篇: 后端通信:WebSocket实时通信 - 从入门到实践详解 下一篇: 后端通信:14. 表单数据提交 - 从入门到实践详解

poll相关推荐