從 Chromium 105 開始,您可以使用 Streams API,在整個主體可用之前啟動要求。
你可以使用這項功能執行下列操作:
- 讓伺服器暖機。 換句話說,您可以在使用者將焦點放在文字輸入欄位時啟動要求,並清除所有標題,然後等待使用者按下「傳送」再傳送輸入的資料。
- 逐步傳送在用戶端產生的資料,例如音訊、視訊或輸入資料。
- 透過 HTTP/2 或 HTTP/3 重新建立 WebSocket。
但由於這是低階網路平台功能,請不要受我的想法限制。 或許您能想到更令人興奮的請求串流用途。
先前在精彩的擷取串流冒險中
回應串流已在所有新式瀏覽器中推出一段時間。 您可以在伺服器傳送回應時存取部分內容:
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 個「結果」的清單,可以立即顯示第一個結果,不必等待所有結果傳回。
總之,這就是回應串流,而我想要談論的令人興奮的新事物是要求串流。
串流要求主體
要求可以有主體:
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',
});
上述指令會將「This is a slow request」一次一個字傳送至伺服器,每個字之間會暫停一秒。
要求主體的每個區塊都必須是 Uint8Array
個位元組,因此我使用 pipeThrough(new TextEncoderStream())
進行轉換。
限制
串流要求是網路的新功能,因此有幾項限制:
半雙工?
如要在要求中使用串流,請將 duplex
要求選項設為 'half'
。
HTTP 有一項鮮為人知的功能 (不過,這是否為標準行為取決於您詢問的對象),就是您可以在傳送要求時開始接收回應。不過,由於這個標記鮮為人知,伺服器並不支援,任何瀏覽器也不支援。
在瀏覽器中,即使伺服器較早傳送回應,回應也一律會在要求主體完全傳送後才提供。 所有瀏覽器擷取作業都適用這項規定。
這個預設模式稱為「半雙工」。不過,部分實作項目 (例如 Deno 中的 fetch
) 預設會將串流擷取設為「全雙工」,也就是說,回應可能會在要求完成前提供。
因此,如要解決這個相容性問題,在瀏覽器中,必須在含有串流主體的請求中指定 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 回應而言,分塊編碼相當常見,但就要求而言則非常罕見,因此相容性風險過高。
潛在問題
這項功能是全新的,目前在網路上使用率不高。 請注意以下問題:
伺服器端不相容
部分應用程式伺服器不支援串流要求,而是會等待收到完整要求,才允許您查看任何內容,這有點違背了串流的意義。 請改用支援串流的應用程式伺服器,例如 NodeJS 或 Deno。
但你還沒脫離險境! 應用程式伺服器 (例如 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 壓縮任意資料。