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

前端使用SSE實(shí)現(xiàn)流式渲染實(shí)踐方案

 更新時(shí)間:2026年05月21日 11:43:49   作者:在黃浦江游泳的碼農(nóng)  
這篇文章主要介紹了前端使用SSE實(shí)現(xiàn)流式渲染實(shí)踐的相關(guān)資料,文中通過代碼介紹了EventSource和Fetch兩種技術(shù)方案,以及具體實(shí)現(xiàn)案例,需要的朋友可以參考下

背景

最近在做一個(gè)AI項(xiàng)目,后端調(diào)用大模型API輸出的接口為SSE(服務(wù)端流式發(fā)送事件),前端需要不斷去監(jiān)聽并獲取SSE接口流出來的數(shù)據(jù),然后進(jìn)行動(dòng)態(tài)化渲染。

SSE(Server-Sent Events,服務(wù)器發(fā)送事件)是一種允許服務(wù)器向客戶端推送更新的技術(shù)。它基于 HTTP 協(xié)議,允許服務(wù)器向客戶端發(fā)送事件流,而無需客戶端不斷地向服務(wù)器發(fā)送請求來獲取更新。SSE 是一種單向通信機(jī)制,即服務(wù)器可以向客戶端發(fā)送數(shù)據(jù),但客戶端不能通過 SSE 向服務(wù)器發(fā)送數(shù)據(jù)(如果需要雙向通信,可以考慮使用 WebSocket)。

對于前端而言,如何接收SSE響應(yīng)流并完成頁面的動(dòng)態(tài)渲染是我們需要關(guān)注的。這篇文章主要想給做類似場景開發(fā)的同學(xué)做一個(gè)技術(shù)參考。

前端方案

EventSource

EventSource是一個(gè)用于創(chuàng)建到服務(wù)器的單向事件源的 API,允許服務(wù)器向客戶端推送事件。基于HTTP,EventSource的工作原理是建立一個(gè)持久的 HTTP 連接,服務(wù)器可以通過這個(gè)連接向客戶端發(fā)送消息,客戶端只負(fù)責(zé)接收消息,不需要向服務(wù)端發(fā)送輪詢請求。

const eventSource = new EventSource('https://example.com/events');

// 監(jiān)聽消息事件
eventSource.onmessage = (event) => {
  console.log('接收到消息:', event.data);
};

// 監(jiān)聽錯(cuò)誤事件
eventSource.onerror = (error) => {
  console.error('發(fā)生錯(cuò)誤:', error);
};

// 監(jiān)聽連接打開事件
eventSource.onopen = () => {
  console.log('連接已建立');
};

// 監(jiān)聽連接關(guān)閉事件
eventSource.onclose = () => {
  console.log('連接已關(guān)閉');
};

基本用法很簡單,注冊回調(diào)事件監(jiān)聽服務(wù)器響應(yīng)的數(shù)據(jù)流即可。

我們在onmessage回調(diào)中可以拿到服務(wù)器返回給我們的數(shù)據(jù),客戶端只需要被動(dòng)接收。當(dāng)請求失敗,連接斷開時(shí),EventSource 默認(rèn)會(huì)在連接斷開后自動(dòng)重連,如果需要自定義重連邏輯,需要在onerror事件處理程序中實(shí)現(xiàn)。

EventSource只支持GET請求,不支持自定義請求頭,沒有請求體,只能將請求參數(shù)拼接到url上,需要注意url長度限制。

Fetch

fetch 本身不直接支持流式輸出,但可以使用 ReadableStream TextDecoder 等 Web Streams API 來實(shí)現(xiàn)類似的效果。

const response = await fetch(apiUrl, {
        method: 'POST',
        credentials: 'include',
        headers: {
          'Content-Type': 'application/json',
        },
        body: JSON.stringify({
          messages: [
            {
              role: 'user',
              content: '前端學(xué)習(xí)曲線',
            },
          ],
        }),
      });
      const resHeader = response.headers;
      const contentType: string = resHeader.get('content-type') ?? '';
      if (contentType.indexOf('text/event-stream') > -1) {
        const reader = response?.body?.getReader();
        const decoder = new TextDecoder();

        while (true) {
          const { value, done } = (await reader?.read()) ?? {};
          if (done) break;
          const txt = decoder.decode(value);
          // 切割當(dāng)前流,并組裝成數(shù)組,若數(shù)組中包含多個(gè)流,則循環(huán)處理
          const strlist = txt?.split('\n\n')?.filter(Boolean);
          if (strlist) {
            for (const deltaItem of strlist) {
              getStreamValueToView(deltaItem);
            }
          }
        }
      }

這段代碼就是通過fetch完成的sse流式渲染,它通過構(gòu)造一個(gè)響應(yīng)可讀流,不斷循環(huán)讀取每一個(gè)流的信息,并解構(gòu)出流中的具體值和狀態(tài),當(dāng)狀態(tài)結(jié)束,中斷循環(huán),否則通過decoder.decode將字節(jié)流解碼為普通文本。然后根據(jù)SSE接口返回?cái)?shù)據(jù)的格式,對文本進(jìn)行切割組裝成一個(gè)新的數(shù)組,最后執(zhí)行渲染邏輯。

fetch請求可以支持POST請求方式來實(shí)現(xiàn)SSE效果,而且請求參數(shù)長度可以得到很大的拓展,符合長文本輸入的需求,另外Fetch是瀏覽器原生API支持度好,簡單易用。但是對SSE的適配不如EventSource,尤其想要控制響應(yīng)流的關(guān)閉時(shí),沒有EventSource方便。

以上兩種方式是實(shí)現(xiàn)SSE流式渲染的常用方案,下面就拿最近做的項(xiàng)目來梳理一下前端處理接口流式數(shù)據(jù)渲染的流程。

實(shí)現(xiàn)案例

對于頁面樣式以及其他交互功能的實(shí)現(xiàn)本文暫不做介紹,只聚焦如何渲染后端返回的數(shù)據(jù)。

先介紹一下整個(gè)實(shí)現(xiàn)背景:后端SSE接口有3min超時(shí)時(shí)間(網(wǎng)關(guān)限制),而SSE的響應(yīng)時(shí)間一般又會(huì)超過這個(gè)時(shí)間(模型思考時(shí)間較長),因此前端在監(jiān)聽SSE接口流式數(shù)據(jù)的時(shí)間不能大于3min,需要做分片渲染。后端的返回結(jié)果是字符串JSON,所以只需要解析這個(gè)JSON,就能拿到對應(yīng)的數(shù)據(jù)。

對于前端而言,整個(gè)流式數(shù)據(jù)的渲染,只需要關(guān)注響應(yīng)層面,因?yàn)槲覀冏鳛閿?shù)據(jù)的下游,只要能將數(shù)據(jù)渲染出來就可以了。由于具有停止回答功能,前端需要控制SSE響應(yīng)流的關(guān)閉,技術(shù)上選用EventSource實(shí)現(xiàn)。

/* 每過一個(gè)固定時(shí)間間隔執(zhí)行一次ES實(shí)例化 */
const intervalRun = (rid?: string, tid?: string) => {
      closeStream();
      eventSource.current = new EventSource(apiUrlByDetail(rid, tid));
      eventSource.current.onmessage = (event) => {
        render(event); // 核心渲染邏輯
      };
    // 請求斷開,需要關(guān)閉流
      eventSource.current.onerror = () => {
        closeStream();
        closeInterval();
      };
    };

closeStream是關(guān)閉流的方法:

function closeStream() {
    if (eventSource.current) {
      eventSource.current.close(); // 終止SSE響應(yīng)
      eventSource.current = null;
    }
  }

closeInterval是關(guān)閉定時(shí)器方法:

function closeInterval() {
    if (sourceReqTimerId.current) {
      clearInterval(sourceReqTimerId.current);
      sourceReqTimerId.current = null;
    }
  }

剛進(jìn)來先將之前沒有關(guān)閉的流關(guān)閉,然后重新實(shí)例化一個(gè)新的EventSource實(shí)例,服務(wù)器收到對應(yīng)請求,開始發(fā)送響應(yīng)流,客戶端不斷獲取響應(yīng)數(shù)據(jù),將數(shù)據(jù)傳入渲染方法,進(jìn)行頁面動(dòng)態(tài)渲染。

const interval = (currReqId?: string, currTraceId?: string) => {
      if (currReqId && currTraceId) {
        intervalRun(currReqId, currTraceId);
        sourceReqTimerId.current = setInterval(() => {
          intervalRun(currReqId, currTraceId);
        }, MAX_INTERVAL_TIME);
      }
    };

創(chuàng)建定時(shí)器,每隔MAX_INTERVAL_TIME執(zhí)行一次intervalRun。

下面就是渲染函數(shù),執(zhí)行核心渲染邏輯

const render = (event: Any) => {
      try {
        // 解析事件數(shù)據(jù),提取data字段
        const { data } = event.data ? JSON.parse(event.data) : {};
        // 存在reqId,開始執(zhí)行渲染邏輯
        if (data?.reqId) {
          // 解構(gòu)獲取reqId和traceId
          const { reqId: rid, traceId: tid } = data;
          // 設(shè)置請求ID和跟蹤ID
          setIdInfo({
            reqId: rid,
            traceId: tid,
          });
          // 獲取流數(shù)據(jù)并渲染到視圖
          getStreamValueToView(data, status);
        }
        // 流輸出結(jié)束
        if (data?.finished) {
          closeStream(); // 關(guān)閉流連接
          closeInterval(); // 清除定時(shí)器
          setRenderStatus(StreamStatusEnum.FINISHED); // 設(shè)置渲染狀態(tài)為已完成
        }
      } catch (error) {
        // 設(shè)置渲染狀態(tài)為錯(cuò)誤
        setRenderStatus(StreamStatusEnum.ERROR);
        // 打印錯(cuò)誤信息
        console.error(error);
      }
    };

因?yàn)槭莿?dòng)態(tài)流的輸出,因此我們需要?jiǎng)?chuàng)建一個(gè)狀態(tài)枚舉,不同狀態(tài)對應(yīng)不同的頁面展示。

/**
 * 流式渲染枚舉
 */
export enum StreamStatusEnum {
  /** 未開始 */
  NOT_BEGIN,
  /** 正在打印 */
  PRINTING,
  /** 打印結(jié)束 */
  FINISHED,
  /** 打印終止 */
  STOPPED,
  /** 打印出錯(cuò) */
  ERROR,
}

根據(jù)不同枚舉值,形成狀態(tài)的過渡。

如果存在reqId(標(biāo)識(shí)),代表當(dāng)前的響應(yīng)有效,可以向頁面渲染;如果出現(xiàn)結(jié)束標(biāo)識(shí)finished,則清除SSE連接和定時(shí)器,同時(shí)將狀態(tài)設(shè)置為完成狀態(tài)。

  function getStreamValueToView(deltaValue: Record<string, Any>, status: 'view' | 'detail' = 'view') {
    try {
      /** 針對SSE事件數(shù)據(jù)類型處理 */
      if (renderStatus === StreamStatusEnum.NOT_BEGIN) {
        setRenderStatus(StreamStatusEnum.PRINTING);
      }
      if (deltaValue) {
        /** 等待結(jié)果生成 */
        if (!deltaValue?.resultMap?.agentType) {
          setStartLoadingText(deltaValue?.responseAll || '');
        }
        const tasks = deltaValue?.resultMap?.multiAgent?.tasks;
        if (tasks?.length) {
          /** 對話渲染 */
          chatRender(tasks, status);
        }
      }
    } catch (error) {
      console.error(error);
    }
  }

能進(jìn)入這個(gè)方法,代表開始向頁面渲染,更新狀態(tài)為打印中。

拿到響應(yīng)數(shù)據(jù),若不存在agentType,展示loading狀態(tài),代表大模型還沒有開始向外輸出數(shù)據(jù)。(這里的標(biāo)識(shí)不是固定的,根據(jù)實(shí)際情況具體分析)

當(dāng)存在tasks.length,代表大模型已經(jīng)返回出內(nèi)容,此時(shí)走chatRender邏輯。chatRender中有一個(gè)核心的根據(jù)狀態(tài)更新的操作方法,入?yún)楫?dāng)前流,根據(jù)當(dāng)前流的狀態(tài)來進(jìn)行差異化更新。在本需求中,存在深度思考---數(shù)據(jù)分析---生成報(bào)告---總結(jié),共四種中間狀態(tài),如下代碼所示:

const operater = (currentTask: Any) => {
      /** 深度思考部分 */
      if (currentTask.messageType === MessageTypeEnum.ToolThought) {
        setDeepThinkingTextMap((prev) => ({
          ...prev,
          [currentTask.messageId]: currentTask.toolThought,
        }));
      }
      /** 數(shù)據(jù)分析模塊,展示數(shù)據(jù)分析狀態(tài) */
      if (currentTask.messageType === MessageTypeEnum.DataAnalysis) {
        setDeepThinkingTextMap((prev) => ({
          ...prev,
          [currentTask.messageId]: MessageTypeEnum.DataAnalysis,
        }));
      }
      /** 頁面報(bào)告部分,展示對應(yīng)報(bào)告內(nèi)容 */
      if (currentTask.messageType === MessageTypeEnum.Html) {
        setDeepThinkingTextMap((prev) => ({
          ...prev,
          [currentTask.messageId]: MessageTypeEnum.Html,
        }));
      }
      /** 結(jié)尾文案,總結(jié) */
      if (currentTask.messageType === MessageTypeEnum.Result) {
        setResultText(currentTask.result || '');
      }
    };

歷史數(shù)據(jù)需要被記錄進(jìn)映射中(交互需要),所以對于每一個(gè)最新的狀態(tài)數(shù)據(jù),都需要其對應(yīng)的messageId去綁定。然后回到chatRender,tasks是一個(gè)二維數(shù)組,這里我們暫時(shí)只取tasks?.[0](當(dāng)前需求場景下只依賴數(shù)組第一項(xiàng))。

function multiRender(tasks: Record<string, Any>[], status: 'view' | 'detail' = 'view') {
    const singlePlanTasks = tasks?.[0];
    const operater = (currentTask: Any) => {......}
    if (singlePlanTasks?.length) {
        const currentTask = singlePlanTasks[singlePlanTasks.length - 1] ?? {};
        operater(currentTask);
    }
  }

每次拿到當(dāng)前數(shù)組的最后一位(最新的流狀態(tài))。大模型是遞增返回的,最新的數(shù)據(jù)入棧,根據(jù)棧的后進(jìn)先出特性,最新入棧的最先被取走,因此每次獲取數(shù)組最后一位的數(shù)據(jù),就是當(dāng)前大模型返回的最新數(shù)據(jù)。

對于停止回答,只需要給按鈕綁定一個(gè)關(guān)閉方法,并發(fā)送一個(gè)關(guān)閉請求給到后端即可,后端拿到請求會(huì)同步執(zhí)行關(guān)閉操作。

async function onStop() {
    closeStream();
    closeInterval();
    setRenderStatus(StreamStatusEnum.STOPPED);
    const { traceId: tid, reqId: rid } = idInfo;
    await stopAnalysisStream({
      traceId: tid,
      reqId: rid,
    });
  }

以上就是整個(gè)SSE的前端渲染過程,我們重點(diǎn)關(guān)注響應(yīng)部分,并將響應(yīng)結(jié)構(gòu)解析出來,拿到頁面上需要的內(nèi)容,進(jìn)行更新/替換等一系列操作。

在和大模型進(jìn)行聊天交互的時(shí)候,大模型返回給我們的內(nèi)容并非一次性展示出來的,而是一個(gè)一個(gè)字吐出來的,像打字一樣。前端實(shí)現(xiàn)的方案可以采用字符串的拼接或替換:

  1. 拼接:后端返回的文案是斷斷續(xù)續(xù)的。

例如:
stream1: 今
stream2: 天
stream3: 是
stream4: 周
stream5: 一

此時(shí)需要進(jìn)行字符串的拼接,將拼接后的字符串渲染到頁面上。

  1. 替換:本需求實(shí)現(xiàn)方案,見下方代碼
setDeepThinkingTextMap((prev) => ({
    ...prev,
    [currentTask.messageId]: currentTask.toolThought,
 }));

currentTask.toolThought是遞增返回的,前端不斷去更新這個(gè)state,會(huì)連續(xù)觸發(fā)渲染,進(jìn)而實(shí)現(xiàn)頁面上的文本動(dòng)態(tài)打印,本質(zhì)上是用最新的流中的文案替換上次舊的文案

例如:
stream1: 今
stream2: 今 天
stream3: 今天 是
stream4: 今天是 周
stream5: 今天是周 一

以上兩種方案強(qiáng)依賴后端輸出的每個(gè)流中的文案長度,也可以前端去控制:定時(shí)器+切割字符串。

總結(jié)

到此這篇關(guān)于前端使用SSE實(shí)現(xiàn)流式渲染實(shí)踐方案的文章就介紹到這了,更多相關(guān)前端SSE流式渲染內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!

相關(guān)文章

  • JS實(shí)現(xiàn)手機(jī)號脫敏的方法詳解

    JS實(shí)現(xiàn)手機(jī)號脫敏的方法詳解

    脫敏指的是通過特定的技術(shù)手段對敏感數(shù)據(jù)進(jìn)行處理,使其不再直接暴露給用戶或系統(tǒng),防止敏感信息泄露,通常在測試、開發(fā)、數(shù)據(jù)處理等場景中使用,本文給大家介紹了JS實(shí)現(xiàn)手機(jī)號脫敏的方法,需要的朋友可以參考下
    2025-03-03
  • js實(shí)現(xiàn)的簡單圖片浮動(dòng)效果完整實(shí)例

    js實(shí)現(xiàn)的簡單圖片浮動(dòng)效果完整實(shí)例

    這篇文章主要介紹了js實(shí)現(xiàn)的簡單圖片浮動(dòng)效果,以完整實(shí)例形式分析了javascript實(shí)現(xiàn)圖片定時(shí)漂浮移動(dòng)功能的方法,涉及javascript定時(shí)函數(shù)與數(shù)值動(dòng)態(tài)運(yùn)算相關(guān)技巧,需要的朋友可以參考下
    2016-05-05
  • JS延時(shí)器提示框的應(yīng)用實(shí)例代碼解析

    JS延時(shí)器提示框的應(yīng)用實(shí)例代碼解析

    這篇文章主要介紹了JS延時(shí)器提示框的應(yīng)用實(shí)例代碼解析 的相關(guān)資料,需要的朋友可以參考下
    2016-04-04
  • JavaScript設(shè)計(jì)模式之代理模式簡單實(shí)例教程

    JavaScript設(shè)計(jì)模式之代理模式簡單實(shí)例教程

    這篇文章主要介紹了JavaScript設(shè)計(jì)模式之代理模式,簡單描述了代理模式的概念、功能、組成并結(jié)合實(shí)例形式較為詳細(xì)的分析了javascript代理模式的定義與使用相關(guān)操作技巧,需要的朋友可以參考下
    2018-07-07
  • javascript用函數(shù)實(shí)現(xiàn)對象的方法

    javascript用函數(shù)實(shí)現(xiàn)對象的方法

    這篇文章主要介紹了javascript用函數(shù)實(shí)現(xiàn)對象的方法,涉及javascript函數(shù)使用技巧,需要的朋友可以參考下
    2015-05-05
  • 50行代碼實(shí)現(xiàn)Webpack組件使用次數(shù)統(tǒng)計(jì)

    50行代碼實(shí)現(xiàn)Webpack組件使用次數(shù)統(tǒng)計(jì)

    這篇文章主要介紹了50行代碼實(shí)現(xiàn)Webpack組件使用次數(shù)統(tǒng)計(jì),文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧
    2021-03-03
  • 今天分享幾個(gè)少見卻很有用的 JS 技巧

    今天分享幾個(gè)少見卻很有用的 JS 技巧

    這篇文章主要介紹了今天分享幾個(gè)少見卻很有用的 JS 技巧,非常不錯(cuò),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下
    2021-10-10
  • countUp.js實(shí)現(xiàn)數(shù)字動(dòng)態(tài)變化效果

    countUp.js實(shí)現(xiàn)數(shù)字動(dòng)態(tài)變化效果

    這篇文章主要為大家詳細(xì)介紹了countUp.js實(shí)現(xiàn)數(shù)字動(dòng)態(tài)變化效果,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下
    2019-10-10
  • JS中數(shù)組合并的幾種常見方法

    JS中數(shù)組合并的幾種常見方法

    這篇文章主要給大家介紹了關(guān)于JS中數(shù)組合并的幾種常見方法,在開發(fā)的過程中,我們很多時(shí)候會(huì)遇到需要將兩個(gè)數(shù)組合并成一個(gè)數(shù)組的情況,文中通過實(shí)例代碼介紹的非常詳細(xì),出現(xiàn)需要的朋友可以參考下
    2023-07-07
  • js實(shí)現(xiàn)從左向右滑動(dòng)式輪播圖效果

    js實(shí)現(xiàn)從左向右滑動(dòng)式輪播圖效果

    這篇文章主要為大家詳細(xì)介紹了js實(shí)現(xiàn)從左向右滑動(dòng)式輪播圖效果,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下
    2017-07-07

最新評論

广南县| 怀化市| 基隆市| 绩溪县| 黑河市| 玉门市| 土默特右旗| 黔江区| 江城| 宜黄县| 临泽县| 东阳市| 雷波县| 平度市| 大理市| 遂宁市| 尼勒克县| 永泰县| 游戏| 社旗县| 青川县| 永和县| 杭锦旗| 怀仁县| 保亭| 澜沧| 什邡市| 达孜县| 襄垣县| 石首市| 沙湾县| 雷山县| 汝州市| 长兴县| 永嘉县| 奇台县| 横山县| 宜春市| 长白| 治多县| 上杭县|