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

ตั้งแต่ 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',
});

คำสั่งข้างต้นจะส่ง "This is a slow request" ไปยังเซิร์ฟเวอร์ทีละคำ โดยเว้นวรรค 1 วินาทีระหว่างแต่ละคำ

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

ข้อจำกัด

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

Half duplex

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

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

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

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

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

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

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

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

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

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

อนุญาตให้ใช้การเปลี่ยนเส้นทาง 303 เนื่องจากจะเปลี่ยนวิธีการเป็น GET อย่างชัดเจนและทิ้งเนื้อหาของคำขอ

ต้องใช้ CORS และทริกเกอร์คำขอ Preflight

คำขอสตรีมมีเนื้อหา แต่ไม่มีส่วนหัว Content-Length เนื่องจากเป็นคำขอประเภทใหม่ จึงต้องใช้ CORS และคำขอเหล่านี้จะทริกเกอร์คำขอก่อนส่งเสมอ

ไม่อนุญาตให้ใช้คำขอสตรีมมิง no-cors

ใช้กับ HTTP/1.x ไม่ได้

ระบบจะปฏิเสธการดึงข้อมูลหากการเชื่อมต่อเป็น HTTP/1.x

เนื่องจากตามกฎของ HTTP/1.1 เนื้อหาของคำขอและการตอบกลับต้องส่งส่วนหัว Content-Length เพื่อให้ฝั่งตรงข้ามทราบปริมาณข้อมูลที่จะได้รับ หรือเปลี่ยนรูปแบบของข้อความเพื่อใช้การเข้ารหัสแบบเป็นกลุ่ม การเข้ารหัสแบบเป็นกลุ่มจะแบ่งเนื้อหาออกเป็นส่วนๆ โดยแต่ละส่วนจะมี 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