طلبات البث باستخدام واجهة برمجة تطبيقات الجلب

اعتبارًا من الإصدار 105 من Chromium، يمكنك بدء طلب قبل توفّر النص الكامل باستخدام واجهة برمجة التطبيقات 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 "نتيجة"، يمكنك عرض النتيجة الأولى فور تلقّيها بدلاً من الانتظار إلى أن تصلك كل النتائج المائة.

حسنًا، هذه هي أحداث البث المباشر التي أردّ الردّ عليها. أما الميزة الجديدة والمشوّقة التي أريد التحدّث عنها فهي أحداث البث المباشر بناءً على طلبات المشاهدين.

نصوص طلبات البث

يمكن أن تحتوي الطلبات على نصوص:

await fetch(url, {
  method: 'POST',
  body: requestBody,
});

في السابق، كان عليك تجهيز النص الكامل قبل بدء الطلب، ولكن في الإصدار 105 من Chromium، يمكنك تقديم 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',
});

سيؤدي الإجراء أعلاه إلى إرسال "هذا طلب بطيء" إلى الخادم، كلمة واحدة في كل مرة، مع التوقف لمدة ثانية بين كل كلمة.

يجب أن يكون كل جزء من محتوى الطلب Uint8Array بايت، لذلك أستخدم pipeThrough(new TextEncoderStream()) لإجراء عملية التحويل نيابةً عني.

القيود

طلبات البث هي ميزة جديدة على الويب، لذا فهي تخضع لبعض القيود:

هل الاتصال أحادي الاتجاه؟

للسماح باستخدام أحداث البث في طلب، يجب ضبط خيار طلب duplex على 'half'.

من الميزات غير المعروفة في HTTP (على الرغم من أنّ ما إذا كان هذا السلوك عاديًا يعتمد على الشخص الذي تسأله) أنّه يمكنك بدء تلقّي الاستجابة أثناء إرسال الطلب. ومع ذلك، لا يُعرف هذا الإجراء كثيرًا، لذا لا تتوافق معه الخوادم بشكل جيد، ولا يتوافق معه أي متصفّح.

في المتصفّحات، لا يصبح الردّ متاحًا أبدًا إلى أن يتم إرسال محتوى الطلب بالكامل، حتى إذا أرسل الخادم ردًا في وقت أقرب. وينطبق ذلك على جميع عمليات جلب المتصفّحات.

يُعرف هذا النمط التلقائي باسم "نصف الازدواج". ومع ذلك، فإنّ بعض عمليات التنفيذ، مثل fetch في Deno، تستخدم تلقائيًا وضع "الازدواج الكامل" لعمليات جلب البث، ما يعني أنّه يمكن أن يصبح الردّ متاحًا قبل اكتمال الطلب.

لذلك، لحلّ مشكلة التوافق هذه، يجب تحديد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 بشكلٍ ملائم. وبالتالي، إذا تم ضبط هذا العنوان، نعرف أنّ المتصفح لا يتيح أحداث البث في عناصر الطلبات، ويمكننا الخروج مبكرًا.

يتيح 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.