انتقل إلى المحتوى
HexaTransfer
العودة إلى المدونة
تعمق تقني

الرفع التدريجي للملفات: دليل UX وتقني

ابنِ تجارب رفع ملفات تدريجية بالسحب والإفلات وأشرطة التقدم والتعامل السلس مع الأخطاء لتجربة مستخدم أفضل.

الرفع التدريجي للملفات يمنح المستخدمين ردود فعل فورية وموثوقة في كل مرحلة: في لحظة إفلات الملف، تأكيد اختيار؛ أثناء الرفع، شريط تقدم سلس مع وقت وصول واقعي؛ عند الفشل، خيارات إعادة محاولة محددة؛ بعد الاكتمال، حالة نجاح واضحة مع الخطوات التالية. المكونات التقنية هي: السحب والإفلات عبر HTML5 DataTransfer API، ورفع مجزأ قابل للاستئناف، وتدفق fetch مع ReadableStream لتتبع التقدم البايتي الدقيق، وإدارة الحالة التي تصمد عبر إعادة تحميل التبويبات عبر IndexedDB. حين يُنفَّذ بشكل صحيح، مستخدم يرفع 5 جيجابايت لا يتساءل أبداً إذا كان التطبيق مجمَّداً.

ما معنى "التدريجي" هنا

للتدريجي معنيان في هذا السياق. الأول: التحسين التدريجي، حتى يعمل الرفع كـ <input type="file"> POST عادي على متصفح من 2012، ويكتسب السحب والإفلات والتجزئة وإعادة المحاولة حين يتوفر JavaScript. الثاني: الكشف التدريجي، حيث تكشف الواجهة التعقيد عند الحاجة فقط — اعرض نسبة مئوية أثناء الرفع، لكن اعرض تفاصيل إعادة المحاولة عند الفشل فقط. المعنيان يُشيران إلى المبدأ ذاته: يجب ألا يصطدم المستخدم بطريق مسدود، ولا ينتظر بلا معلومات.

نمط الفشل الواجب التجنب هو "دوّامة الغموض" — رسوم متحركة تحميل عامة لا تُعطي مؤشراً على التقدم أو وقت الوصول أو حدوث خطأ. المستخدمون يلغون الرفع الذي لا يثقون به.

السحب والإفلات الذي لا يتصارع مع المتصفح

واجهة السحب والإفلات في HTML5 مشهورة بالأخطاء. بعض القواعد التي تجعلها مقبولة:

const dropzone = document.querySelector('.dropzone');
dropzone.addEventListener('dragover', (e) => {
  e.preventDefault();
  dropzone.classList.add('dragging');
});
dropzone.addEventListener('dragleave', () => {
  dropzone.classList.remove('dragging');
});
dropzone.addEventListener('drop', (e) => {
  e.preventDefault();
  dropzone.classList.remove('dragging');
  handleFiles([...e.dataTransfer.files]);
});

استدعِ preventDefault على dragover أو لن يقبل هدف الإفلات الملف. استخدم e.dataTransfer.items بدلاً من files إذا كنت بحاجة لقبول مجلدات عبر webkitGetAsEntry() — الطريقة الوحيدة للتقاط محتويات المجلدات بشكل تكراري على Chrome وFirefox.

اجعل البديل قابلاً للاستخدام أيضاً: <label> مرئي يُغلِّف <input type="file" multiple> منسَّق يعمل لـ 100% من المستخدمين بما في ذلك لوحة المفاتيح وقارئات الشاشة.

عرض تقدم يُصدِّقه المستخدمون

أشرطة التقدم تقفز لثلاثة أسباب: أحجام أجزاء غير متساوية، وبدء TCP البطيء، والتخزين المؤقت في طبقة الشبكة. خفِّفها بمتوسط متتالٍ لثانيتين:

const samples = []; // [{ time, bytes }]
function recordSample(bytes) {
  const now = performance.now();
  samples.push({ time: now, bytes });
  while (samples.length > 1 && now - samples[0].time > 2000) samples.shift();
}
function throughput() {
  if (samples.length < 2) return 0;
  const delta = samples[samples.length - 1];
  const base = samples[0];
  return (delta.bytes - base.bytes) / ((delta.time - base.time) / 1000);
}

احسب وقت الوصول كـ (totalBytes - uploadedBytes) / throughput()، حدِّد العرض بحد أدنى 5 ثوانٍ، وصِّغه بمصطلحات إنسانية: "نحو دقيقتين" لا "124.3 ثانية." اعرض كلاً من النسبة المئوية وعداد البايت ("340 ميجابايت من 2.1 جيجابايت") — المستخدمون يقارنون الاثنين حين يشعرون بشيء غير صحيح.

حالات الخطأ مع استرداد قابل للتنفيذ

رسائل "فشل الرفع" العامة تُدمِّر الثقة. صنِّف الأخطاء في خمسة أقسام وأظهِر كل منها بشكل مميز:

  • انقطاع الشبكة (حدث عدم اتصال، إعادة ضبط TCP): "جار إعادة الاتصال..." مع إعادة محاولة تلقائية
  • خطأ خادم 5xx: "خطأ في الخادم، إعادة المحاولة خلال 5 ثوانٍ" مع زر إعادة محاولة يدوي
  • خطأ خادم 4xx (413 كبير جداً، 415 نوع خاطئ): "الملف مرفوض: كبير جداً" مع استبدال الملف
  • انتهاء صلاحية المصادقة (401، 403): "انتهت الجلسة، سجِّل الدخول للمتابعة"
  • انهيار العميل (خطأ JS، المتصفح أغلق التبويبة): الاسترداد من IndexedDB عند إعادة التحميل

قارِن الرسالة بالإجراء الواحد الذي يحلها. إذا كان المستخدم غير متصل، اعرض حالة الاتصال/عدم الاتصال مُراقَبة عبر navigator.onLine وحدث online.

تتبع البايتات بتدفقات Fetch

XMLHttpRequest.upload.onprogress كانت الطريقة التقليدية لتتبع تقدم الرفع، لكنها غير موثوقة على HTTP/3 وتُفوِّت البايتات المُخزَّنة في مخزن الإرسال. النهج الحديث يستخدم ReadableStream لإحصاء البايتات أثناء إنتاجها:

function trackedStream(blob, onBytes) {
  let sent = 0;
  return new ReadableStream({
    async pull(controller) {
      const reader = blob.stream().getReader();
      while (true) {
        const { done, value } = await reader.read();
        if (done) { controller.close(); return; }
        sent += value.byteLength;
        onBytes(sent);
        controller.enqueue(value);
      }
    }
  });
}

مرِّر التدفق كـ body لـ fetch مع duplex: 'half'. دعم Safari لتدفقات الطلبات جاء في 17.4؛ قبل ذلك، ارجع إلى XMLHttpRequest. هذا يمنحك تقدماً دقيقاً بالميلي ثانية مرتبطاً بالبايتات الفعلية المُسلَّمة لطبقة الشبكة.

الإيقاف المؤقت والاستئناف والإلغاء

المستخدمون يتوقعون زر إيقاف مؤقت لأي شيء يستغرق أكثر من دقيقة. مع الرفع المجزأ، الإيقاف المؤقت هو "توقف عن إرسال أجزاء جديدة"، والاستئناف يلتقط من حيث توقفت قائمة العمل. الإلغاء يستخدم AbortController:

const ctrl = new AbortController();
cancelButton.onclick = () => ctrl.abort();
await fetch(url, { method: 'PUT', body: blob, signal: ctrl.signal });

عند الإجهاض، نظِّف: احذف جلسة الرفع على الخادم حتى لا يتسرب التخزين، امسح إدخال IndexedDB، عُد إلى الحالة الأولية. الإيقاف المؤقت يجب أن يحتفظ بالحالة؛ الإلغاء يجب أن يُدمِّرها. اجعل التمييز مرئياً في الواجهة.

الصمود عبر إعادة تحميل التبويبة

احتفظ بحالة الرفع بعد كل جزء ناجح:

await idb.put('uploads', {
  sessionId, fileFingerprint, fileName, fileSize,
  completedChunks: [...done], updatedAt: Date.now()
}, sessionId);

البصمة هي SHA-256 لأول ميجابايت من الملف بالإضافة إلى الحجم وlastModified — كافية لإعادة التعرف على الملف حين يختاره المستخدم مرة أخرى بعد إعادة التحميل. عند تحميل الصفحة، تحقق من IndexedDB للجلسات الأقل من ساعة واعرض الاستئناف: "لديك رفع قيد التقدم من 12 دقيقة. هل تستأنف؟" لا تستأنف تلقائياً بدون موافقة — المستخدمون أحياناً يُعيدون التحميل خصيصاً للإلغاء.

تفاعلات متاحة وصديقة للوحة المفاتيح

منطقة إفلات تستجيب للسحب فقط تُقصي مستخدمي قارئات الشاشة ولوحة المفاتيح. أضِف:

  • role="button" وtabindex="0" على منطقة الإفلات
  • معالج مفتاح Enter/Space الذي ينقر على إدخال الملف
  • aria-live="polite" على منطقة التقدم حتى تُعلِن قارئات الشاشة المعالم
  • أنماط تركيز مرئية، لا مجرد تحوم
  • تصنيفات واضحة — "رفع ملف" أفضل من "تصفح" وكلاهما أفضل من أيقونة مجردة

اختبار لوحة المفاتيح سريع: افصل الماوس 10 دقائق وحاول إكمال رفع. إذا لم تستطِع، لن يستطيع جزء من مستخدميك أيضاً.

مُرفِّع HexaTransfer يستخدم هذا النمط التدريجي بالضبط — احتياطي النموذج العادي، وتحسين السحب والإفلات، وتدفق fetch، والاستئناف المدعوم بـ IndexedDB، والاسترداد المحدد من الأخطاء.

التفاصيل التي يلاحظها المستخدمون فعلاً

الصقل الذي يُفرِّق بين رافعات لا تُنسى ورائعة يعيش في اللحظات الصغيرة: رسوم متحركة عند الإفلات تؤكد استلام الملف، وشريط تقدم يمتلئ بانسياب لا بقفزات، ووقت وصول يصبح أكثر دقة مع الوقت لا يقفز بشكل مجنون، ورسائل خطأ محددة تُخبرك بما تفعله لاحقاً، ونافذة استئناف بعد تحديث غير مقصود، وحالة اكتمال تستمر طويلاً بما يكفي لنسخ رابط المشاركة، وسلوك إلغاء يوقف الرفع فوراً. كل هذه الأشياء بضعة أسطر برمجية. أطلِقها جميعاً ورافعتك ستبدو أفضل بمراحل من معالجة <input type="file"> الافتراضية.

جرّبها على hexatransfer.com — مجاناً، بدون حساب، حتى 10 جيجابايت.

أرسل ملفات كبيرة بأمان مع تشفير من طرف إلى طرف

انقل ملفات حتى 10 جيجابايت مجاناً مع تشفير من طرف إلى طرف. لا حاجة لحساب. يتم تشفير ملفاتك في متصفحك قبل الرفع — لا أحد آخر يستطيع قراءتها.

إرسال ملف