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

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

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

需求背景

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

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

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

SSE(Server-Sent Events)是一種基于HTTP的單向服務(wù)端推送技術(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)。
  • 默認行為
    • 使用簡單GET請求建立連接。
    • 響應(yīng)頭需包含 Content-Type: text/event-stream。
    • 自動處理連接重連(客戶端默認重試機制)。

3. 事件流格式

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

4. 自動重連

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

5. 適用場景

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

6. 限制

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

現(xiàn)有方案

針對SSE接口瀏覽器其實給我提供了一個簡單高效的api(EventSource),他是一個構(gòu)造函數(shù),其實例會對HTTP服務(wù)器開啟一個持久化的連接,以 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ù)進行業(yè)務(wù)處理
    ...業(yè)務(wù)代碼
};

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

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

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

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

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ù)進行業(yè)務(wù)處理
        ...業(yè)務(wù)代碼
      }
    });
  }
}

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

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

這種方式實現(xiàn)需要后端分塊傳輸,然后去監(jiān)聽progress事件,無法真正實現(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é)

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

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

// streamUtils.ts

/**
 * SSE 請求配置選項
 * @template T - 期望解析的數(shù)據(jù)類型 (默認為 string)
 */
type SSEOptions<T> = {
  /**
   * 請求目標 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ù)到達回調(diào) (必需)
   * @param data - 解析后的數(shù)據(jù)對象
   */
  onOpen: (data: String) => void;
  onData: (data: T) => void;

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

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

/**
 * SSE 流處理核心方法
 * @template T - 期望解析的數(shù)據(jù)類型
 * @param options - 配置選項
 * @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();

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

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

  // 解構(gòu)配置參數(shù)并設(shè)置默認值
  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é)束

        // 解碼當前 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() || '';

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

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

      onComplete?.(); // 觸發(fā)完成回調(diào)
    } catch (error) {
      // 忽略主動中斷產(chǎn)生的錯誤
      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) => {
      // 處理初始請求錯誤 (如網(wǎng)絡(luò)問題)
      if (error instanceof Error && error.name !== 'AbortError') {
        onError?.(error);
      }
    });

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

/**
 * 默認 SSE 解析器
 * @param eventChunk - 原始事件字符串
 * @returns 解析后的數(shù)據(jù)對象或 null
 *
 * @example 輸入示例
 * "data: Hello\ndata: World\n\n"
 *
 * @example 輸出結(jié)果
 * "Hello\nWorld" (自動合并多個 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) {
      // 有返回數(shù)據(jù)了
     
    },
    onError(err) {
      console.log("會話失敗");
      error.value = `請求失敗: ${err.message}`;
      
    },
    onComplete() {
      console.log("會話結(jié)束");
      
    },
  });

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

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

相關(guān)文章

最新評論

上林县| 广宗县| 敦化市| 闻喜县| 枝江市| 正安县| 三穗县| 阿拉善左旗| 农安县| 海阳市| 镇宁| 石嘴山市| 青海省| 龙州县| 抚松县| 达日县| 谷城县| 津南区| 石屏县| 梅河口市| 准格尔旗| 海晏县| 桐柏县| 棋牌| 盐源县| 抚顺市| 余庆县| 九台市| 通江县| 广灵县| 万载县| 迭部县| 黄浦区| 莱阳市| 罗江县| 沾益县| 房产| 新巴尔虎左旗| 台湾省| 庆安县| 依安县|