深度拆解 fetch-event-source库实现原理

0 阅读3分钟

前言

在 AI 大模型火热的今天,流式输出(Streaming)已成为标配。虽然浏览器原生提供了 EventSource (SSE),但在复杂的业务实战中,它却显得力不从心。本文将带你深度剖析 fetch-event-source 的底层实现,看看它是如何突破原生限制,优雅实现流式交互的。

一、 为什么原生 EventSource 走到了尽头?

原生 EventSource 在 AI 聊天场景中有两个“死穴”:

  1. 方法受限:只能发送 GET 请求。AI 聊天往往需要携带庞大的上下文(Context),URL 长度限制是无法逾越的障碍。
  2. 鉴权困境:无法自定义 Header。在需要通过 Authorization 传递 Token 的现代 Web 应用中,这非常致命。

fetch-event-source 的出现,本质上是给 fetch 套上了一层 SSE 的协议外壳,完美继承了 fetch 的灵活性。


二、 核心原理:基于 ReadableStream 的流式解析

fetch-event-source 的核心魔法在于利用了 fetch 返回值中的 Response.body。它是一个 ReadableStream(可读流),允许我们在数据还没全部到达时,就开始处理已经“流”进来的字节块。

1. 协议头强制对齐

要模拟 SSE,请求头必须严格遵守规范:

  • Accept: text/event-stream:告知后端我们需要流式响应。
  • Cache-Control: no-cache:禁用缓存,确保实时性。
  • Connection: keep-alive:保持长连接。

2. 状态机解析逻辑

由于 SSE 格式具有高度可预测性(以 \n 分隔行,以 \n\n 分隔消息块),我们可以通过一个简单的状态机进行逐行扫描:

  • data: 开头 -> 暂存数据片段。
  • event: 开头 -> 记录事件类型。
  • retry: 开头 -> 更新客户端的重连等待时间。
  • 空行 (\n\n) -> 表示一条消息解析完成,触发 onmessage 回调。

三、 手写一个简易版

理解原理最好的方式就是复刻它。以下是基于 fetchTextDecoder 的核心实现逻辑:

async function fetchEventSource(url, options) {
  const { signal, onopen, onmessage, onerror, retryDelay = 1000 } = options;
  let retryCount = 0;

  // 1. 循环处理(失败重试)
  while (!signal.aborted) {
    try {
      const response = await fetch(url, {
        method: 'POST', // 突破 GET 限制,支持 POST 发送上下文
        headers: {
          'Accept': 'text/event-stream',
          'Cache-Control': 'no-cache',
          'Content-Type': 'application/json',
          ...options.headers,
        },
        body: JSON.stringify(options.body),
        signal,
      });

      // 2. 响应合法性校验
      if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
      if (!response.headers.get('Content-Type')?.includes('text/event-stream')) {
        throw new Error('Invalid Content-Type, expected text/event-stream');
      }

      onopen?.({ response });

      // 3. 读取流式响应体 (核心)
      const reader = response.body.getReader();
      const decoder = new TextDecoder('utf-8');
      let buffer = ''; 

      while (true) {
        const { done, value } = await reader.read();
        if (done) break;

        // 解码二进制数据并追加到缓冲区
        buffer += decoder.decode(value, { stream: true });
        
        // 4. 按 SSE 规范拆分消息块 (\n\n)
        let parts = buffer.split('\n\n');
        buffer = parts.pop(); // 最后一个可能是残缺的,留到下一轮处理

        for (const part of parts) {
          // 这里解析 data: event: 等字段
          const parsed = parseSSEPart(part); 
          onmessage?.(parsed);
        }
      }

      await reader.releaseLock();
      if (signal.aborted) break;

      throw new Error('Connection closed by server');
    } catch (error) {
      // 5. 错误处理与指数退避重连
      const retry = onerror?.(error) ?? true;
      if (!retry || signal.aborted) break;

      const delay = retryDelay * Math.pow(2, retryCount);
      await new Promise(resolve => setTimeout(resolve, delay));
      retryCount++;
    }
  }
}

四、 总结

fetch-event-source 并不是魔法,它只是站在了 fetchReadableStream 的肩膀上,通过手动实现 SSE 协议解析,解决了原生 API 的痛点。在 AI 对话应用中,它是实现实时、鉴权、高扩展性流式输出的最佳实践。