คำขอสตรีมมิงที่มี API การดึงข้อมูล

ใน Chromium 105 คุณจะเริ่มคำขอได้ก่อนที่จะมีข้อมูลทั้งหมดพร้อมใช้งานโดยใช้ Streams API

คุณสามารถใช้สิ่งนี้เพื่อ:

  • ทำให้เซิร์ฟเวอร์อุ่นเครื่อง กล่าวคือ คุณสามารถเริ่มคำขอเมื่อผู้ใช้โฟกัสช่องป้อนข้อความ และนำส่วนหัวทั้งหมดออกไปได้ จากนั้นรอจนกว่าผู้ใช้กด "ส่ง" ก่อนส่งข้อมูลที่ป้อนลงไป
  • ค่อยๆ ส่งข้อมูลที่สร้างขึ้นในไคลเอ็นต์ เช่น ข้อมูลเสียง วิดีโอ หรือข้อมูลอินพุต
  • สร้าง Web Socket อีกครั้งผ่าน 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,
});

ก่อนหน้านี้คุณต้องพร้อมใช้งานส่วนต่างๆ ของร่างกายก่อนจึงจะเริ่มคำขอได้ แต่ตอนนี้คุณสามารถระบุข้อมูล ReadableStream ของคุณเองใน Chromium 105 ได้แล้ว

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',
});

คำขอข้างต้นจะส่ง "คำขอนี้ช้า" ไปยังเซิร์ฟเวอร์ทีละคำ โดยมีการหยุดชั่วคราวหนึ่งวินาทีระหว่างแต่ละคำ

แต่ละส่วนของเนื้อหาคำขอต้องเป็น Uint8Array ของไบต์ ฉันจึงใช้ pipeThrough(new TextEncoderStream()) เพื่อแปลงให้ฉัน

ข้อจำกัด

คำขอสตรีมมิงเป็นขุมพลังใหม่สำหรับเว็บ คำขอเหล่านี้จึงมีข้อจำกัดบางอย่าง ดังนี้

Half Duplex

หากต้องการอนุญาตให้ใช้สตรีมในคำขอ ต้องตั้งค่าตัวเลือกคำขอ duplex เป็น 'half'

คุณลักษณะที่ไม่ค่อยเป็นที่รู้จักของ HTTP คือ (แต่การทำงานมาตรฐานจะขึ้นอยู่กับผู้ที่คุณถาม) คือคุณสามารถเริ่มรับการตอบกลับในขณะที่ยังคงส่งคำขอได้ อย่างไรก็ตาม โดเมนไม่เป็นที่รู้จักมากนัก เซิร์ฟเวอร์ไม่รองรับเบราว์เซอร์ และทุกเบราว์เซอร์ก็ไม่รองรับเช่นกัน

การตอบกลับในเบราว์เซอร์จะใช้ไม่ได้จนกว่าจะมีการส่งคำขออย่างสมบูรณ์ แม้ว่าเซิร์ฟเวอร์จะส่งการตอบกลับเร็วกว่าก็ตาม กรณีนี้เกิดขึ้นกับการดึงข้อมูลเบราว์เซอร์ทั้งหมด

รูปแบบเริ่มต้นนี้เรียกว่า "half duplex" อย่างไรก็ตาม การติดตั้งใช้งานบางรายการ เช่น fetch ใน Deno จะมีค่าเริ่มต้นเป็น "full Duplex" สำหรับการดึงข้อมูลแบบสตรีมมิง หมายความว่าการตอบกลับจะพร้อมใช้งานก่อนที่คำขอจะเสร็จสมบูรณ์

ในการแก้ปัญหาความเข้ากันได้นี้ในเบราว์เซอร์ duplex: 'half' เพื่อระบุในคำขอที่มีเนื้อความของสตรีม

ในอนาคต เบราว์เซอร์อาจรองรับ duplex: 'full' สำหรับคำขอสตรีมมิงและไม่ใช่สตรีมมิง

ในระหว่างนี้ วิธีที่ดีที่สุดในการสื่อสารแบบ 2 ทางคือการดึงข้อมูล 1 ครั้งด้วยคำขอสตรีมมิง แล้วดึงข้อมูลอีกครั้งเพื่อรับการตอบกลับสตรีมมิง เซิร์ฟเวอร์ต้องมีวิธีบางอย่างในการเชื่อมโยงคำขอ 2 รายการนี้ เช่น รหัสใน URL นี่คือวิธีการทำงานของการสาธิต

การเปลี่ยนเส้นทางแบบจำกัด

การเปลี่ยนเส้นทาง HTTP บางรูปแบบกำหนดให้เบราว์เซอร์ส่งเนื้อหาของคำขอไปยัง URL อื่นอีกครั้ง เพื่อรองรับความต้องการนี้ เบราว์เซอร์จะต้องบัฟเฟอร์เนื้อหาของสตรีม ซึ่งมีลักษณะเช่นนี้ จึงไม่สามารถทำได้

หากคำขอมีเนื้อหาสตรีมมิงและการตอบกลับเป็นการเปลี่ยนเส้นทาง HTTP อื่นที่ไม่ใช่ 303 การดึงข้อมูลจะปฏิเสธและจะไม่ติดตามการเปลี่ยนเส้นทาง

อนุญาตให้มีการเปลี่ยนเส้นทาง 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 เท่านั้น คุณจึงไม่จำเป็นต้องกังวลเกี่ยวกับพร็อกซีระหว่างคุณกับผู้ใช้ แต่ผู้ใช้อาจเรียกใช้พร็อกซีในเครื่องของตน ซอฟต์แวร์ป้องกันอินเทอร์เน็ตบางตัวใช้เพื่อให้ตรวจสอบทุกสิ่งที่อยู่ระหว่างเบราว์เซอร์และเครือข่ายได้ และอาจมีบางกรณีที่ซอฟต์แวร์นี้เก็บบัฟเฟอร์คำขอ

หากคุณต้องการป้องกันปัญหานี้ คุณสามารถสร้าง "การทดสอบฟีเจอร์" คล้ายกับการสาธิตข้างต้นที่คุณพยายามสตรีมข้อมูลบางอย่างโดยไม่ปิดสตรีม หากเซิร์ฟเวอร์ได้รับข้อมูล ก็จะตอบกลับผ่านการดึงข้อมูลอื่นได้ จากนั้นไคลเอ็นต์จะรองรับคำขอสตรีมมิงจากต้นทางถึงปลายทาง

การตรวจหาฟีเจอร์

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,
});

แต่ในตอนนี้ ทุกสิ่งที่คุณส่งไปยังสตรีมที่เขียนได้จะเป็นส่วนหนึ่งของคำขอ ซึ่งจะช่วยให้คุณเขียนสตรีมร่วมกันได้ เช่น ตัวอย่างตลกๆ ที่ดึงข้อมูลจาก URL หนึ่ง บีบอัด และส่งไปยัง URL อื่น

// 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