تتيح الدالة fetch
تتبع عملية التنزيل download. لاحظ أنه لا توجد حاليًا طريقة تسمح للدالة fetch
بتتبع عملية الرفع upload، نستخدم لهذه الغاية الكائن XMLHttpRequest الذي سنغطيه لاحقًا.
تُستخدم الخاصية response.body
لتتبع تقدم التنزيل، وتمثل هذه الخاصية كائن ReadableStream
، وهو كائن خاص يزودنا عند وصوله بجسم الطلب كتلةً بكتلة chunk-by-chunk، ستجد وصفًا لمجاري التدفق القابلة للقراءة Readable streams في توصيف الواجهة Streams API، وتمنح الخاصية response.body
تحكمًا كاملًا بعملية القراءة على خلاف التابعين ()response.text
و()response.json
وغيرهما. كما تمنح إمكانية تقدير الوقت المستغرق في أية لحظة. إليك مثالُا عن شيفرة تقرأ الاستجابة من response.body
:
// والطرق الأخرى response.json() بدلا من const reader = response.body.getReader(); // حلقة لا نهائية حتي يكتمل التنزيل while(true) { // عند آخر جزء true القيمة done ستحمل //لبايتات كل جزء Unit8Array هو value const {done, value} = await reader.read(); if (done) { break; } console.log(`Received ${value.length} bytes`) }
ستكون نتيجة الاستدعاء ()await reader.read
كائنًا له الخاصيتان التاليتان:
-
done
: تأخذ القيمةtrue
عندم اكتمال عملية القراءة، وإلا فستكون قيمتهاfalse
. -
value
: مصفوفة من النوعUint8Array
.
اقتباستصف الواجهة البرمجية المرور iteration غير المتزامن الذي يمكن تطبيقه على
ReadableStream
باستخدام الحلقةfor await..of
، لكنه لا يُدعم بشكل واسع (راجع browser issues)، لذلك نستخدم الحلقةwhile
، حيث سنحصل على أجزاء متتالية من الاستجابة عبرها حتى انتهاء التحميل، أي حتى تحمل الخاصيةdone
القيمةtrue
، وللحصول على سجل العملية سنحتاج إلى كل جزءvalue
استقبلناه، وذلك لإضافة حجمه إلى عداد الحلقة، إليك المثال الكامل الذي يحصل على الاستجابة، ويصل إلى سجلات تقدم عملية التنزيل عبر الطرفية:
//واحصل على قارئ للبيانات Fetch الخطوة1: إبدأ تنفيذ الدالة let response = await fetch('https://api.github.com/repos/javascript-tutorial/en.javascript.info/commits?per_page=100'); const reader = response.body.getReader(); // الخطوة2: احصل على الحجم الكلي const contentLength = +response.headers.get('Content-Length'); // الخطوة3 : إقرأ البيانات let receivedLength = 0; // حجم البايتات المستقبلة حتى اللحظة let chunks = []; // مصفوفة الأجزاء المستلمة التي تمثل جسم الاستجابة while(true) { const {done, value} = await reader.read(); if (done) { break; } chunks.push(value); receivedLength += value.length; console.log(`Received ${receivedLength} of ${contentLength}`) } // الخطوة 4: ضم الأجزاء في مصفوفة واحدة let chunksAll = new Uint8Array(receivedLength); // (4.1) let position = 0; for(let chunk of chunks) { chunksAll.set(chunk, position); // (4.2) position += chunk.length; } // الخطوة5: الترميز في سلسلة نصية let result = new TextDecoder("utf-8").decode(chunksAll); // النهاية let commits = JSON.parse(result); alert(commits[0].author.login);
المخرجات:
"Received 258566 of 0" "Received 444982 of 0"
لنشرح الشيفرة السابقة:
-
لقد نفّذنا الدالة
ftech
، لكننا استخلصنا مجرى التدفق()reader response.body.getReader
بدلًا من استدعاء التابع()response.json
، ولايمكن استخدام الطريقتين معًا لقراءة الاستجابة، استخدم إحداهما للحصول على النتيجة. -
يمكننا قبل الشروع في قراءة الاستجابة تحديد الحجم الكلي لها عن طريق الترويسة
Content-Length
، وقد لا تكون الترويسة موجودةً في الطلبات ذات الأصل المختلط Cross-origin، لكن لن يُعدَّها الخادم عمليًا، وستبقى في مكانها. -
نستدعي التابع
()await reader.read
حتى ينهي عمله، ونُجمِّع أجزاء الاستجابة في المصفوفةchunks
، وهذا الأمر ضروري لأن الاستجابة ستختفي ولن نتمكن من إعادة قراءتها باستخدام()response.json
ولا بأي طريقة أخرى، وستحصل على خطأ إذا حاولت ذلك. -
سنحصل في النهاية على
chunks
وهي مصفوفة من الأجزاء لها النوعUint8Array
، وعلينا تجميعها ضمن نتيجة واحدة، ولسوء الحظ لا يوجد تابع لضمها، لهذا علينا كتابة الشيفرة التي ستنجز العملية: -
إنشاء المصفوفة
(chunksAll = new Uint8Array(receivedLength،
وهي مصفوفة من النوعUint8Array
لها حجم جميع الأجزاء. -
استخدام التابع
(set(chunk, position.
لنسخ كل جزء بدوره إليها. -
سنحصل على النتيجة ضمن المصفوفة
chunksAll
، وهي مصفوفة من البايتات وليست نصًا، ولتحويلها إلى نص لا بد من تفسير هذه البيانات عن طريق الكائن TextDecoder، ثم استخدامJSON.parse
إن استدعت الحاجة.
لكن ماذا لو احتجنا إلى محتوىً ثنائي بدل النص؟ سيكون الأمر أبسط، علينا فقط استبدال الخطوتين 4 و5 بسطر وحيد يُنشئ كائن بيانات ثنائية Blob
يضم كل الأجزاء.
let blob = new Blob(chunks);
وهكذا سنحصل على النتيجة بالصيغة التي نريد مع إمكانية تتبع تقدم العملية، مرةً أخرى يجب الانتباه إلى أن العملية غير صالحة لتتبع تقدم عملية الرفع، أي لا يمكن استخدام Fetch
، بل فقط للتنزيل.
ولا بد من التحقق من حجم البيانات الواصلة receivedLength
في كل لحظة ضمن الحلقة وإنهائها بمجرد وصولها إلى حد معين، إذا لم يكن حجم البيانات التي سنستقبلها معروفًا، وبالتالي لن تستهلك المصفوفة chunks
الذاكرة.
الكائن AbortController: مقاطعة العمليات غير المتزامنة
تعيد fetch
كما نعرف وعدًا promise، ولكننا نعلم أنّ JavaScript لا تقبل إلغاء الوعود عمومًا، فكيف سنلغي عملية fetch
أثناء تنفيذها؟
هنالك كائن خاص مدمج لهذا الغرض هو AbortController
، يمكن استخدامه لإلغاء fetch
وغيرها من المهام غير المتزامنة، ويطبّق مباشرةً.
لننشئ متحكمًا بالشكل التالي:
let controller = new AbortController();
والمتحكم هو كائن شديد البساطة، له:
-
تابع وحيد هو
()abort
. -
وخاصية واحدة هي
signal
تسمح بإعداد مستمع حدث event listener له.
أما عند استدعاء التابع ()abort
فسيحدث الآتي:
-
تحرّض الخاصية
controller.signal
وقوع الحدثabort
. -
تأخذ الخاصية
controller.signal.aborted
القيمةtrue
.
تكون للعملية في العادة مرحلتان:
-
المرحلة التي تُنفِّذ عمليةً قابلة للإلغاء، وتهيئ مستمع حدث للخاصية
controller.signal
. -
المرحلة التي تلغي: وتُنفَّذ باستدعاء التابع
()controller.abort
عندما يتطلب الأمر.
إليك مثالًا كاملًا دون Fetch
:
let controller = new AbortController(); let signal = controller.signal; // القيام بعملية قابلة للإلغاء // "signal" الحصول على الكائن // controller.abort() ضبط إطلاق المستمع عند استدعاء signal.addEventListener('abort', () => alert("abort!")); // القيام بالإلغاء controller.abort(); // abort! // true القيمة signal.aborted إطلاق الحدث ويصبح لــ alert(signal.aborted); // true
لاحظ أنّ الكائن AbortController
هو مجرد أداة لتمرير الحدث abort
عند استدعاء التابع ()abort
، ومن الواضح أنه بالإمكان تنفيذ مستمع حدث كهذا باستخدام شيفرتنا الخاصة دون الحاجة إلى AbortController
، لكن أهميته ستظهر عندما نعلم أن fetch
تعرف تمامًا كيفية التعامل معه، فهو متكامل معها.
مقاطعة العملية Fetch
لإلغاء العملية fetch
علينا تمرير قيمة الخاصية signal
العائدة للكائن AbortController
مثل خيار لها:
let controller = new AbortController(); fetch(url, { signal: controller.signal });
تعلم fetch
تمامًا كيفية التعامل مع AbortController
، وستستمع إلى الحدث abort
الذي تحرّض الخاصية signal
وقوعه.
لإلغاء العملية سنستدعي التابع:
controller.abort();
وهكذا تلغى العملية، حيث تحصل fetch
على الحدث abort
من الخاصية signal
وتلغي الطلب، عند إلغاء fetch
سيُرفض الوعد الذي تعيده وسيُرمى الخطأ AbortError
، وينبغي التعامل معه من خلال حلقة try..catch
مثلًا.
إليك مثالًا كاملًا مع استخدام fetch
، حيث تلغى العملية بعد ثانية واحدة:
// الإلغاء خلال ثانية واحدة let controller = new AbortController(); setTimeout(() => controller.abort(), 1000); try { let response = await fetch('/article/fetch-abort/demo/hang', { signal: controller.signal }); } catch(err) { if (err.name == 'AbortError') { // handle abort() alert("Aborted!"); } else { throw err; } }
كائن قابل للتوسع
يسمح الكائنAbortController
بإلغاء عدة عمليات معًا، إليك الشيفرة التمثيلية التالية التي تحضر عدة موارد على التوازي، ثم تستخدم كائن متحكم وحيدًا لإلغائها جميعًا:
let urls = [...]; //قائمة بالموارد التي ينبغي إحضارها let controller = new AbortController(); // fetch مصفوفة من الوعود التي ستعيدها عمليات let fetchJobs = urls.map(url => fetch(url, { signal: controller.signal })); let results = await Promise.all(fetchJobs); // بمجرد استدعاء حدث الإلغاء ستلغى جميع عمليات الإحضار
وسنتمكن أيضًا من إلغاء أي عمليات أخرى غير متزامنة مع عمليات fetch
باستخدام كائن AbortController
وحيد، بمجرد الاستماع إلى الحدث abort
:
let urls = [...]; let controller = new AbortController(); let ourJob = new Promise((resolve, reject) => { // المهمة المطلوب إلغاءها ... controller.signal.addEventListener('abort', reject); }); let fetchJobs = urls.map(url => fetch(url, { // عمليات الإحضار signal: controller.signal })); // انتظار إنجاز جميع العمليات let results = await Promise.all([...fetchJobs, ourJob]); // بمجرد استدعاء حدث الإلغاء ستلغى جميع عمليات الإحضار // بالإضافة إلى بقية المهام
خلاصة
بهذا نكون قد تعرفنا على كيفية تتبع عملية التنزيل باستخدام Fetch، وذلك بالاعتماد على عدة خاصيات، كما تعرفنا على كيفية مقاطعة العملية Fetch، وذلك بالاعتماد على الكائنات الآتية:
-
AbortController
: هو كائن بسيط يولّد الحدثabort
على الخاصيةsignal
عند استدعاء التابع()abort
، الذي يعطي الخاصيةsignal.aborted
القيمة "true" أيضًا. -
تتكامل
fetch
مع هذا الكائن، حيث تُمرر الخاصيةsignal
كخيار لتستمع إليه، وبالتالي يصبح إلغاؤها ممكنًا. -
يمكن استخدام
AbortController
في شيفرتنا، حيث يستمع التابع()abort
إلى الحدثabort
بعملية بسيطة تطبق في أي مكان، كما يمكن استخدامها دون استخدامfetch
.
ترجمة -وبتصرف- للفصلين Fetch: Download Progress وFetch: Abort من سلسلة The Modern JavaScript Tutorial.
أفضل التعليقات
لا توجد أية تعليقات بعد
انضم إلى النقاش
يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.