Skip to content

EventSource API

EventSource 是浏览器提供的 SSE 客户端 API,用于接收服务器推送的事件流。本节详细讲解 EventSource 的构造方法、事件监听、自定义事件处理、连接状态管理以及与 fetch 配合实现带请求体的 SSE 连接。

前置知识

阅读本节前,建议先了解:Server-Sent Events 基础

基础概念

EventSource 构造函数

javascript
// 基本用法
const es = new EventSource(url);

// 带跨域凭证
const es2 = new EventSource(url, { withCredentials: true });
参数类型说明
urlstringSSE 服务器地址
withCredentialsboolean跨域时是否发送 Cookie

语法与 API 详解

创建 EventSource

javascript
// 同源连接
const es = new EventSource('/api/sse');

// 跨域连接(需要服务器支持 CORS)
const es2 = new EventSource('https://api.example.com/sse');

// 跨域连接(带凭证)
const es3 = new EventSource('https://api.example.com/sse', {
  withCredentials: true
});

监听默认消息

javascript
const es = new EventSource('/api/sse');

// 方式一:onmessage 属性
es.onmessage = (event) => {
  console.log('默认消息:', event.data);
};

// 方式二:addEventListener
es.addEventListener('message', (event) => {
  console.log('默认消息:', event.data);
});

// 解析 JSON 数据
es.onmessage = (event) => {
  const data = JSON.parse(event.data);
  console.log(data);
};

监听自定义事件

javascript
const es = new EventSource('/api/sse');

// 监听命名事件
es.addEventListener('notification', (event) => {
  console.log('通知:', event.data);
});

es.addEventListener('price-update', (event) => {
  const price = JSON.parse(event.data);
  console.log(`价格更新: ${price.symbol} = ${price.value}`);
});

es.addEventListener('user-online', (event) => {
  console.log(`用户上线: ${event.data}`);
});

es.addEventListener('alert', (event) => {
  alert(event.data);
});

内置事件

javascript
// 连接打开
es.onopen = () => {
  console.log('SSE 连接已建立');
};

// 错误事件
es.onerror = (event) => {
  if (es.readyState === EventSource.CONNECTING) {
    console.log('正在重连...');
  } else if (es.readyState === EventSource.CLOSED) {
    console.log('连接已关闭');
  }
};

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

详细说明

readyState 状态

javascript
const es = new EventSource('/api/sse');

switch (es.readyState) {
  case EventSource.CONNECTING: // 0
    console.log('正在建立连接');
    break;
  case EventSource.OPEN: // 1
    console.log('连接已建立,可以接收消息');
    break;
  case EventSource.CLOSED: // 2
    console.log('连接已关闭');
    break;
}

// 监听状态变化
let prevState = es.readyState;
const checkState = setInterval(() => {
  if (es.readyState !== prevState) {
    prevState = es.readyState;
    const states = ['CONNECTING', 'OPEN', 'CLOSED'];
    console.log('状态变化:', states[prevState]);
  }
}, 100);

URL 属性

javascript
const es = new EventSource('https://example.com/sse?id=123&token=abc');
console.log(es.url); // 'https://example.com/sse?id=123&token=abc'

服务器端数据格式详解

javascript
// ===== 服务器端 SSE 数据格式 =====

// 1. 最简单的消息
// "data: hello\n\n"

// 2. 多行数据
// "data: line 1\ndata: line 2\ndata: line 3\n\n"
// 客户端 event.data = "line 1\nline 2\nline 3"

// 3. 带事件类型
// "event: notification\ndata: 新消息\n\n"
// 客户端触发 'notification' 事件

// 4. 带 ID
// "id: 100\ndata: 消息内容\n\n"
// 断线重连时发送 Last-Event-ID: 100

// 5. 带重试间隔
// "retry: 5000\n\n"
// 重连间隔改为 5 秒

// 6. 注释(以 : 开头)
// ": 这是一条注释\n\n"
// 浏览器会忽略注释行

// 7. 组合使用
// "id: 42\nevent: price\ndata: {\"symbol\":\"AAPL\",\"price\":150.25}\nretry: 3000\n\n"

Node.js SSE 服务端完整示例

javascript
const http = require('http');

http.createServer((req, res) => {
  // 设置 CORS 头
  res.setHeader('Access-Control-Allow-Origin', '*');
  res.setHeader('Access-Control-Allow-Headers', 'Content-Type');

  // 处理 OPTIONS 预检请求
  if (req.method === 'OPTIONS') {
    res.writeHead(200);
    res.end();
    return;
  }

  // SSE 响应
  res.writeHead(200, {
    'Content-Type': 'text/event-stream',
    'Cache-Control': 'no-cache',
    'Connection': 'keep-alive'
  });

  // 发送注释
  res.write(': SSE 连接已建立\n\n');

  // 发送重试间隔
  res.write('retry: 3000\n\n');

  let msgId = 0;
  const interval = setInterval(() => {
    msgId++;

    // 默认消息
    res.write(`id: ${msgId}\n`);
    res.write(`data: ${JSON.stringify({ id: msgId, time: new Date().toISOString() })}\n\n`);

    // 自定义事件(每 10 条发一次)
    if (msgId % 10 === 0) {
      res.write(`event: milestone\ndata: 已发送 ${msgId} 条消息\n\n`);
    }

    // 价格更新事件
    res.write(`event: price\ndata: ${JSON.stringify({
      symbol: 'AAPL',
      price: (150 + Math.random() * 10).toFixed(2)
    })}\n\n`);
  }, 2000);

  req.on('close', () => {
    clearInterval(interval);
    console.log('客户端断开');
  });
}).listen(3000);

使用 fetch API 发送带参数的 SSE

EventSource 构造函数不支持自定义请求头和 POST 请求体。如需在 SSE 连接时发送参数,可以使用 fetch + ReadableStream

javascript
// 使用 fetch 发送带请求体的 SSE 连接
async function connectSSEWithBody(url, body) {
  const response = await fetch(url, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(body)
  });

  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');
    buffer = lines.pop(); // 保留未完成的行

    for (const line of lines) {
      if (line.startsWith('data: ')) {
        const data = line.slice(6);
        console.log('收到数据:', data);
      }
      if (line.startsWith('event: ')) {
        const eventType = line.slice(7);
        console.log('事件类型:', eventType);
      }
    }
  }
}

// 使用示例
connectSSEWithBody('/api/sse', { token: 'abc123', channel: 'updates' });

实战示例

SSE 实时日志监控

html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <title>SSE 日志监控</title>
  <style>
    body { font-family: monospace; padding: 20px; background: #1a1a2e; color: #e0e0e0; }

    .header {
      display: flex; justify-content: space-between; align-items: center;
      margin-bottom: 20px;
    }

    h2 { color: #4fc3f7; }

    .status { font-size: 13px; }
    .status.connected { color: #81c784; }
    .status.disconnected { color: #ef5350; }

    .log-panel {
      background: #0d0d1a; border-radius: 12px; padding: 16px;
      min-height: 500px; max-height: 70vh; overflow-y: auto;
    }

    .log-entry {
      padding: 3px 0;
      display: flex; gap: 12px;
    }

    .log-time { color: #666; min-width: 80px; }
    .log-level { min-width: 60px; font-weight: bold; }
    .log-level.info { color: #64b5f6; }
    .log-level.warn { color: #ffb74d; }
    .log-level.error { color: #ef5350; }
    .log-level.debug { color: #888; }
    .log-msg { color: #e0e0e0; word-break: break-all; }

    .controls {
      margin-top: 16px; display: flex; gap: 10px;
    }

    .btn {
      padding: 8px 20px; border: none; border-radius: 6px;
      cursor: pointer; font-size: 13px; font-weight: 600;
      color: white; background: #4fc3f7;
    }
  </style>
</head>
<body>
  <div class="header">
    <h2>实时日志监控</h2>
    <span class="status disconnected" id="status">未连接</span>
  </div>

  <div class="log-panel" id="logPanel">等待连接...</div>

  <div class="controls">
    <button class="btn" onclick="connectSSE()">连接</button>
    <button class="btn" onclick="clearLogs()">清空</button>
  </div>

  <script>
    let es = null;
    let count = 0;

    function connectSSE() {
      // 实际使用: es = new EventSource('/api/logs');
      count = 0;

      const levels = ['INFO', 'DEBUG', 'WARN', 'ERROR'];
      const messages = [
        '用户 admin 登录成功',
        '数据库查询耗时 230ms',
        '内存使用率超过 80%',
        '请求超时: GET /api/users',
        '缓存命中: key=user:123',
        'WebSocket 连接数: 156',
        '定时任务执行完成',
        'API 速率限制触发: 192.168.1.100'
      ];

      const interval = setInterval(() => {
        count++;
        const level = levels[Math.floor(Math.random() * levels.length)];
        const msg = messages[Math.floor(Math.random() * messages.length)];
        addLog(level, msg);
      }, 800);

      es = { interval };

      document.getElementById('status').textContent = '已连接 (模拟)';
      document.getElementById('status').className = 'status connected';
    }

    function addLog(level, msg) {
      const panel = document.getElementById('logPanel');
      const time = new Date().toLocaleTimeString('zh-CN', { hour12: false });
      const cls = level.toLowerCase();

      panel.innerHTML += `<div class="log-entry">
        <span class="log-time">${time}</span>
        <span class="log-level ${cls}">[${level}]</span>
        <span class="log-msg">${msg}</span>
      </div>`;

      panel.scrollTop = panel.scrollHeight;
    }

    function clearLogs() {
      document.getElementById('logPanel').innerHTML = '';
    }
  </script>
</body>
</html>

注意事项

  1. EventSource 只支持 GET 请求,无法发送请求体
  2. 跨域 SSE 需要服务器设置正确的 CORS 头
  3. 同一域名最多 6 个并发 EventSource 连接
  4. close() 后不会自动重连
  5. 自动重连会发送 Last-Event-ID 请求头

最佳实践

  1. 为不同类型的数据使用不同的事件名称
  2. 在服务器端正确发送 SSE 格式(注意 \n\n 结尾)
  3. 监听 error 事件区分连接状态
  4. 使用 id 字段支持断点续传
  5. 需要发送参数时使用 fetch + ReadableStream 替代

下一节

继续学习:SSE vs WebSocket 对比

参考链接