使用 Fetch API 執行串流要求

Jake Archibald
Jake Archibald

從 Chromium 105 版開始,您可以使用 Streams API 提出要求,再將整個主體提供給您。

這項功能可用來:

  • 暖機伺服器。 換句話說,您可以在使用者聚焦在文字輸入欄位後開始要求,然後取得所有標頭,再等到使用者按下「傳送」即可然後再傳送他們輸入的資料
  • 逐步傳送用戶端產生的資料,例如音訊、影片或輸入資料。
  • 透過 HTTP/2 或 HTTP/3 重新建立網路通訊端。

但由於這是低階網路平台功能,因此不受我的構想限制。 你可以想出更精彩的要求串流用途。

示範

藉此展示您如何將使用者資料串流到伺服器,並傳回可即時處理的資料。

好吧,這不是想像中最有想像力的例子,我只想簡單說明一下嗎?

總之,它是如何運作的?

先前介紹擷取串流的精彩冒險

目前所有新式瀏覽器都支援回應串流, 這些方法可讓您存取伺服器傳出的回應部分:

const response = await fetch(url);
const reader = response.body.getReader();

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

console.log('Response fully received');

每個 value 都是 Uint8Array 的位元組。 您取得的陣列數量和陣列大小取決於網路速度。 如果您的連線速度快,則會取得較少、較大的區塊規模。 如果連線速度緩慢,您會看到更多較小的區塊。

如要將位元組轉換為文字,您可以使用 TextDecoder;如果目標瀏覽器支援這項功能,則可使用新的轉換串流:

const response = await fetch(url);
const reader = response.body.pipeThrough(new TextDecoderStream()).getReader();

TextDecoderStream 是轉換串流,會擷取所有 Uint8Array 區塊,並將其轉換為字串。

串流非常適合,因為一旦收到資料,就可以開始採取行動。 舉例來說,要是您收到包含 100 個「結果」的清單,您可以在收到結果時立即顯示第一筆結果,不必等待所有結果都傳回 100 個。

總之,這就是回應串流,我想介紹的是「要求串流」。

串流要求主體

要求可能包含主體:

await fetch(url, {
  method: 'POST',
  body: requestBody,
});

以前,您需要讓整個主體準備就緒,才能開始要求,但在 Chromium 105 中,您可以提供自己的 ReadableStream 資料:

function wait(milliseconds) {
  return new Promise(resolve => setTimeout(resolve, milliseconds));
}

const stream = new ReadableStream({
  async start(controller) {
    await wait(1000);
    controller.enqueue('This ');
    await wait(1000);
    controller.enqueue('is ');
    await wait(1000);
    controller.enqueue('a ');
    await wait(1000);
    controller.enqueue('slow ');
    await wait(1000);
    controller.enqueue('request.');
    controller.close();
  },
}).pipeThrough(new TextEncoderStream());

fetch(url, {
  method: 'POST',
  headers: {'Content-Type': 'text/plain'},
  body: stream,
  duplex: 'half',
});

上述指令會將「這是緩慢的要求」,一次一個字詞,每個字詞之間有 1 秒暫停。

要求主體的每個區塊都必須是 Uint8Array 位元組,因此我使用 pipeThrough(new TextEncoderStream()) 進行轉換。

限制

串流要求是網路的全新功能,因此有一些限制:

還是半雙工?

如要允許要求使用串流,請將 duplex 要求選項設為 'half'

HTTP 有一項不太為人知的功能 (不過是否標準行為需視詢問者而定),亦即您在傳送要求時可以開始接收回應。 但是它鮮為人知,因此並未受到伺服器支援,而且沒有任何瀏覽器支援。

在瀏覽器中,即使伺服器較快傳送回應,在要求主體完成傳送前,回應都永遠不會出現。 對所有瀏覽器擷取作業而言都是如此。

預設模式稱為「半雙工」。 但部分實作 (例如 Deno 中的 fetch) 會預設為「full duplex」則代表在要求完成前可以使用回應。

因此,為解決這個相容性問題,瀏覽器需要 duplex: 'half' 指定為包含串流內文的要求。

日後,瀏覽器可能會支援對串流和非串流要求 duplex: 'full'

與此同時,雙工通訊的最佳做法是先使用串流要求進行擷取,然後再建立另一個擷取來接收串流回應。 伺服器必須設法為這兩個要求建立關聯,例如網址中的 ID。 這就是示範的運作方式。

受限制的重新導向

某些形式的 HTTP 重新導向需要瀏覽器重新傳送請求主體到其他網址。 為了支援這項功能,瀏覽器必須緩衝串流內容,導致中斷點無法播放,所以不會這麼做。

反之,如果要求具有串流主體,且回應是 303 以外的 HTTP 重新導向,則擷取作業將拒絕,系統也不會追蹤重新導向。

允許使用 303 重新導向,因為這類重新導向會將方法明確變更為 GET 並捨棄要求主體。

需要 CORS 並觸發預檢

串流要求具有主體,但沒有 Content-Length 標頭。 此為新的要求,因此必須使用 CORS,且這些要求一律會觸發預檢。

系統不允許串流 no-cors 要求。

無法在 HTTP/1.x 上運作

如果連線為 HTTP/1.x,擷取作業就會遭到拒絕。

這是因為根據 HTTP/1.1 規則,要求和回應主體必須傳送 Content-Length 標頭,因此另一端知道收到的資料量,或者將訊息格式變更為使用區塊編碼。使用區塊編碼時,內文會分為多個部分,每個部分都有各自的內容長度。

談到 HTTP/1.1 回應時,分塊編碼很常見,但要求的編碼非常少見,因此造成相容性風險過高。

潛在問題

這是一項新功能,目前網路已不敷使用。 以下是幾個需要留意的問題:

伺服器端不相容

某些應用程式伺服器不支援串流要求,而是等系統收到完整的要求後,才會讓您看到任何要求。對前述作業來說,這項做法就會失效。 請改用支援串流的應用程式伺服器,例如 NodeJSDeno

但你還沒離開星球! 應用程式伺服器 (例如 NodeJS) 通常位於另一個伺服器後方 (通常稱為「前端伺服器」),而該伺服器可能位於 CDN 後方。 如果任何這些事件決定先緩衝要求,再將要求傳送至鏈結中的下一個伺服器,那就會失去要求串流的好處。

無法控制的相容性

這項功能僅適用於 HTTPS,因此你無須擔心你與使用者之間的 Proxy,但使用者可能是在他們的電腦上執行 Proxy。 某些網際網路防護軟體會允許軟體監控瀏覽器與網路之間的所有情況,在某些情況下,這類軟體也會緩衝要求主體。

如要防範這種情況,可以建立「功能測試」與上方的示範類似,可在不關閉串流的情況下嘗試串流部分資料。 如果伺服器收到資料,就能透過不同的擷取方式回應。 發生這種情況時,您就會知道用戶端支援端對端串流要求。

特徵偵測

const supportsRequestStreams = (() => {
  let duplexAccessed = false;

  const hasContentType = new Request('', {
    body: new ReadableStream(),
    method: 'POST',
    get duplex() {
      duplexAccessed = true;
      return 'half';
    },
  }).headers.has('Content-Type');

  return duplexAccessed && !hasContentType;
})();

if (supportsRequestStreams) {
  // …
} else {
  // …
}

如果您想進一步瞭解這項功能的運作方式,請按照下列步驟操作:

如果瀏覽器不支援特定的 body 類型,就會對物件呼叫 toString(),並使用結果做為主體。 因此,如果瀏覽器不支援要求串流,要求主體就會成為 "[object ReadableStream]" 字串。 使用字串做為主體時,您可以輕鬆將 Content-Type 標頭設為 text/plain;charset=UTF-8。 因此,如果設定了該標頭,我們就會知道瀏覽器「不支援」要求物件的串流,並可以提早退出。

Safari「確實」支援要求物件中的串流,但「不」允許與 fetch 搭配使用,因此系統會測試 duplex 選項,因為 Safari 目前不支援該選項。

與可寫入串流搭配使用

有時只要使用 WritableStream,就能更輕鬆地使用訊息串。 您可以用「身分」是可讀取/寫入的組合,可接收傳遞至可寫入端的內容,並傳送至可讀取的結尾。 您可以建立一個不含引數的 TransformStream,藉此建立其中一項:

const {readable, writable} = new TransformStream();

const responsePromise = fetch(url, {
  method: 'POST',
  body: readable,
});

您傳送至可寫入串流的所有內容都會包含在要求中。 這可讓你同時編寫串流。 舉例來說,以下是一個傻瓜範例:系統會從某個網址擷取資料,經過壓縮後傳送至另一個網址:

// Get from url1:
const response = await fetch(url1);
const {readable, writable} = new TransformStream();

// Compress the data from url1:
response.body.pipeThrough(new CompressionStream('gzip')).pipeTo(writable);

// Post to url2:
await fetch(url2, {
  method: 'POST',
  body: readable,
});

上述範例使用了壓縮串流,以使用 gzip 壓縮任意資料。