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

جيك أرشيبالد
جيك أرشيبالد

بدءًا من الإصدار 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 "نتيجة"، يمكنك عرض النتيجة الأولى فور تلقيها، بدلاً من انتظار جميع النتائج البالغ عددها 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، خلف خادم آخر، يُعرف غالبًا باسم "خادم الواجهة الأمامية"، والذي قد يكون خلف شبكة توصيل للمحتوى. وإذا قرر أيٌ من هؤلاء المستخدمين تخزين الطلب مؤقتًا قبل تقديمه للخادم التالي في السلسلة، ستفقد ميزة بث الطلب.

عدم التوافق خارج سيطرتك

ونظرًا لأن هذه الميزة لا تعمل إلا من خلال 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.