博客 / 詳情

返回

Fetch+stream實現流式輸出

Fetch API 允許你跨網絡獲取資源,真正好的是,瀏覽器最近增加了將 fetch 響應作為可讀流使用的能力。
這是一個簡單明瞭的fetch+stream輸出的示例:

try {
  // 定義手動停止接口
  abortController.value = new AbortController();
  
  // 發送請求
  let response = await fetch(url, {
    method: 'post',
    headers: {
      Authorization: '你的token',
      'Content-Type': 'application/json'
    },
    body: JSON.stringify(params), // 序列化傳入參數
    signal: abortController.value.signal 
  });
  
  // ok字段判斷是否成功獲取到數據流
  if (!response.ok) throw new Error('Network response was not ok');
  if (!response.body) throw new Error('Network response was not body');

  // 用來獲取一個可讀的流的讀取器(Reader)以流的方式處理響應體數據
  const reader = response.body.getReader();
  
  // 將流中的字節數據解碼為文本字符串
  const textDecoder = new TextDecoder();
  
  while (true) {
    // 檢查是否已中斷
    if (abortController.value.signal.aborted) break;
    
    // done表示流是否已經完成讀取  value包含讀取到的數據塊
    const { done, value } = await reader.read();
    if (done) {
      // 流式數據讀取完成
      break;
    }
    
    // 拿到的value就是後端分段返回的數據,大多是以data:開頭的字符串
    // 需要通過decode方法處理數據塊,例如轉換為文本或進行其他操作
    // (replace可選)數據可能不是你想要的格式,做特殊處理(僅移除單獨的換行符,保留連續的換行符)
    const chunkText = textDecoder.decode(value).replace(/(?<!\n)\n(?!\n)/g, '');
    
    // 無數據則跳過
    if (!chunkText) continue;
    
    // 有數據則拼接,此處可以寫你自己的邏輯代碼,比如數據清洗
    last.content += chunkText;

  }
  // 流式片段遍歷結束,停止響應
  responseClose();
} catch (err) {
  console.log(err);
  responseClose();
}
  1. 通過textDecoder.decode將流式字節數據解碼為文本字符串
  2. 通過response.body.getReader();讀取流的主體
  3. 通過reader.read();方法讀取流的分塊,讀出分塊後,可以做你喜歡的事

下面是一個在AI對話中的場景示例:

// 開始對話
const chatList = ref<any>([]);
const isChatting = ref(false);
const abortController = ref(); // 手動停止對話
const startChat = async (keyword: string) => {
  try {
    if (!keyword) return MessagePlugin.warning('請輸入內容');
    // 先停止對話
    await stopChat();
    let last = chatList.value.at(-1);
    // 參數
    let params = {}; 
    // 對話中
    isChatting.value = true;
    const url = 'xxxx/xxx/xxxAPI';
    abortController.value = new AbortController();
    // 發送請求
    let response = await fetch(url, {
      method: 'post',
      headers: {
        Authorization: '你的token',
        'Content-Type': 'application/json'
      },
      body: JSON.stringify(params), // 序列化傳入參數
      signal: abortController.value.signal // 定義手動停止接口
    });
    // 處理401
    if (response.status == 401) return login();
    // ok字段判斷是否成功獲取到數據流
    if (!response.ok) throw new Error('Network response was not ok');
    if (!response.body) throw new Error('Network response was not body');
    
    // 用來獲取一個可讀的流的讀取器(Reader)以流的方式處理響應體數據
    const reader = response.body.getReader();
    // 將流中的字節數據解碼為文本字符串
    const textDecoder = new TextDecoder();
    while (true) {
      // 檢查是否已中斷
      if (abortController.value.signal.aborted) {
        break;
      }
      // done表示流是否已經完成讀取  value包含讀取到的數據塊
      const { done, value } = await reader.read();
      if (done) {
        // 流式數據讀取完成
        break;
      }
      // 拿到的value就是後端分段返回的數據,大多是以data:開頭的字符串
      // 需要通過decode方法處理數據塊,例如轉換為文本或進行其他操作
      // (replace可選)數據可能不是你想要的格式,做特殊處理(僅移除單獨的換行符,保留連續的換行符)
      const chunkText = textDecoder.decode(value).replace(/(?<!\n)\n(?!\n)/g, '');
      // 無數據則跳過
      if (!chunkText) continue;
      // 有數據則拼接,此處可以寫你自己的邏輯代碼,比如數據清洗
      last.content += chunkText;
      // 滾動條置底-可選
      scrollMax(false);
    }
    // 流式片段遍歷結束
    responseClose();
  } catch (err) {
    console.log(err);
    responseClose();
  } finally {
  }
};

// 響應結束
const responseClose = () => {
  // 停止流式對話
  stopChat();
};

// 手動停止函數
const stopChat = async () => {
  // 手動終止fetch請求
  if (abortController.value) {
    abortController.value.abort();
    abortController.value = null;
  }
  // 停止當前對話
  isChatting.value = false;
  // 處理最後一條數據
  let lastData = chatList.value.at(-1);
  lastData.loading = false;
  lastData.contentEnd = true;
};

參考文檔:
Fetch API
使用流的方式處理 Fetch
TextDecoder.decode
AbortController:AbortController() 構造函數

user avatar
0 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.