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

بدءًا من الإصدار 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,
});

في السابق، كان عليك تجهيز النص الكامل قبل بدء الطلب، ولكن في الإصدار 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.