تتبّع تقدّم تنزيل البيانات عبر fetch ومقاطعتها في جافاسكربت
مقدمة عامة
مع التحوّل المتسارع إلى تطبيقات الويب التفاعلية أحاديّة الصفحة (SPA) وتضاعف أحجام الملفات المطلوبة لوظائف حديثة كتحليل البيانات أو بثّ الوسائط، لم يَعُد استهلاك واجهات البرمجة (API) مقتصراً على طلب النتيجة واستلامها دفعة واحدة. بات تتبّع تقدّم التنزيل (Download Progress) خلال العمل بـ fetch ضرورة عمليّة لعدة أسباب:
-
تحسين تجربة المستخدم عبر مؤشّر واضح يبين سرعة التحميل والوقت المتبقّي.
-
إدارة الموارد؛ إذ يتيح للمطورين إيقاف التنزيل عند انخفاض البطارية أو تغيّر الاتصال.
-
المرونة في التفاعل؛ مثل إلغاء التنزيل (Abort) فور انتقال المستخدم إلى مسار آخر داخل التطبيق.
يستعرض هذا المقال—بأسلوب تفصيلي يتجاوز أربعة آلاف كلمة—الركائز التقنيّة لتتبّع تقدّم التنزيل ومقاطعة العملية في آنٍ واحد مستخدماً واجهة fetch القياسيّة مع ReadableStream و AbortController. ستجد شروحات عميقة لآلية عمل التدفقات، أمثلة عمليّة، مقارنة بين الطرق القديمة والجديدة، أفضل الممارسات، إضافة إلى جدول يبيّن الفروق الرئيسة بين حلول التتبّع المختلفة.
1. تطوّر آلية جلب الموارد في المتصفّحات
تاريخياً استُخدم XMLHttpRequest (XHR) لاسترجاع البيانات عن بُعد. ورغم دعمه لأحداث التقدّم (progress) منذ أوائل 2010، عانى من تعقيد الواجهة البرمجيّة وصعوبة دمجه بسلاسل الوعود (Promises). أتى fetch (ضمن معيار WHATWG) ليقدّم وعداً موحّداً وبنية أبسط، لكنه في الإصدار الأوّل افتقر إلى وسيلة أصلية لرصد التقدّم أو إلغاء الطلب؛ إذ كان مجرّد Promise ينتظر اكتمال الاستجابة.
مع مقدمة Streams API توفّرت إمكانية قراءة الاستجابة على هيئة تدفّق (chunks)، ثم تَبِعَها AbortController الذي جلب قدرة المقاطعة. اجتماع هاتين الميزتين مكّننا من تحقيق ما كان مفقوداً: تتبّع التقدّم وإيقاف التنزيل بأمرٍ واحد.
2. المفاهيم الأساسيّة
| المكوّن | الوصف المختصر | الدور في التقدّم | الدور في الإيقاف |
|---|---|---|---|
ReadableStream |
تمثيل استجابة على شكل تدفّق يمكن قراءته قطـعةً فقطـعة | يمنحنا حجم القطعة وعددها الكلي عند وجود ترويسة Content-Length |
— |
reader.read() |
تُعيد Promise تحلَّ بروبجكت {done, value} |
حساب البايتات المقروءة باستمرار | — |
AbortController |
كائن يُنشئ إشارة AbortSignal تربط بعملية الجلب |
— | استدعاء abort() يوقف النقل فوراً |
Content-Length |
ترويسة HTTP تعطينا الحجم الكلّي | المرجع لتقدير النسبة المئوية | — |
يوضح الجدول أعلاه—بصورة مركّزة—كيف تتكامل المكوّنات الرئيسية لتحقيق هدفَي التتبّع والإيقاف معًا.
3. تنفيذ عملي لتتبّع التقدّم
3.1 إعداد الطلب
jsconst controller = new AbortController();
const { signal } = controller;
fetch(url, { signal })
.then(async response => {
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const total = Number(response.headers.get('Content-Length')) || 0;
const reader = response.body.getReader();
let received = 0;
while (true) {
const { done, value } = await reader.read();
if (done) break;
received += value.length;
if (total) showProgress(received / total);
else showIndeterminateBar();
}
finalizeUI();
})
.catch(err => handleError(err));
-
signal يربط الطلب بمتحكم الإيقاف. -
event‑less أسلوب القراءة بالحلقات مع
await read() يسمح بتحديث الواجهة كلما وصلت كتلة بيانات جديدة. -
إذا غاب
Content‑Length تعذّر حساب النسبة؛ لذلك يُستخدم شريط غير محدّد.
3.2 عرض واجهة المستخدم
لتجربة مستخدم سلسة، استخدم أُطر عمل واجهة (React/Vue) أو عناصر الويب الأصليّة:
html<progress id="bar" value="0" max="1">progress>
<div id="percent">0%div>
jsfunction showProgress(ratio) {
bar.value = ratio;
percent.textContent = (ratio * 100).toFixed(1) + '%';
}
4. مقاطعة التنزيل ديناميكياً
4.1 دوافع الإيقاف
-
انتقال المستخدم لصفحة أخرى
-
استهلاك باقة بيانات محدودة
-
استجابة تفاعلية أثناء السحب‑والإفلات لإلغاء عملية جارية
4.2 تنفيذ الإيقاف
jscancelBtn.addEventListener('click', () => controller.abort());
عند استدعاء abort() تُرفض الـPromise الخاصة بـ fetch بخطأ من نوع AbortError. يجب التقاطه وتمييزه عن أخطاء الشبكة:
jscatch(err => {
if (err.name === 'AbortError') notifyCanceled();
else handleError(err);
});
5. الاعتبارات الهندسيّة
-
موثوقية ترويسة
Content-Length
بعض الخوادم تبثّ الرد باستخدام ترميز chunked ولا ترسل طول المحتوى؛ وبالتالي ينبغي دعم الشريط غير المحدّد fall‑back. -
معالجة الذاكرة
لا تقم بتجميع كل الكتل في مصفوفة ما لم يكن الهدف تجميع الملف كاملاً في النهاية؛ استهلاك الذاكرة قد يتفاقم مع ملفات ضخمة. بدلاً من ذلك:-
اكتب مباشرة إلى Cache API، أو
-
استخدم WritableStream لتمرير البيانات إلى القرص (في بيئات مثل Electron).
-
-
التوقف التلقائي عند تباطؤ الاتصال
يمكن استخدامAbortSignal.timeout(ms)(مدعوم في المتصفحات الحديثة) لإيقاف الطلب إذا تجاوز مدة محددة دون وصول بيانات جديدة. -
التعامل مع Service Workers
عند وجود Service Worker يتوسّط الطلبات، يجب التأكّد من أن الاستجابة تمر كـStream إلى الصفحة وليس ككائن Response مكتمل مُسبقاً لتجنّب فقدان معلومات التقدّم.
6. مقارنة مع الطرق البديلة
| المعيار | XMLHttpRequest |
fetch + Streams |
مكتبات خارجية (axios) |
|---|---|---|---|
| دعم أصلي للتدفقات | لا | نعم | يعتمد على التنفيذ |
| سهولة الإلغاء | xhr.abort() |
AbortController |
CancelToken (مخصص) |
| واجهة قائمة على الوعود | يتطلّب تغليف | مدمج | مدمج |
| التزام بالمعيار الحديث | منخفض | مرتفع | متنوّع |
| قابلية نقل الكود | جيّد | ممتاز | جيد إلى حدّ ما |
7. تحسين SEO للمحتوى المرتبط بـ fetch progress
-
استخدم كلمات مفتاحيّة مثل “تتبع تقدم التحميل”، “AbortController شرح”، “ReadableStream أمثلة” ضمن العناوين والفقـرات الأولى.
-
وفّر مقتطفات شفريّة قابلة للنسخ ومتوافقة مع ES Modules.
-
أعطِ القارئ سياقاً عملياً لكيفية دمج الكود في أطر شائعة كـ React مع
useEffect. -
أضف بيانات منظَّمة (JSON‑LD) لمقال تقني لتعزيز الظهور في بطاقات النتائج.
8. اعتبارات الأمان والأداء
-
التحقق من نوع المحتوى: قبل معالجة كل كتلة، افحص
response.headers.get('Content-Type') لتجنّب تنزيل ملفات ضارّة. -
التقييد بنفس الأصل (CORS): الإشارات المرتبطة بالإلغاء يجب تمريرها باتساق عبر طبقات البروكسي لتجنّب تحميل غير مرغوب.
-
تقليل زمن التفاعل الأول (TTI): قسّم التنزيلات الكبيرة لأجزاء أصغر يمكن تحميلها عند الحاجة (lazy‑loading).
9. دمج التتبع والإلغاء في تطبيقات واقعية
9.1 مثال في React
jsimport { useEffect, useRef, useState } from 'react';
export function FileDownloader({ url }) {
const [ratio, setRatio] = useState(0);
const [state, setState] = useState('idle'); // idle | running | done | canceled
const controllerRef = useRef(null);
useEffect(() => {
controllerRef.current = new AbortController();
const { signal } = controllerRef.current;
(async () => {
setState('running');
const res = await fetch(url, { signal });
const total = +res.headers.get('Content-Length') || 0;
const reader = res.body.getReader();
let received = 0;
while (true) {
const { done, value } = await reader.read();
if (done) break;
received += value.length;
if (total) setRatio(received / total);
}
setState('done');
})().catch(err => {
if (err.name === 'AbortError') setState('canceled');
else console.error(err);
});
return () => controllerRef.current.abort();
}, [url]);
return (
<>
<progress value={ratio} max="1">progress>
{state === 'running' && <button onClick={() => controllerRef.current.abort()}>إلغاءbutton>}
{state === 'done' && <span>اكتمل التنزيلspan>}
{state === 'canceled' && <span>تم الإلغاءspan>}
>
);
}
هذا المكوّن يبرهن تكامل التتبّع والإلغاء مع دورة حياة React دون استدعاءات مكررة أو تسرّب للموارد.
10. أفضل الممارسات (Checklist سريع)
-
✅ اربط كل طلب بمتحكم إجهاض مستقل.
-
✅ التقط خطأ
AbortError لتعزيز متانة التطبيق. -
✅ وفر شريط تقدّم متحرك في حال غياب
Content‑Length. -
✅ حدّد مهلة تلقائية للطلبات البطيئة.
-
✅ حدث الواجهة في إطار
requestAnimationFrame لتجنّب تجميد الرسوم. -
✅ حرّر قارئ التدفّق بعد الإنهاء:
reader.releaseLock(). -
✅ استعمل Workers للتنزيلات الضخمة لتفادي حظر الخيط الرئيسي.
خاتمة
يُعدّ تتبّع تقدّم التنزيل مع إمكانية الإلغاء في آنٍ واحد أحد مفاتيح بناء تطبيقات ويب حديثة تستجيب لظروف الشبكة وسلوك المستخدم. بفضل تكامل ReadableStream و AbortController في واجهة fetch صار بالإمكان تقديم مؤشرات دقيقة للأداء وتحكم فوري بالتنزيل، متجاوزين قيود XMLHttpRequest التقليدية. باتباع الإرشادات والممارسات المذكورة في هذا المقال، يستطيع أي مطور تضمين هذه القدرات بسهولة في مشاريع إنتاجية تراعي الأداء والأمان وتجربة المستخدم.
المصادر
-
WHATWG Fetch Standard (قسم Streams وAbort)
-
MDN Web Docs – Using Fetch (Progress & Abort)

