← 精品代码功能 · 可复用实现库

精品功能 06:前端 SSE 流式客户端(半包处理 + 类型化事件)

(同类实现:查资料/web/js/common.js 及 script.js 的 readSSEStream、shijing-v5-dev/src/hooks/useSSEStream.ts)

正确处理"半个事件包"和错误分级,并有 TypeScript 类型保证。

所以必须手动读流。而网络传输会随机切包,必须处理不完整的半包。

核心实现

类型化事件协议(第 12-27 行)——把字符串事件名变成编译期可检查的联合类型:

export type ServerEvent =
  | { type: 'session'; session_id: string; max_rounds: number }
  | { type: 'worry'; text: string }
  | { type: 'angel'; text: string }
  | { type: 'interrupt'; by: string }
  | { type: 'judge'; angel: number; demon: number; ... }
  | { type: 'error'; message: string; retryable?: boolean };

流读取核心(第 77-107 行)——半包处理是关键:

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

for (;;) {
  const { value, done } = await reader.read();
  if (done) break;
  buffer += decoder.decode(value, { stream: true }).replace(/\r\n/g, '\n');
  let idx: number;
  while ((idx = buffer.indexOf('\n\n')) >= 0) {   // SSE 用空行分隔事件
    flush(buffer.slice(0, idx));                  // 完整事件才处理
    buffer = buffer.slice(idx + 2);
  }
}
if (buffer.trim()) flush(buffer);                 // 结尾残留也处理

错误分级(第 64-74 行)——把 FastAPI 的两种 detail 格式都变成人话:

if (!res.ok || !res.body) {
  let msg = `请求失败(HTTP ${res.status})`;
  try {
    const data = await res.json();
    if (typeof data?.detail === 'string') msg = data.detail;
    else if (Array.isArray(data?.detail)) msg = '参数不合法:' + data.detail.map(d => d.msg).join(';');
  } catch { /* ignore */ }
  onEvent({ type: 'error', message: msg, retryable: res.status >= 500 });
  return;
}

设计亮点

  1. 半包/粘包处理:buffer 累积 + 按 \n\n 切分 + 残留留到下一轮——流式解析的必修课
  2. AbortSignal 支持:可取消进行中的请求(切页面/重新开始时用)
  3. 错误分级:retryable: status >= 500 告诉调用方"值不值得重试"
  4. onEvent 回调:解析与业务解耦,谁用谁传处理函数
  5. 类型化事件:事件名/字段写错在编译期就报错(对比手写字符串事件名)
  6. X-API-Key 请求头:把密钥交给用户自己保管,服务端不必存

可复用性评估

开源化建议

对照开源

相关课程

第 09 课(流式输出)、第 13 课(前端工程化)、第 14 课(流式会话进阶)