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 });| 参数 | 类型 | 说明 |
|---|---|---|
url | string | SSE 服务器地址 |
withCredentials | boolean | 跨域时是否发送 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>注意事项
EventSource只支持 GET 请求,无法发送请求体- 跨域 SSE 需要服务器设置正确的 CORS 头
- 同一域名最多 6 个并发 EventSource 连接
close()后不会自动重连- 自动重连会发送
Last-Event-ID请求头
最佳实践
- 为不同类型的数据使用不同的事件名称
- 在服务器端正确发送 SSE 格式(注意
\n\n结尾) - 监听
error事件区分连接状态 - 使用
id字段支持断点续传 - 需要发送参数时使用
fetch+ReadableStream替代
下一节
继续学习:SSE vs WebSocket 对比