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

من 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 "نتيجة"، يمكنك عرض أول نتيجة فور استلامها، بدلاً من انتظار النتائج المائة كلها.

على أي حال، هذه هي ساحات الردود، ولكن الموضوع الجديد المثير الذي أريد التحدُّث عنه هو طلبات البث.

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

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

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

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

يجب أن يكون كل جزء من نص الطلب هو 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.