從 Chromium 105 開始,您可以使用 Streams API,在取得完整主體之前啟動要求。
您可以使用這項功能來:
- 讓伺服器預熱。換句話說,您可以在使用者將焦點放在文字輸入欄位後,開始要求操作,並移除所有標頭,然後等到使用者按下「傳送」後,再傳送他們輸入的資料。
- 逐步傳送在用戶端產生的資料,例如音訊、視訊或輸入資料。
- 透過 HTTP/2 或 HTTP/3 重新建立網路 Socket。
但由於這是低階網路平台功能,請不要受限於我的想法。或許您可以想出更令人興奮的應用程式串流用途。
示範
這張圖顯示如何從使用者串流資料至伺服器,並傳回可即時處理的資料。
沒錯,這不是最有想像力的例子,我只是想讓它簡單一點,好嗎?
總之,這項功能的運作方式為何?
先前在「探索動態資料流」的驚奇旅程
Response 串流已在所有新式瀏覽器中提供一段時間。讓您在回應從伺服器傳送到應用程式時,存取回應的部分內容:
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,
});
先前,您必須先準備好整個 body,才能啟動要求,但在 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
,處理串流會更容易。您可以使用「identity」串流執行這項操作,這是可讀/可寫的組合,可將傳遞至可寫端的任何內容,傳送至可讀端。您可以建立 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 壓縮任意資料。