پخش جریانی درخواست ها با fetch API، جریان درخواست ها با fetch 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" را هر بار یک کلمه و با یک ثانیه مکث بین هر کلمه به سرور ارسال می کند.

هر تکه از بدنه درخواست باید یک Uint8Array از بایت ها باشد، بنابراین من از pipeThrough(new TextEncoderStream()) برای انجام تبدیل برای خود استفاده می کنم.

محدودیت ها

درخواست‌های جریان یک قدرت جدید برای وب هستند، بنابراین با چند محدودیت همراه هستند:

نیمه دوبلکس؟

برای اجازه دادن به جریان‌ها برای استفاده در یک درخواست، گزینه درخواست duplex باید روی 'half' تنظیم شود.

یکی از ویژگی‌های کمتر شناخته شده HTTP (اگرچه، اینکه آیا این رفتار استاندارد است یا خیر بستگی به این دارد که از چه کسی سؤال می‌کنید) این است که می‌توانید در حالی که هنوز در حال ارسال درخواست هستید، پاسخ را دریافت کنید. با این حال، آنقدر کم شناخته شده است، که به خوبی توسط سرورها پشتیبانی نمی شود، و توسط هیچ مرورگری پشتیبانی نمی شود.

در مرورگرها، تا زمانی که بدنه درخواست به طور کامل ارسال نشود، پاسخ هرگز در دسترس قرار نمی گیرد، حتی اگر سرور زودتر پاسخی را ارسال کند. این برای همه واکشی های مرورگر صادق است.

این الگوی پیش‌فرض با نام «نیمه دوبلکس» شناخته می‌شود. با این حال، برخی از پیاده‌سازی‌ها، مانند fetch در Deno ، برای واکشی‌های جریانی به طور پیش‌فرض روی «full duplex» تنظیم شده‌اند، به این معنی که پاسخ می‌تواند قبل از تکمیل درخواست در دسترس باشد.

بنابراین، برای حل این مشکل سازگاری، در مرورگرها duplex: 'half' مشخص شود.

در آینده، duplex: 'full' ممکن است در مرورگرها برای درخواست‌های پخش جریانی و غیر جریانی پشتیبانی شود.

در این بین، بهترین کار بعدی برای ارتباط دوطرفه این است که یک واکشی با یک درخواست استریم انجام دهید، سپس واکشی دیگری برای دریافت پاسخ جریان ایجاد کنید. سرور به راهی برای مرتبط کردن این دو درخواست نیاز دارد، مانند یک شناسه در 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 تنظیم می کند. بنابراین، اگر آن هدر تنظیم شده باشد، می‌دانیم که مرورگر جریان‌ها را در اشیاء درخواست پشتیبانی نمی‌کند و می‌توانیم زودتر از آن خارج شویم.

سافاری جریان‌ها را در اشیاء درخواستی پشتیبانی می‌کند ، اما اجازه نمی‌دهد از آن‌ها با fetch استفاده شود، بنابراین گزینه duplex آزمایش می‌شود که سافاری در حال حاضر از آن پشتیبانی نمی‌کند.

استفاده با جریان های قابل نوشتن

وقتی 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 استفاده می کند.