تنفيذ الرفع المجزأ للملفات في JavaScript
نفّذ عمليات رفع ملفات مجزأة قابلة للاستئناف في JavaScript. تعامل مع الملفات الكبيرة وتتبع التقدم وتعافَ من انقطاعات الشبكة.
الرفع المجزأ في JavaScript يُقسِّم ملفاً كبيراً إلى أجزاء بحجم ثابت (عادةً 5-10 ميجابايت)، يرفع كلاً منها كطلب HTTP منفصل، ثم يُعيد تجميعها على الخادم. النمط يحل ثلاث مشاكل حقيقية: المتصفحات والوكلاء يُنهون الطلبات فوق 2 جيجابايت، شبكات الجوال تقطع الاتصال في منتصف الرفع، والمستخدمون يريدون ردود فعل على التقدم. التنفيذ الفعّال يستخدم File.slice() لقطع الأجزاء، وfetch مع AbortSignal لكل جزء، وتجميعاً من جانب الخادم عبر S3 multipart أو مُجمِّع مخصص، وفهرساً محلياً في IndexedDB حتى يصمد الاستئناف عبر إعادة تحميل التبويبات.
لماذا الأجزاء أفضل من الرفع في طلب واحد
ملف 4 جيجابايت مرفوع كطلب واحد يفشل لأسباب متوقعة: client_max_body_size الافتراضي في Nginx يبلغ 1 ميجابايت، Cloudflare يحدُّ رفعات الطبقة المجانية بـ 100 ميجابايت لكل طلب، AWS API Gateway يتوقف بقوة عند 10 ميجابايت، وSafari على الجوال يُنهي التبويبات التي تحتفظ بـ ArrayBuffer بحجم 4 جيجابايت في الذاكرة. الرفعات المجزأة تتجنب كل تلك الحدود. تحصل أيضاً على أشرطة تقدم تتحرك فعلاً، وإعادة محاولة لا تبدأ من الصفر، وقدرة على الإيقاف المؤقت والاستئناف. المقايضة هي حالة خادم أكبر وجولات أكثر — نحو طلب HTTP واحد لكل 5 ميجابايت، ما يعني 2,000 طلب لملف 10 جيجابايت.
اختيار حجم الجزء
حجم الجزء مقايضة بين الإنتاجية والمرونة. صغير جداً (أقل من 1 ميجابايت) وتقضي وقتاً أكثر في مصافحات TLS من البيانات. كبير جداً (فوق 100 ميجابايت) واتصال مقطوع يُضيع دقائق من الرفع. النقطة المثلى لمعظم الشبكات هي 5-10 ميجابايت، ما يتوافق مع الحد الأدنى لـ S3 multipart (5 ميجابايت) ويتناسب مع أحجام نافذة TCP النموذجية بعد البداية البطيئة.
قِس شبكة المستخدم أولاً:
const downlink = navigator.connection?.downlink ?? 10;
const chunkSize = downlink > 20 ? 10 * 1024 * 1024 : 5 * 1024 * 1024;
على اتصال 100 ميجابت، تنتهي أجزاء 10 ميجابايت في ثانية تقريباً. على 4G، أجزاء 5 ميجابايت تُعطي تعافياً أفضل حين تدخل نفقاً.
تقطيع الملف وتبصيمه
File.slice() تُعيد Blob يُشير إلى نفس البايتات الأساسية على القرص دون نسخ، لذا تقطيع ملف 20 جيجابايت لا يكلف شيئاً:
function* sliceFile(file, chunkSize) {
for (let offset = 0; offset < file.size; offset += chunkSize) {
yield {
index: Math.floor(offset / chunkSize),
blob: file.slice(offset, offset + chunkSize),
start: offset,
end: Math.min(offset + chunkSize, file.size)
};
}
}
احسب بصمة SHA-256 لكل جزء قبل الرفع حتى يتمكن الخادم من التحقق من السلامة:
const buffer = await chunk.blob.arrayBuffer();
const digest = await crypto.subtle.digest('SHA-256', buffer);
const hash = Array.from(new Uint8Array(digest))
.map(b => b.toString(16).padStart(2, '0')).join('');
لـ 10 جيجابايت من البيانات، التبصيم يُضيف ربما 20 ثانية على حاسوب حديث — يستحق للكشف عن التلف الصامت على روابط خلوية متذبذبة.
الرفع بتزامن محكوم
الرفعات المتسلسلة تُضيع النطاق الترددي؛ التوازي غير المحدود يُعطِّل المتصفح. حد تزامن 3-4 أجزاء نشطة يوازن بين الاثنين:
async function uploadAll(file, sessionId) {
const queue = [...sliceFile(file, 5 * 1024 * 1024)];
const workers = Array.from({ length: 4 }, async () => {
while (queue.length) {
const chunk = queue.shift();
await uploadChunk(chunk, sessionId);
emitProgress(chunk.index);
}
});
await Promise.all(workers);
}
كل استدعاء uploadChunk هو PUT /upload/:sessionId/:index مع البلوب (blob) كجسم والبصمة في ترويسة. استخدم AbortController لكل جزء حتى تتمكن من إلغاء طلبات فردية دون إنهاء الدفعة كلها.
إعادة المحاولة بدون إغراق الخادم
أخطاء الشبكة تحتاج توقفاً أسياً، لا حلقات إعادة محاولة مكثفة. سياسة معقولة: 3 محاولات، تأخير أساسي 500 مللي ثانية، اضطراب حتى 50%:
async function uploadChunk(chunk, sessionId, attempt = 0) {
try {
const res = await fetch(`/upload/${sessionId}/${chunk.index}`, {
method: 'PUT', body: chunk.blob, headers: { 'X-Hash': chunk.hash }
});
if (!res.ok) throw new Error(`HTTP ${res.status}`);
} catch (e) {
if (attempt >= 3) throw e;
const delay = 500 * 2 ** attempt + Math.random() * 250;
await new Promise(r => setTimeout(r, delay));
return uploadChunk(chunk, sessionId, attempt + 1);
}
}
عامِل ردود 5xx كقابلة لإعادة المحاولة، و4xx كمميتة (ما عدا 408 و429). على 429، احترم ترويسة Retry-After بدلاً من التوقف المحلي.
الاستئناف بعد إعادة تحميل التبويبة
احتفظ بحالة الرفع في IndexedDB بعد كل جزء ناجح:
await db.put('uploads', {
sessionId, fileName: file.name, fileSize: file.size,
completedChunks: [...completedSet], updatedAt: Date.now()
}, sessionId);
حين يُعيد المستخدم فتح الصفحة بنفس منتقي الملفات، قارِن size الملف وlastModified واسمه مع الجلسات المحفوظة. عند التطابق، اسأل الخادم عن الأجزاء التي استلمها بالفعل (طلب GET /upload/:sessionId/status بسيط يُعيد خريطة بت يعمل)، ثم ارفع الأجزاء الناقصة فقط. بروتوكول tus يُرسمِّن بالضبط هذا النمط بترويسة Upload-Offset، ومكتبة tus-js-client تُقدِّم تنفيذاً متيناً إذا لا تريد بناء نظامك الخاص.
تجميع الأجزاء على الخادم
خياران جديان: S3 multipart upload حيث يصبح كل جزء PartNumber وCompleteMultipartUpload النهائية تُلصِقها، أو مُجمِّع مخصص يكتب كل جزء في ملف مؤقت ويُسلسِلها في النهاية. S3 multipart أرخص على الحجم لأنك لا تدفع للخروج أثناء التجميع وR2 يُعطي قراءات بصفر خروج. النهج المخصص أسهل في التصحيح ويتيح التشفير عبر التدفق أثناء التجميع.
للأسلوب S3:
const upload = await s3.createMultipartUpload({ Bucket, Key });
// لكل جزء: s3.uploadPart({ UploadId, PartNumber, Body })
await s3.completeMultipartUpload({ UploadId, MultipartUpload: { Parts } });
انتبه لحد 10,000 جزء — للملفات فوق 50 جيجابايت تحتاج أجزاء 5 ميجابايت+ للبقاء تحته.
تتبع تقدم يثق به المستخدمون
أشرطة تقدم تقفز تبدو مكسورة. احسب التقدم كبايتات مرفوعة من إجمالي البايتات، لا كأجزاء مكتملة، وخفِّفه بمتوسط متحرك لثانيتين لإخفاء التذبذب. استخدم fetch مع ReadableStream وTransform لإحصاء البايتات، إذ XMLHttpRequest.upload.onprogress لا تُطلَق بشكل موثوق دائماً على HTTP/3. اعرض وقت الوصول بقسمة البايتات المتبقية على الإنتاجية الأخيرة، لكن حدِّد بـ 5 ثوانٍ كحد أدنى لتجنب تجربة "ثانيتان متبقيتان... لـ 10 دقائق".
تجنب الأخطاء الشائعة
ثلاثة أخطاء تُفسِد الرفعات المجزأة في الإنتاج: نسيان تعيين Content-Length لكل جزء (يُعطِّل بعض وكلاء الحافة)، وإعادة استخدام نفس معرِّف الجلسة لملفات مختلفة (يُفسِد التجميع)، والسماح للمستخدم بتغيير الملف في منتصف الرفع بدون إصدار الجلسة. دائماً بصِّم الـ 1 ميجابايت الأول من الملف بالإضافة إلى حجمه وlastModified لتعريف الجلسات. ولا تثق بـ lastModified وحدها — macOS Finder يُحدِّثها عند تغييرات البيانات الوصفية.
HexaTransfer يستخدم خط أنابيب مجزأ وقابل للاستئناف مثل هذا لرفعاته بـ 10 جيجابايت، مع إضافة AES-256-GCM من جانب العميل لكل جزء قبل PUT.
تجميع كل شيء
أداة رفع مجزأ للإنتاج هي ربما 300 سطر JavaScript: تقطيع بـ File.slice، تبصيم بـ SubtleCrypto، رفع 3-4 أجزاء بالتوازي مع توقف أسي، استمرار حالة الجلسة في IndexedDB، والسماح للخادم بتجميع الأجزاء عبر S3 multipart أو مُجمِّع مخصص. اختبره مع تبديلات وضع الطائرة وإعادة تحميل التبويبات وملف 15 جيجابايت على 4G قبل الثقة به. حين يعمل، إضافة التشفير والتقدم وقابلية الاستئناف هي طبقات إضافية على نفس الهيكل.
جرّبها على hexatransfer.com — مجاناً، بدون حساب، حتى 10 جيجابايت.
أرسل ملفات كبيرة بأمان مع تشفير من طرف إلى طرف
انقل ملفات حتى 10 جيجابايت مجاناً مع تشفير من طرف إلى طرف. لا حاجة لحساب. يتم تشفير ملفاتك في متصفحك قبل الرفع — لا أحد آخر يستطيع قراءتها.
إرسال ملف