最新国产好看的视频,伊人天堂AV在线,国产Aaaaaa视频,蜜臀视频在线观看一区,人妻av色图,密臀久久久精品影片,青青视频免费观看毛片,久草在线观看视,国产三级精品色情在线

前端通過fetch模擬調(diào)用SSE接口的實(shí)現(xiàn)方案

 更新時(shí)間:2025年09月09日 09:54:09   作者:山山而川840  
SSE即Server-Sent Events,是一種服務(wù)器向?yàn)g覽器推送數(shù)據(jù)的技術(shù),并且不斷地發(fā)送數(shù)據(jù),保持連接打開,這篇文章主要介紹了前端通過fetch模擬調(diào)用SSE接口的實(shí)現(xiàn)方案,需要的朋友可以參考下

需求背景

團(tuán)隊(duì)要在系統(tǒng)集成AI大模型,需要實(shí)現(xiàn)類似于市面上的AI問答的聊天窗口。

技術(shù)調(diào)研

由于大模型接口是SSE接口,我們先了解一下SEE接口的特性。

SSE(Server-Sent Events)是一種基于HTTP的單向服務(wù)端推送技術(shù),適用于實(shí)時(shí)數(shù)據(jù)更新場景(如新聞推送、股票行情)。以下是其核心特性:

1. 單向通信

  • 方向:僅服務(wù)端→客戶端單向推送,客戶端無法通過SSE通道發(fā)送數(shù)據(jù)(需配合其他API如fetch)。
  • 對比WebSocket:WebSocket是雙向通信,SSE更輕量且無需額外協(xié)議升級。

2. 基于HTTP協(xié)議

  • 兼容性:直接復(fù)用HTTP協(xié)議,無需像WebSocket那樣升級協(xié)議(Upgrade: websocket)。
  • 默認(rèn)行為
    • 使用簡單GET請求建立連接。
    • 響應(yīng)頭需包含 Content-Type: text/event-stream
    • 自動處理連接重連(客戶端默認(rèn)重試機(jī)制)。

3. 事件流格式

  • 數(shù)據(jù)格式:每條消息以data:開頭,以\n\n結(jié)尾,例如:
    data: {"time": "2025-09-02T00:00:00"}\n\n
    
  • 多字段支持
    • event:自定義事件類型(如 event: update\n)。
    • id:消息ID,用于斷線重連時(shí)定位。
    • retry:指定重連延遲(毫秒)。

4. 自動重連

  • 機(jī)制:連接中斷后,瀏覽器自動嘗試重新連接(默認(rèn)間隔約3秒)。
  • 控制:可通過retry字段或監(jiān)聽onerror事件自定義重試邏輯。

5. 適用場景

  • 推薦場景:實(shí)時(shí)通知、日志流、進(jìn)度更新等服務(wù)端主導(dǎo)推送的需求。
  • 不適用場景:需要客戶端頻繁交互(如在線游戲、聊天室)。

6. 限制

  • 協(xié)議限制:僅支持文本數(shù)據(jù)(二進(jìn)制需編碼為Base64)。
  • 瀏覽器限制
    • 最大并發(fā)連接數(shù)(同一域名下通常6個(gè),HTTP/2可復(fù)用)。
    • 部分老舊瀏覽器(如IE)不支持。

現(xiàn)有方案

針對SSE接口瀏覽器其實(shí)給我提供了一個(gè)簡單高效的api(EventSource),他是一個(gè)構(gòu)造函數(shù),其實(shí)例會對HTTP服務(wù)器開啟一個(gè)持久化的連接,以 text/event-stream 格式發(fā)送事件,此連接會一直保持開啟直到通過調(diào)用 EventSource.close()關(guān)閉,使用示例:

const eventSource = new EventSource('你的SSE接口地址');

eventSource.onmessage = (event) => {
  console.log('接收到數(shù)據(jù):', event.data);
  // 對響應(yīng)數(shù)據(jù)進(jìn)行業(yè)務(wù)處理
    ...業(yè)務(wù)代碼
};

eventSource.onerror = (error) => {
  console.error('SSE連接錯(cuò)誤:', error);
  // 可選:重連邏輯
};

上面這種方案簡單高效,但是他只能發(fā)送get請求,并且請求參數(shù)不能手動直接添加到URL上,必須通過手動構(gòu)造URL字符串來實(shí)現(xiàn)。

const params = new URLSearchParams({ key1: 'value1', key2: 'value2' });
const url = `/api/v1/sse?${params.toString()}`;
const eventSource = new EventSource(url);

如果想了解更多有關(guān) EventSource Web api 信息的,可以研讀下面這篇文檔EventSource:https://developer.mozilla.org/zh-CN/docs/Web/API/EventSource

問題來了

由于系統(tǒng)需要集成的SSE接口是post請求,所以上面的方案不適用,經(jīng)過調(diào)研有兩種實(shí)現(xiàn)方案:

1.使用fetch+ReadableStream實(shí)現(xiàn) SSE

由于fetch 可以通過流式響應(yīng)逐步讀取數(shù)據(jù),模擬SSE的持續(xù)特性。(強(qiáng)烈推薦)

async function fetchSSE(url, options = {}) {
  const response = await fetch(url, {
    ...options,
    // 請求頭可根據(jù)接口文檔自行調(diào)整
    headers: {
      'Accept': 'text/event-stream', // 聲明需要SSE格式
      ...options.headers,
    },
  });

  if (!response.ok) Promise.reject(new Error(response.status));

  const reader = response.body.getReader();
  const decoder = new TextDecoder();

  while (true) {
    const { done, value } = await reader.read();
    if (done) break;
    const chunk = decoder.decode(value);
    // 解析SSE格式的數(shù)據(jù)(如 "data: ...\n\n")
    chunk.split('\n\n').forEach(event => {
      if (event.trim()) {
        const data = event.replace(/^data: /, '').trim();
        // 持續(xù)解析數(shù)據(jù)進(jìn)行業(yè)務(wù)處理
        ...業(yè)務(wù)代碼
      }
    });
  }
}

// 調(diào)用示例
fetchSSE('url', {
  method: 'POST',
  headers: { 'Authorization': 'Bearer xxx' },
  body: JSON.stringify({ key: 'value' }),
}).catch(error => {
    // 錯(cuò)誤處理
})

2. 使用XMLHttpRequest(傳統(tǒng)Ajax)模擬SSE

這種方式實(shí)現(xiàn)需要后端分塊傳輸,然后去監(jiān)聽progress事件,無法真正實(shí)現(xiàn)流式解析,不推薦使用,案例代碼:

function xhrSSE(url) {
  const xhr = new XMLHttpRequest();
  xhr.open('GET', url, true);
  xhr.setRequestHeader('Accept', 'text/event-stream');
  xhr.onprogress = function() {
    // 增量獲取響應(yīng)文本(需后端支持分塊傳輸)
    const newData = xhr.responseText.substring(lastIndex);
    lastIndex = xhr.responseText.length;
    // 業(yè)務(wù)處理
    ...業(yè)務(wù)代碼
  };
  xhr.send();
}

總結(jié)

上面三種方式都能實(shí)現(xiàn)SSE接口的調(diào)用和響應(yīng)解析,如果是get請求推薦使用瀏覽器提供的api(EventSource),如果是post請求則推薦使用fetch;開發(fā)過程中遇到的業(yè)務(wù)場景,記錄一下,希望能夠幫助到遇到同樣問題的小伙伴。

附:fetch 模擬 sse 請求方式封裝

// streamUtils.ts

/**
 * SSE 請求配置選項(xiàng)
 * @template T - 期望解析的數(shù)據(jù)類型 (默認(rèn)為 string)
 */
type SSEOptions<T> = {
  /**
   * 請求目標(biāo) URL (必需)
   * @example "/api/chat-stream"
   */
  url: string;

  /**
   * fetch 請求初始化配置
   * @default { method: 'GET' }
   * @example {
   *   method: "POST",
   *   headers: { "Authorization": "Bearer token" },
   *   body: JSON.stringify({ prompt: "Hello" })
   * }
   */
  requestInit?: RequestInit;

  /**
   * 自定義事件塊解析器
   * @param eventChunk - 原始事件字符串 (包含 "data:" 等前綴)
   * @returns 解析后的數(shù)據(jù)對象或 null (表示無效數(shù)據(jù))
   * @default defaultParser
   */
  parser?: (eventChunk: string) => T | null;

  /**
   * 數(shù)據(jù)到達(dá)回調(diào) (必需)
   * @param data - 解析后的數(shù)據(jù)對象
   */
  onOpen: (data: String) => void;
  onData: (data: T) => void;

  /**
   * 錯(cuò)誤處理回調(diào)
   * @param error - 遇到的錯(cuò)誤對象
   */
  onError?: (error: Error) => void;

  /**
   * 流接收完成回調(diào)
   */
  onComplete?: () => void;
};

/**
 * SSE 流處理核心方法
 * @template T - 期望解析的數(shù)據(jù)類型
 * @param options - 配置選項(xiàng)
 * @returns 包含中止方法的對象 { abort: () => void }
 *
 * @example 基本使用
 * const { abort } = fetchStream({
 *   url: "/api/stream",
 *   onData: data => console.log(data),
 *   onError: err => console.error(err)
 * });
 *
 * @example 帶自定義解析器
 * fetchStream({
 *   parser: chunk => ({ msg: chunk.trim() }),
 *   // ...其他配置
 * });
 */
export function fetchStream<T = string>(options: SSEOptions<T>) {
  // 創(chuàng)建中止控制器用于中斷請求
  const controller = new AbortController();

  // 文本解碼器用于處理二進(jìn)制流
  const decoder = new TextDecoder();

  // 保存不完整的事件塊 (跨 chunk 的場景)
  let partialChunk = '';

  // 解構(gòu)配置參數(shù)并設(shè)置默認(rèn)值
  const { parser = defaultParser, onOpen, onData, onError, onComplete } = options;
  onOpen && onOpen('會話開始')
  /**
   * 處理流數(shù)據(jù)的內(nèi)部方法
   * @param response - fetch 返回的響應(yīng)對象
   */
  async function handleStream(response: Response) {
    try {
      // 獲取可讀流讀取器
      const reader = response.body?.getReader();
      if (!reader) throw new Error('Failed to get stream reader');

      // 持續(xù)讀取數(shù)據(jù)流
      while (true) {
        const { done, value } = await reader.read();
        if (done) break; // 流讀取結(jié)束

        // 解碼當(dāng)前 chunk 并拼接之前未完成的數(shù)據(jù)
        const chunk = decoder.decode(value, { stream: true });

        // 使用通用行結(jié)束符分割事件塊 (兼容不同系統(tǒng))
        // 注意:SSE 規(guī)范要求用 \n\n 分割,但某些服務(wù)可能使用 \r\n\r\n
        const events = (partialChunk + chunk).split(/\r\n\r\n|\n\n/);

        // 保存未完成的事件塊供下次處理
        partialChunk = events.pop() || '';

        // 處理每個(gè)完整的事件塊
        for (const eventChunk of events) {
          const data = parser(eventChunk);
          if (data !== null) {
            onData(data); // 觸發(fā)數(shù)據(jù)回調(diào)
          }
        }
      }

      // 處理剩余數(shù)據(jù) (最后一個(gè)事件塊)
      if (partialChunk) {
        const data = parser(partialChunk);
        if (data !== null) onData(data);
      }

      onComplete?.(); // 觸發(fā)完成回調(diào)
    } catch (error) {
      // 忽略主動中斷產(chǎn)生的錯(cuò)誤
      if (error instanceof Error && error.name !== 'AbortError') {
        onError?.(error);
      }
    }
  }

  // 發(fā)起 fetch 請求
  fetch(options.url, {
    ...options.requestInit, // 用戶自定義配置
    signal: controller.signal, // 綁定中止信號
    headers: {
      Accept: 'text/event-stream', // 確保接收 SSE 流
      ...options.requestInit?.headers, // 合并用戶自定義 headers
    },
  })
    .then(handleStream)
    .catch((error) => {
      // 處理初始請求錯(cuò)誤 (如網(wǎng)絡(luò)問題)
      if (error instanceof Error && error.name !== 'AbortError') {
        onError?.(error);
      }
    });

  // 返回中止方法供外部調(diào)用
  return {
    /** 中止當(dāng)前請求 */
    abort: () => controller.abort(),
  };
}

/**
 * 默認(rèn) SSE 解析器
 * @param eventChunk - 原始事件字符串
 * @returns 解析后的數(shù)據(jù)對象或 null
 *
 * @example 輸入示例
 * "data: Hello\ndata: World\n\n"
 *
 * @example 輸出結(jié)果
 * "Hello\nWorld" (自動合并多個(gè) data 行)
 */
function defaultParser<T = string>(eventChunk: string): T | null {
  try {
    let content = '';
    // 逐行處理事件內(nèi)容
    for (const line of eventChunk.split('\n')) {
      // 僅處理 data 字段 (忽略 event/id/retry 等)
      if (line.startsWith('data:')) {
        content += line.slice(5).trim() + '\n'; // 保留換行結(jié)構(gòu)
      }
    }
    content = content.trim(); // 去除首尾空白

    // 空內(nèi)容返回 null
    if (!content) return null;

    // 嘗試解析為 JSON,失敗則返回原始字符串
    try {
      return JSON.parse(content) as T;
    } catch {
      return content as unknown as T;
    }
  } catch (e) {
    console.error('SSE parsing error:', e);
    return null;
  }
}

調(diào)用方法

let abortController: (() => void) | null = null; // 中止控制器

// 發(fā)起SSE請求
const { abort } = fetchStream({
   url: `/ai/chat/memory`,
    requestInit: {
      method: "POST",
      body: formData
    },
    onOpen(data) {
      // 開始
    },
    onData(data) {
      // 有返回?cái)?shù)據(jù)了
     
    },
    onError(err) {
      console.log("會話失敗");
      error.value = `請求失敗: ${err.message}`;
      
    },
    onComplete() {
      console.log("會話結(jié)束");
      
    },
  });

  abortController = abort; // 保存中止函數(shù)
};
// 組件卸載時(shí)中止請求
onUnmounted(() => {
  abortController?.();
});

到此這篇關(guān)于前端通過fetch模擬調(diào)用SSE接口的文章就介紹到這了,更多相關(guān)前端模擬調(diào)用SSE接口內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!

相關(guān)文章

最新評論

中宁县| 岳池县| 甘孜| 乌什县| 彭州市| 灵宝市| 襄汾县| 高州市| 长白| 嘉鱼县| 金塔县| 佳木斯市| 出国| 南平市| 息烽县| 盐池县| 岳阳市| 广河县| 山西省| 林甸县| 顺昌县| 夏邑县| 合江县| 蒲江县| 水富县| 永胜县| 临潭县| 墨竹工卡县| 都江堰市| 大埔区| 华宁县| 嘉兴市| 巴彦县| 公主岭市| 临猗县| 高阳县| 昆山市| 石家庄市| 博野县| 通渭县| 南雄市|