1、为什么使用EventSource,Fetch + Accept: text/event-stream, 也可以吧?
可以
// 手动处理所有细节
const response = await fetch('/api/stream', {
headers: { 'Accept': 'text/event-stream' }
});
const reader = response.body.getReader();
const decoder = new TextDecoder();
while (true) {
const { value, done } = await reader.read();
if (done) break;
const chunk = decoder.decode(value);
// 需要手动解析: "data: {...}\n\n"
const lines = chunk.split('\n');
// 还要自己处理重连、错误恢复...
}
2、EventSource只能GET请求,如果需要传递参数怎么办?
拼接URL,复杂的情况只能其他形式处理了,例如先 POST 创建,再 GET 订阅
3、什么情况用EventSource(高级api),什么情况用Fetch + Accept(低级)?他们的区别是什么?
95% 的情况下用EventSource ,因为:
- 代码简洁,维护成本低,10行代码解决,Fetch + Accept需要50行
- 浏览器优化更好
- 符合 SSE 设计初衷
只有在你需要:
- POST 请求发送数据
- 非标准协议解析
- 特殊认证/重试逻辑
- 现有代码库已用 Fetch
4、SSE的默认重连行为:EventSource的规范中没有提供设置超时或重连间隔的API。因此这种想法EventSource无法直接实现。
这个Fetch + Accept可以自定义超时设置和重连间隔设置,简陋有简陋的好处,以下是ai生成的代码,可以参考思路
构造:
class CustomSSEClient {
constructor(url, options = {}) {
this.url = url;
this.options = {
retryCount: 3,
retryDelay: 1000,
timeout: 30000,
onMessage: () => {},
onError: () => {},
onOpen: () => {},
...options
};
this.retries = 0;
this.controller = null;
this.isConnected = false;
}
async connect() {
try {
this.controller = new AbortController();
const timeoutId = setTimeout(() => {
this.controller.abort();
}, this.options.timeout);
const response = await fetch(this.url, {
method: 'GET',
headers: {
'Accept': 'text/event-stream',
'Cache-Control': 'no-cache',
},
signal: this.controller.signal
});
clearTimeout(timeoutId);
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
this.isConnected = true;
this.retries = 0; // 重置重试计数
this.options.onOpen();
await this.processStream(response.body);
} catch (error) {
this.handleError(error);
}
}
async processStream(readableStream) {
const reader = readableStream.getReader();
const decoder = new TextDecoder();
try {
while (this.isConnected) {
const { value, done } = await reader.read();
if (done) {
console.log('Stream completed');
break;
}
const chunk = decoder.decode(value);
this.parseAndHandleEvents(chunk);
}
} catch (error) {
this.handleError(error);
} finally {
reader.releaseLock();
}
}
parseAndHandleEvents(chunk) {
const lines = chunk.split('\n');
let currentEvent = {};
for (const line of lines) {
if (line.startsWith('data: ')) {
currentEvent.data = line.slice(6);
} else if (line.startsWith('event: ')) {
currentEvent.type = line.slice(7);
} else if (line.startsWith('id: ')) {
currentEvent.id = line.slice(4);
} else if (line.startsWith('retry: ')) {
// 忽略服务器的retry指令,我们自己控制
} else if (line === '') {
// 空行表示事件结束
if (currentEvent.data) {
this.options.onMessage({
type: currentEvent.type || 'message',
data: currentEvent.data
});
}
currentEvent = {};
}
}
}
handleError(error) {
this.isConnected = false;
// 特定错误不重试
if (error.name === 'AbortError') {
console.log('Connection timeout');
this.options.onError(new Error('Connection timeout'));
return;
}
if (this.retries < this.options.retryCount) {
this.retries++;
const delay = this.options.retryDelay * this.retries; // 指数退避
console.log(`Reconnecting in ${delay}ms (attempt ${this.retries})`);
setTimeout(() => this.connect(), delay);
} else {
console.log('Max retries exceeded');
this.options.onError(new Error('Max retries exceeded'));
}
}
close() {
this.isConnected = false;
if (this.controller) {
this.controller.abort();
}
}
}
基础使用:
const sseClient = new CustomSSEClient('/api/stream', {
retryCount: 5,
retryDelay: 2000,
timeout: 15000,
onMessage: (event) => {
try {
const data = JSON.parse(event.data);
console.log('Received:', data);
if (data.status === 'done') {
sseClient.close(); // 任务完成,主动关闭
}
} catch (err) {
console.error('Parse error:', err);
}
},
onError: (error) => {
console.error('SSE Error:', error.message);
// 可以显示用户友好的错误信息
showErrorMessage(`连接失败: ${error.message}`);
},
onOpen: () => {
console.log('SSE连接已建立');
showConnectionStatus('connected');
}
});
// 开始连接
sseClient.connect();
// 手动关闭
// sseClient.close();
5、断点重连机制
断点重连机制,如果中断了之后,下次请求带上新的seq序号,后端应该不会去重新请求第三方ai接口了吧?那意思是他会把本次请求存储起来,后续再去读?那什么时候删除呢?
举例说明:应该是前端每次都需要传一个会议任务号给后端,例如前端告诉后端老大我们有个任务号为“A”的任务需要执行,后端老大看见任务“A”之后先去看任务“A”是否有会议室,如果没有后端老大会开一个会议室(假设为“H1”),有就告诉前端去对应会议室,于是第一次请求的时候后端开发会去“H1”的会议室去执行任务,前端从会议室的门“door1”进去等待后端不断返回结果,由于前端的各种原因(网络,手动关闭等)导致前端离开了会议室,但是后端不管你,继续执行他的任务,执行完一些给你一些,发现前端走了之后,就会把先执行完的结果放一边,然后把门“door1”关上,如果半小时之后还是没有人来,那就把会议室关了,所有请求结果给后端老大,后端老大存起来这个任务A的结果,下次前端需要请求的时候拿着任务号“A”去找后端老大,重复以上流程,当发现有任务A有会议室那就让前端从新门(door2)进去(因为新的一次请求需要重新发起链接,上一次的门door已经关闭不可用了)会议室A等结果,进门之后发现有一些任务已经完成了,前端就可以直接拿到这些任务先渲染到页面上,然后后端也会把后续的任务给前端
看着有点乱,讲故事的形式描述一下
公司组织架构
| 角色 | 职责 | 技术对应 |
|---|---|---|
| 前端 | 前端任务 | 浏览器客户端 |
| 后端老大 | 分配任务 | SessionManager |
| 后端开发 | 调用ai接口,真正干活的 | AI服务 |
| 会议室 | 项目工作区 | ConversationSession |
| 门 | 进展汇报通道 | SSE连接 |
| 任务号 | 项目编号 | ConversationId |
| 文件柜 | 成果存档处 | generatedChunks数组 |
第一章:新项目启动
// 周一早上9:00
前端:"老大!我们接到客户新需求,任务号'A',请安排后端开发"
后端老大:"收到!让我看看任务'A'..."
检查公司项目登记表...
"任务'A'没有会议室"
"创建新会议室'A1',安排后端开发工程师入场"
后端开发进入会议室A1:"我开始干活了!"
开始编码...
每完成一个小模块,就朝门口喊:"我要开始了"
"我先喝口水"
"开始开发了"
"摸会鱼"
"继续开发"
......
第二章:突发状况处理
// 前端突然掉线(网络问题)
前端:"太难等了,我去摸会鱼,待会见"
会议室门1自动关闭
// 但会议室内部:
后端开发:"前端虽然走了,但工作继续。"
"继续开发"
"摸会鱼"
把所有成果都记录在会议室白板上
// 30分钟后前端恢复
前端:"老大,我摸鱼回来了,我想看看任务'A'做的怎么样了,后端有没有摸鱼"
后端老大:"好的,我看看,任务'A'的会议室还在"
"给你开个新门2进入会议室A1"
"这是你错过的进展:"
- 启动了项目
- 完成了需求分析
- 喝了口水
- 开始开发了一行代码
前端通过门2进入,立即看到白板上的所有进展
第三章:多人协作场景
// 另一个前端也想了解任务'A'
前端2:"老大,我也想听任务'A'的进展!"
后端老大:"没问题!任务'A'会议室还在运行"
"给你开个新门3进入会议室A1"
现在:
会议室A1内:后端开发继续工作
门2:前端1在收听
门3:前端2在收听
后端开发:"兄弟们,我做完了"
同时向门2和门3喊话
第四章:项目完成与收尾
后端开发:"任务'A'全部完成,最终报告已提交。"
后端老大:
向所有开着的门喊:"项目已完成!"
关闭所有门(前端连接断开)
启动清理倒计时30分钟:
- 期间有新前端来问 → 直接给最终报告
- 30分钟后无人问津 → 清空会议室,开发工程师下班
各种场景演绎
场景A:网络波动
前端:"老大,任务'A'的进展!"
后端老大:"门1已开,请收听..."
[网络断开]
前端:"啊!门1断了!"
后端开发:"继续工作,不理他..."
[网络恢复]
前端:"老大,重新听任务'A'!"
后端老大:"门2已开!这是错过的内容..."
场景B:多标签页
// 同一个任务在多个浏览器标签页打开
标签页1:"老大,任务'A'!" → 开门1
标签页2:"老大,任务'A'!" → 开门2
后端开发:"完成模块X!"
同时喊给门1和门2
场景C:任务完成后的查询
// 任务已完成30分钟内
前端:"老大,任务'A'的最终结果?"
后端老大:"项目已完工,这是最终报告..."
直接给存档的完整结果
// 任务已完成31分钟
前端:"老大,任务'A'?"
后端老大:"会议室已清理,重新提需求做吧,代码已经被我删了..."
核心设计思想
1. 职责分离
后端开发:只负责在会议室里干活
后端老大:只负责会议室和门的调度
前端:只负责通过门收听进展
2. 状态持久化
会议室状态独立于门的存在
门可以随便开关,不影响会议室工作
所有进展都记录在白板上,永不丢失
3. 资源优化
一个后端开发服务多个前端
工作只做一次,成果多人共享
无人关注时自动清理资源
思考来源:EventSource