【AI】EventSourcePolyfill实现流式数据
·
1. 什么是 SSE?
- 定义:SSE(Server-Sent Events,服务器发送事件)是一种基于 HTTP 协议的单向实时通信技术,允许服务器持续向客户端推送数据,客户端无需重复发起请求。
- 特点:
- 单向通信:仅服务器向客户端推送数据
- 长连接:一次连接持续传输,减少握手开销
- 文本协议:数据以 UTF-8 格式传输,遵循特定格式规范
- 自动重连:客户端断开后会自动尝试重连(默认机制)
2. 为什么需要 EventSourcePolyfill?
- 原生
EventSource局限性:- 浏览器兼容性差:不支持 IE 全版本、低版本 Safari/Chrome
- 不支持自定义请求头:无法携带认证 Token、Cookie 等关键信息
- 重连机制不灵活:缺乏自定义超时和重连策略配置
EventSourcePolyfill优势:- 完全兼容原生
EventSourceAPI,无缝替换 - 支持自定义请求头(如
Authorization) - 可配置心跳超时、重连策略
- 兼容所有现代浏览器及 IE 10+
- 完全兼容原生
3. 流式数据传输的应用场景
- AI 大模型对话(逐字生成响应)
- 实时日志监控(后端日志实时推送)
- 股票 / 行情数据实时更新
- 新闻 / 通知实时推送
- 在线协作工具(实时同步状态)
三、环境准备
1. 安装依赖
(1)npm 安装
bash
运行
# 核心依赖
npm install eventsource-polyfill --save
# TypeScript 项目需安装类型声明
npm install @types/eventsource-polyfill --save-dev
(2)CDN 引入(非工程化项目)
html
预览
<script src="https://cdn.jsdelivr.net/npm/eventsource-polyfill@1.0.31/dist/eventsource.min.js"></script>
2. 浏览器兼容性验证
| 浏览器 | 支持版本 | 备注 |
|---|---|---|
| Chrome | 4+ | 原生支持,polyfill 兼容 |
| Firefox | 6+ | 原生支持,polyfill 兼容 |
| Safari | 5+ | 低版本需 polyfill 支持 |
| Edge | 12+ | 原生支持,polyfill 兼容 |
| IE | 10+ | 仅 polyfill 支持 |
| 移动端浏览器 | 主流版本均支持 | 需 polyfill 适配低版本 |
四、基础使用教程
1. 核心 API 详解
(1)构造函数
javascript
运行
new EventSourcePolyfill(url, options);
- url:SSE 服务端接口地址(必填)
- options:配置对象(可选)
| 配置项 | 类型 | 说明 | 默认值 |
|---|---|---|---|
| headers | Object | 自定义请求头(如认证 Token) | {} |
| heartbeatTimeout | Number | 心跳超时时间(毫秒),超时后重连 | 45000 |
| withCredentials | Boolean | 跨域时是否携带 Cookie | false |
| reconnectInterval | Number | 重连间隔时间(毫秒) | 1000 |
(2)核心方法
| 方法名 | 说明 |
|---|---|
close() |
关闭 SSE 连接,停止接收数据 |
addEventListener(type, callback) |
监听指定类型事件 |
removeEventListener(type, callback) |
移除事件监听 |
(3)核心事件
| 事件名 | 触发时机 |
|---|---|
message |
接收服务器默认格式数据(无指定事件名) |
open |
连接建立成功时 |
error |
连接出错或断开时 |
| 自定义事件 | 服务器指定 event 字段时(如 notification) |
2. 最小化示例(原生 JS)
(1)前端代码
javascript
运行
// 引入 polyfill(工程化项目)
import EventSource from 'eventsource-polyfill';
// 初始化 SSE 连接
const es = new EventSourcePolyfill('http://localhost:3001/sse', {
headers: {
'Authorization': 'Bearer your-auth-token' // 携带认证 Token
},
heartbeatTimeout: 60000 // 60秒心跳超时
});
// 连接建立成功
es.addEventListener('open', () => {
console.log('SSE 连接已建立');
});
// 接收默认格式数据
es.addEventListener('message', (e) => {
console.log('收到数据:', JSON.parse(e.data));
});
// 接收自定义事件数据
es.addEventListener('notification', (e) => {
console.log('收到通知:', JSON.parse(e.data));
});
// 连接错误处理
es.addEventListener('error', (err) => {
console.error('SSE 连接错误:', err);
// 可自定义重连逻辑
if (err.readyState === EventSource.CLOSED) {
console.log('连接已关闭,准备重连...');
}
});
// 手动关闭连接(如页面卸载时)
window.addEventListener('beforeunload', () => {
es.close();
});
(2)服务端代码(Node.js/Express)
javascript
运行
const express = require('express');
const cors = require('cors');
const app = express();
// 跨域配置(SSE 必须允许跨域)
app.use(cors({
origin: 'http://localhost:3000', // 前端域名
allowedHeaders: ['Authorization'],
credentials: true
}));
// SSE 接口实现
app.get('/sse', (req, res) => {
// 配置 SSE 响应头(必须)
res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Cache-Control', 'no-cache');
res.setHeader('Connection', 'keep-alive');
res.setHeader('X-Accel-Buffering', 'no'); // 禁用 Nginx 缓冲
// 定时推送数据(模拟流式传输)
let count = 0;
const interval = setInterval(() => {
count++;
// 推送默认事件数据(message 事件)
res.write(`data: ${JSON.stringify({ type: 'default', content: `消息${count}` })}\n\n`);
// 每 5 次推送自定义事件数据
if (count % 5 === 0) {
res.write(`event: notification\ndata: ${JSON.stringify({ title: '通知', content: `第${count}条消息` })}\n\n`);
}
// 推送 10 次后停止
if (count === 10) {
clearInterval(interval);
res.write(`event: complete\ndata: {"status": "done"}\n\n`);
res.end();
}
}, 1000);
// 客户端断开连接时清理资源
req.on('close', () => {
clearInterval(interval);
res.end();
console.log('客户端断开连接');
});
});
// 启动服务器
app.listen(3001, () => {
console.log('SSE 服务启动:http://localhost:3001');
});
3. 关键格式规范
服务器推送数据必须遵循 SSE 协议格式,否则客户端无法解析:
- 基础格式:
data: 数据内容\n\n(末尾必须两个换行) - 自定义事件格式:
event: 事件名\ndata: 数据内容\n\n - 多行数据:
data: 第一行\ndata: 第二行\n\n(客户端会合并为一个字符串) - 示例:
plaintext
// 正确格式 data: {"name": "Alice"}\n\n // 自定义事件格式 event: userLogin data: {"id": 1001, "name": "Alice"}\n\n // 多行数据格式 data: 第一部分数据 data: 第二部分数据 data: 第三部分数据\n\n
五、框架集成实战
1. React 项目集成(AI 流式对话场景)
(1)组件实现
jsx
import React, { useState, useRef, useEffect } from 'react';
import EventSource from 'eventsource-polyfill';
const AIChatStream = () => {
const [input, setInput] = useState('');
const [chatHistory, setChatHistory] = useState([]);
const [currentResponse, setCurrentResponse] = useState('');
const esRef = useRef(null); // 存储 SSE 实例(避免重渲染丢失)
const isStreamingRef = useRef(false); // 标记是否正在流式传输
// 关闭 SSE 连接
const closeSSE = () => {
if (esRef.current) {
esRef.current.close();
esRef.current = null;
}
isStreamingRef.current = false;
};
// 发送 AI 请求并监听流式响应
const sendRequest = () => {
if (!input.trim() || isStreamingRef.current) return;
// 更新对话历史
setChatHistory(prev => [...prev, { role: 'user', content: input }]);
setCurrentResponse('');
setInput('');
isStreamingRef.current = true;
// 初始化 SSE 连接
esRef.current = new EventSourcePolyfill(
`http://localhost:3001/api/ai/stream?question=${encodeURIComponent(input)}`,
{
headers: { 'Authorization': 'Bearer your-ai-token' },
heartbeatTimeout: 60000
}
);
// 接收流式数据
esRef.current.addEventListener('message', (e) => {
const data = JSON.parse(e.data);
if (data.content) {
setCurrentResponse(prev => prev + data.content); // 逐字拼接响应
}
});
// 生成完成事件
esRef.current.addEventListener('complete', () => {
setChatHistory(prev => [...prev, { role: 'ai', content: currentResponse }]);
closeSSE();
});
// 错误处理
esRef.current.addEventListener('error', (err) => {
console.error('AI 流式错误:', err);
setCurrentResponse('响应失败,请重试~');
closeSSE();
});
};
// 组件卸载时关闭连接
useEffect(() => {
return () => closeSSE();
}, []);
return (
<div style={{ maxWidth: 800, margin: '0 auto', padding: 20 }}>
<h3>AI 流式对话</h3>
{/* 对话历史区域 */}
<div style={{
border: '1px solid #eee',
height: 400,
overflowY: 'auto',
padding: 10,
marginBottom: 10
}}>
{chatHistory.map((item, idx) => (
<div key={idx} style={{ margin: '10px 0', padding: 8 }}>
<strong>{item.role === 'user' ? '你' : 'AI'}:</strong>
<span>{item.content}</span>
</div>
))}
{/* 正在生成的 AI 响应 */}
{isStreamingRef.current && (
<div style={{ margin: '10px 0', padding: 8, backgroundColor: '#f5f5f5' }}>
<strong>AI:</strong>
<span>{currentResponse}</span>
<span style={{ animation: 'blink 1s infinite' }}>▌</span>
</div>
)}
</div>
{/* 输入区域 */}
<div style={{ display: 'flex' }}>
<input
type="text"
value={input}
onChange={(e) => setInput(e.target.value)}
placeholder="输入你的问题..."
style={{ flex: 1, padding: 8 }}
disabled={isStreamingRef.current}
/>
<button
onClick={sendRequest}
style={{ marginLeft: 10, padding: '8px 16px' }}
disabled={isStreamingRef.current || !input.trim()}
>
发送
</button>
</div>
<style jsx global>{`
@keyframes blink { 0%,100%{opacity:0} 50%{opacity:1} }
`}</style>
</div>
);
};
export default AIChatStream;
(2)服务端 AI 接口实现(对接 OpenAI)
javascript
运行
const express = require('express');
const cors = require('cors');
const { OpenAI } = require('openai');
const app = express();
// 初始化 OpenAI 客户端
const openai = new OpenAI({ apiKey: 'your-openai-api-key' });
// 跨域配置
app.use(cors({
origin: 'http://localhost:3000',
allowedHeaders: ['Authorization'],
credentials: true
}));
// AI 流式接口
app.get('/api/ai/stream', async (req, res) => {
const { question } = req.query;
const token = req.headers.authorization?.split(' ')[1];
// 认证验证
if (!token || token !== 'your-ai-token') {
res.status(401).send('未授权');
return;
}
// SSE 响应头配置
res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Cache-Control', 'no-cache');
res.setHeader('Connection', 'keep-alive');
res.setHeader('X-Accel-Buffering', 'no');
try {
// 调用 OpenAI 流式接口
const stream = await openai.chat.completions.create({
model: 'gpt-3.5-turbo',
messages: [{ role: 'user', content: question }],
stream: true,
temperature: 0.7
});
// 流式推送 AI 响应
for await (const chunk of stream) {
const content = chunk.choices[0]?.delta?.content || '';
if (content) {
res.write(`data: ${JSON.stringify({ content })}\n\n`);
}
}
// 生成完成
res.write(`event: complete\ndata: {"status": "done"}\n\n`);
res.end();
} catch (err) {
console.error('OpenAI 接口错误:', err);
res.write(`event: error\ndata: {"message": "AI 生成失败"}\n\n`);
res.end();
}
// 客户端断开连接时清理
req.on('close', () => {
stream?.destroy();
res.end();
});
});
app.listen(3001, () => {
console.log('AI 流式服务启动:http://localhost:3001');
});
2. Vue 项目集成(实时日志监控场景)
vue
<template>
<div class="log-monitor">
<h3>实时日志监控</h3>
<div class="log-container" ref="logContainer"></div>
<button @click="startMonitor" :disabled="isMonitoring">开始监控</button>
<button @click="stopMonitor" :disabled="!isMonitoring">停止监控</button>
</div>
</template>
<script>
import EventSource from 'eventsource-polyfill';
export default {
data() {
return {
es: null,
isMonitoring: false
};
},
methods: {
startMonitor() {
if (this.isMonitoring) return;
this.isMonitoring = true;
const logContainer = this.$refs.logContainer;
// 初始化 SSE 连接
this.es = new EventSourcePolyfill('http://localhost:3001/log/stream', {
headers: { 'Authorization': 'Bearer your-token' }
});
// 接收日志数据
this.es.addEventListener('message', (e) => {
const log = JSON.parse(e.data);
const logItem = document.createElement('div');
logItem.className = `log-item log-${log.level}`;
logItem.innerHTML = `[${new Date(log.timestamp).toLocaleString()}] [${log.level}] ${log.message}`;
logContainer.appendChild(logItem);
// 自动滚动到底部
logContainer.scrollTop = logContainer.scrollHeight;
});
this.es.addEventListener('error', (err) => {
console.error('日志监控错误:', err);
this.stopMonitor();
});
},
stopMonitor() {
if (this.es) {
this.es.close();
this.es = null;
}
this.isMonitoring = false;
}
},
beforeUnmount() {
this.stopMonitor();
}
};
</script>
<style scoped>
.log-monitor {
max-width: 1200px;
margin: 20px auto;
padding: 20px;
}
.log-container {
height: 500px;
border: 1px solid #eee;
padding: 10px;
overflow-y: auto;
margin: 10px 0;
font-family: monospace;
}
.log-item {
margin: 4px 0;
padding: 4px;
}
.log-info { color: #333; }
.log-warn { color: #ff9800; }
.log-error { color: #f44336; }
button {
margin-right: 10px;
padding: 8px 16px;
cursor: pointer;
}
button:disabled {
cursor: not-allowed;
opacity: 0.6;
}
</style>
六、高级优化技巧
1. 重连策略优化
默认重连机制可能无法满足复杂场景,可自定义重连逻辑:
javascript
运行
const createSSE = (url, options) => {
let retryCount = 0;
let es = new EventSourcePolyfill(url, options);
es.addEventListener('error', (err) => {
if (err.readyState === EventSource.CLOSED) {
retryCount++;
// 指数退避重连:1s → 2s → 4s → 8s(最多重试 5 次)
const delay = Math.min(1000 * Math.pow(2, retryCount), 8000);
setTimeout(() => {
if (retryCount <= 5) {
console.log(`第 ${retryCount} 次重连...`);
es = createSSE(url, options); // 递归重建连接
} else {
console.log('重连失败,请刷新页面');
}
}, delay);
}
});
return es;
};
// 使用自定义重连策略
const es = createSSE('http://localhost:3001/sse', {
headers: { 'Authorization': 'Bearer your-token' }
});
2. 心跳检测优化
避免长连接因无数据传输被断开:
javascript
运行
// 前端配置
const es = new EventSourcePolyfill('http://localhost:3001/sse', {
heartbeatTimeout: 60000 // 60秒超时
});
// 后端添加心跳推送
app.get('/sse', (req, res) => {
// ... 基础响应头配置
// 心跳定时器(每 20 秒推送一次空数据)
const heartbeatInterval = setInterval(() => {
res.write(`data: {"type": "heartbeat"}\n\n`);
}, 20000);
// 业务数据推送...
// 连接关闭时清理
req.on('close', () => {
clearInterval(heartbeatInterval);
res.end();
});
});
3. 数据节流与防抖
应对高频数据推送导致的 UI 卡顿:
javascript
运行
import { debounce } from 'lodash';
// 防抖处理:50ms 内多次数据推送只更新一次 UI
const updateUI = debounce((data) => {
console.log('更新 UI:', data);
// 渲染逻辑...
}, 50);
// 监听数据时使用防抖函数
es.addEventListener('message', (e) => {
const data = JSON.parse(e.data);
updateUI(data);
});
4. 中断流式传输
支持用户主动取消请求:
javascript
运行
// React 组件中
const cancelStream = () => {
if (esRef.current) {
esRef.current.close();
esRef.current = null;
isStreamingRef.current = false;
setCurrentResponse('已取消响应');
}
};
// 服务端配合
app.get('/api/ai/stream', async (req, res) => {
// ... 初始化逻辑
// 监听客户端关闭事件
req.on('close', () => {
// 中断 AI 模型调用(以 OpenAI 为例)
stream?.destroy();
console.log('用户取消了请求');
res.end();
});
});
5. 跨域与安全优化
(1)严格 CORS 配置
javascript
运行
app.use(cors({
origin: ['http://localhost:3000', 'https://your-domain.com'], // 白名单域名
methods: ['GET'], // SSE 仅支持 GET 方法
allowedHeaders: ['Authorization', 'Content-Type'],
credentials: true,
maxAge: 86400 // 预检请求缓存 24 小时
}));
(2)Token 认证优化
- 避免在 URL 中携带 Token(易被日志记录),通过
headers.Authorization传递 - 实现 Token 过期自动刷新机制:
javascript
运行
es.addEventListener('error', (err) => {
if (err.status === 401) {
// Token 过期,尝试刷新 Token
refreshToken().then(newToken => {
// 重新建立连接
es.close();
createSSE('http://localhost:3001/sse', {
headers: { 'Authorization': `Bearer ${newToken}` }
});
});
}
});
七、常见问题排查
1. 跨域错误(CORS Error)
- 检查服务端是否配置
Access-Control-Allow-Origin头 - 确保前端
withCredentials与后端credentials配置一致 - 确认允许的请求头包含前端传递的自定义头(如
Authorization)
2. 数据无法解析(SyntaxError)
- 检查服务器推送格式是否符合 SSE 规范(末尾必须两个换行)
- 确保推送的
data字段是合法的 JSON 字符串(避免未转义的特殊字符) - 多行数据需每行添加
data:前缀
3. 连接立即断开(readyState: CLOSED)
- 验证服务端响应头是否正确(
Content-Type: text/event-stream) - 检查服务端是否有数据推送(无数据推送会导致连接超时)
- 排查认证 Token 是否有效(401 错误会导致连接关闭)
4. 数据合并推送(失去流式效果)
- 后端添加
X-Accel-Buffering: no头(禁用 Nginx 缓冲) - 避免使用压缩中间件(如
compression),会缓冲响应 - 确保服务端每次
res.write后立即刷新(Node.js 无需额外操作)
5. IE 浏览器不支持
- 确认已正确引入
EventSourcePolyfill(IE 无原生支持) - IE 不支持
Promise,需额外引入es6-promisepolyfill:
bash
运行
npm install es6-promise --save
javascript
运行
import 'es6-promise/auto';
import EventSource from 'eventsource-polyfill';
八、总结与扩展
1. 核心要点总结
EventSourcePolyfill是跨浏览器 SSE 解决方案,兼容原生 API- 流式数据传输的核心是服务器持续推送片段数据,客户端实时渲染
- 关键配置:响应头格式、跨域设置、心跳超时、认证 Token
- 优化重点:重连策略、数据节流、安全认证、中断机制
2. 与其他实时通信技术对比
| 技术 | 优势 | 劣势 | 适用场景 |
|---|---|---|---|
| SSE(本文) | 基于 HTTP、实现简单、跨域友好、轻量 | 单向通信、仅文本传输 | 实时推送、日志监控、AI 对话 |
| WebSocket | 双向通信、支持二进制、低延迟 | 实现复杂、跨域配置繁琐 | 即时聊天、协作工具 |
| Polling(轮询) | 兼容性极高、实现最简单 | 开销大、延迟高 | 简单实时需求、旧浏览器兼容 |
3. 扩展学习资源
更多推荐

所有评论(0)