كيفية إنشاء برنامج تحميل ملفات السحب والإفلات باستخدام Vanilla JavaScript

نشرت: 2022-03-10
ملخص سريع ↬ في هذه المقالة ، سنستخدم "vanilla" ES2015 + JavaScript (لا توجد أطر عمل أو مكتبات) لإكمال هذا المشروع ، ومن المفترض أن لديك معرفة عملية بجافا سكريبت في المتصفح. يجب أن يكون هذا المثال متوافقًا مع كل متصفح دائم الخضرة بالإضافة إلى IE 10 و 11.

من الحقائق المعروفة أن مدخلات اختيار الملفات يصعب تحديد نمطها بالطريقة التي يريدها المطورون ، لذلك يخفيها الكثيرون وينشئون زرًا يفتح مربع حوار اختيار الملف بدلاً من ذلك. في الوقت الحاضر ، على الرغم من ذلك ، لدينا طريقة أكثر دقة للتعامل مع اختيار الملف: السحب والإفلات.

من الناحية الفنية ، كان هذا ممكنًا بالفعل لأن معظم (إن لم يكن كل ) تطبيقات إدخال تحديد الملف سمحت لك بسحب الملفات فوقها لتحديدها ، لكن هذا يتطلب منك إظهار عنصر file بالفعل. لذلك ، دعونا نستخدم واجهات برمجة التطبيقات التي قدمها لنا المتصفح لتنفيذ محدد ملفات السحب والإفلات والتحميل.

في هذه المقالة ، سنستخدم "vanilla" ES2015 + JavaScript (لا توجد أطر عمل أو مكتبات) لإكمال هذا المشروع ، ومن المفترض أن لديك معرفة عملية بجافا سكريبت في المتصفح. هذا المثال - بصرف النظر عن تركيب ES2015 + ، والذي يمكن تغييره بسهولة إلى بناء جملة ES5 أو نقله بواسطة Babel - يجب أن يكون متوافقًا مع كل متصفح دائم الخضرة بالإضافة إلى IE 10 و 11.

إليك نظرة سريعة على ما ستقوم به:

أداة تحميل الصور بالسحب والإفلات أثناء العمل
عرض توضيحي لصفحة ويب يمكنك من خلاله تحميل الصور عن طريق السحب والإفلات ، ومعاينة الصور التي يتم تحميلها على الفور ، وشاهد تقدم التحميل في شريط التقدم.

أحداث السحب والإفلات

أول شيء نحتاج إلى مناقشته هو الأحداث المتعلقة بالسحب والإفلات لأنها القوة الدافعة وراء هذه الميزة. بشكل عام ، هناك ثمانية أحداث يطلقها المتصفح تتعلق بالسحب والإفلات: drag ، dragend ، dragenter ، dragexit ، السحب ، dragleave ، dragover ، dragstart ، drop . لن نتجاوزها جميعًا لأن عمليات drag ، dragend ، dragexit ، dragstart ، كلها تعمل على العنصر الذي يتم سحبه ، وفي حالتنا ، سنقوم بسحب الملفات من نظام الملفات لدينا بدلاً من عناصر DOM ، لذلك لن تظهر هذه الأحداث مطلقًا.

إذا كنت مهتمًا بها ، فيمكنك قراءة بعض الوثائق حول هذه الأحداث على MDN.

المزيد بعد القفز! أكمل القراءة أدناه ↓

كما قد تتوقع ، يمكنك تسجيل معالجات الأحداث لهذه الأحداث بنفس الطريقة التي تسجل بها معالجات الأحداث لمعظم أحداث المستعرض: عبر addEventListener .

 let dropArea = document.getElementById('drop-area') dropArea.addEventListener('dragenter', handlerFunction, false) dropArea.addEventListener('dragleave', handlerFunction, false) dropArea.addEventListener('dragover', handlerFunction, false) dropArea.addEventListener('drop', handlerFunction, false)

فيما يلي جدول صغير يصف ما تفعله هذه الأحداث ، باستخدام dropArea من عينة التعليمات البرمجية لجعل اللغة أكثر وضوحًا:

هدف متى يتم إطلاقها؟
dragenter يتم سحب العنصر المسحوب فوق منطقة الإسقاط ، مما يجعله هدفًا لحدث الإسقاط إذا أسقطه المستخدم هناك.
dragleave يتم سحب العنصر المسحوب من منطقة الإسقاط إلى عنصر آخر ، مما يجعله هدفًا لحدث الإسقاط بدلاً من ذلك.
dragover كل بضع مئات من الألف من الثانية ، بينما يكون العنصر المسحوب فوق منطقة الإسقاط ويتحرك.
drop يحرر المستخدم زر الماوس ، ويسقط العنصر المسحوب في منطقة الإسقاط.

لاحظ أن العنصر الذي تم سحبه يتم سحبه فوق طفل من dropArea ، سيتم إطلاق dropArea في منطقة dragleave وسيتم إطلاق عنصر dragenter على هذا العنصر الفرعي لأنه target الجديد. سينتشر حدث drop حتى dropArea (ما لم يتم إيقاف النشر بواسطة مستمع حدث مختلف قبل أن يصل إلى هناك) ، لذلك سيستمر إطلاقه في dropArea على الرغم من أنه ليس target للحدث.

لاحظ أيضًا أنه من أجل إنشاء تفاعلات سحب وإفلات مخصصة ، ستحتاج إلى استدعاء event.preventDefault() في كل من المستمعين لهذه الأحداث. إذا لم تقم بذلك ، سينتهي المتصفح بفتح الملف الذي أسقطته بدلاً من إرساله إلى معالج حدث drop .

إعداد النموذج الخاص بنا

قبل أن نبدأ في إضافة وظيفة السحب والإفلات ، سنحتاج إلى نموذج أساسي بإدخال file قياسي. من الناحية الفنية ، هذا ليس ضروريًا ، ولكن من الجيد تقديمه كبديل في حال كان لدى المستخدم متصفح بدون دعم لواجهة برمجة تطبيقات السحب والإفلات.

 <div> <form class="my-form"> <p>Upload multiple files with the file dialog or by dragging and dropping images onto the dashed region</p> <input type="file" multiple accept="image/*" onchange="handleFiles(this.files)"> <label class="button" for="fileElem">Select some files</label> </form> </div>

هيكل بسيط جدا. قد تلاحظ وجود معالج onchange على input . سنلقي نظرة على ذلك لاحقًا. سيكون من الجيد أيضًا إضافة action إلى form وزر submit لمساعدة هؤلاء الأشخاص الذين ليس لديهم JavaScript ممكّنًا. ثم يمكنك استخدام JavaScript للتخلص منها للحصول على شكل أنظف. على أي حال ، ستحتاج إلى برنامج نصي من جانب الخادم لقبول التحميل ، سواء كان ذلك شيئًا تم تطويره داخليًا أو كنت تستخدم خدمة مثل Cloudinary للقيام بذلك نيابة عنك. بخلاف تلك الملاحظات ، لا يوجد شيء مميز هنا ، لذلك دعونا نلقي بعض الأنماط في:

 #drop-area { border: 2px dashed #ccc; border-radius: 20px; width: 480px; font-family: sans-serif; margin: 100px auto; padding: 20px; } #drop-area.highlight { border-color: purple; } p { margin-top: 0; } .my-form { margin-bottom: 10px; } #gallery { margin-top: 10px; } #gallery img { width: 150px; margin-bottom: 10px; margin-right: 10px; vertical-align: middle; } .button { display: inline-block; padding: 10px; background: #ccc; cursor: pointer; border-radius: 5px; border: 1px solid #ccc; } .button:hover { background: #ddd; } #fileElem { display: none; }

لم يتم تفعيل العديد من هذه الأساليب بعد ، لكن هذا جيد. تتمثل النقاط البارزة في الوقت الحالي في أن إدخال file مخفي ، ولكن تم تصميم تسميته label كزر ، لذلك سيدرك الأشخاص أنه يمكنهم النقر فوقه لإظهار مربع حوار تحديد الملف. نحن أيضًا نتبع اصطلاحًا من خلال تحديد منطقة الإسقاط بخطوط متقطعة.

إضافة وظيفة السحب والإفلات

الآن نصل إلى جوهر الموقف: السحب والإفلات. دعنا نلقي نصًا في أسفل الصفحة ، أو في ملف منفصل ، كيفما كنت ترغب في القيام بذلك. أول شيء نحتاجه في البرنامج النصي هو إشارة إلى منطقة الإسقاط حتى نتمكن من إرفاق بعض الأحداث بها:

 let dropArea = document.getElementById('drop-area')

الآن دعونا نضيف بعض الأحداث. سنبدأ بإضافة معالجات إلى جميع الأحداث لمنع السلوكيات الافتراضية ومنع الأحداث من الظهور أعلى من اللازم:

 ;['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => { dropArea.addEventListener(eventName, preventDefaults, false) }) function preventDefaults (e) { e.preventDefault() e.stopPropagation() }

الآن دعنا نضيف مؤشرًا للسماح للمستخدم بمعرفة أنه قد قام بالفعل بسحب العنصر فوق المنطقة الصحيحة باستخدام CSS لتغيير لون لون حدود منطقة الإسقاط. يجب أن تكون الأنماط موجودة بالفعل ضمن #drop-area.highlight selector ، لذلك دعونا نستخدم JS لإضافة فئة highlight تلك وإزالتها عند الضرورة.

 ;['dragenter', 'dragover'].forEach(eventName => { dropArea.addEventListener(eventName, highlight, false) }) ;['dragleave', 'drop'].forEach(eventName => { dropArea.addEventListener(eventName, unhighlight, false) }) function highlight(e) { dropArea.classList.add('highlight') } function unhighlight(e) { dropArea.classList.remove('highlight') }

كان علينا استخدام كل من dragenter أوفر dragover بسبب ما ذكرته سابقًا. إذا بدأت بالمرور مباشرة فوق dropArea ثم قمت بالمرور فوق أحد العناصر الفرعية ، فسيتم dragleave وستتم إزالة التمييز. يتم dragover حدث السحب بعد أحداث dragenter dragleave ، لذلك ستتم إضافة التظليل مرة أخرى إلى dropArea قبل أن نراه يتم إزالته.

نقوم أيضًا بإزالة التمييز عندما يترك العنصر المسحوب المنطقة المخصصة أو عند إسقاط العنصر.

الآن كل ما علينا فعله هو معرفة ما يجب فعله عند إسقاط بعض الملفات:

 dropArea.addEventListener('drop', handleDrop, false) function handleDrop(e) { let dt = e.dataTransfer let files = dt.files handleFiles(files) }

هذا لا يقربنا من الاكتمال ، لكنه يقوم بأمرين مهمين:

  1. يوضح كيفية الحصول على البيانات الخاصة بالملفات التي تم إسقاطها.
  2. يأخذنا إلى نفس المكان الذي كان فيه input file من خلال معالج onchange الخاص به: انتظار handleFiles .

ضع في اعتبارك أن files ليست مصفوفة ، ولكنها قائمة FileList . لذلك ، عندما نطبق handleFiles ، سنحتاج إلى تحويله إلى مصفوفة لتكرارها بسهولة أكبر:

 function handleFiles(files) { ([...files]).forEach(uploadFile) }

كان ذلك مضادًا للتغير المناخي. دعنا ندخل في uploadFile الحقيقية .

 function uploadFile(file) { let url = 'YOUR URL HERE' let formData = new FormData() formData.append('file', file) fetch(url, { method: 'POST', body: formData }) .then(() => { /* Done. Inform the user */ }) .catch(() => { /* Error. Inform the user */ }) }

هنا نستخدم FormData ، واجهة برمجة تطبيقات متصفح مدمجة لإنشاء بيانات النموذج لإرسالها إلى الخادم. ثم نستخدم واجهة برمجة تطبيقات fetch لإرسال الصورة بالفعل إلى الخادم. تأكد من تغيير عنوان URL للعمل مع النهاية الخلفية أو الخدمة ، ثم formData.append أي بيانات نموذج إضافية قد تحتاجها لمنح الخادم جميع المعلومات التي يحتاجها. بدلاً من ذلك ، إذا كنت تريد دعم Internet Explorer ، فقد ترغب في استخدام XMLHttpRequest ، مما يعني أن uploadFile سيبدو كما يلي:

 function uploadFile(file) { var url = 'YOUR URL HERE' var xhr = new XMLHttpRequest() var formData = new FormData() xhr.open('POST', url, true) xhr.addEventListener('readystatechange', function(e) { if (xhr.readyState == 4 && xhr.status == 200) { // Done. Inform the user } else if (xhr.readyState == 4 && xhr.status != 200) { // Error. Inform the user } }) formData.append('file', file) xhr.send(formData) }

اعتمادًا على كيفية إعداد الخادم الخاص بك ، قد ترغب في التحقق من نطاقات مختلفة من أرقام status بدلاً من 200 فقط ، ولكن لأغراضنا ، سيعمل هذا.

ميزات إضافية

هذه هي جميع الوظائف الأساسية ، لكننا غالبًا ما نريد المزيد من الوظائف. على وجه التحديد ، في هذا البرنامج التعليمي ، سنضيف جزء معاينة يعرض جميع الصور المختارة للمستخدم ، ثم سنضيف شريط تقدم يتيح للمستخدم رؤية تقدم عمليات التحميل. لذا ، فلنبدأ في معاينة الصور.

معاينة الصورة

هناك طريقتان يمكنك القيام بذلك: يمكنك الانتظار حتى بعد تحميل الصورة واطلب من الخادم إرسال عنوان URL للصورة ، ولكن هذا يعني أنك بحاجة إلى الانتظار وقد تكون الصور كبيرة جدًا في بعض الأحيان. البديل - الذي سنستكشفه اليوم - هو استخدام FileReader API على بيانات الملف التي تلقيناها من حدث drop . هذا غير متزامن ، ويمكنك بدلاً من ذلك استخدام FileReaderSync ، ولكن قد نحاول قراءة العديد من الملفات الكبيرة على التوالي ، لذلك قد يؤدي ذلك إلى حظر الخيط لفترة طويلة وإفساد التجربة حقًا. لذلك دعونا previewFile وظيفة ملف المعاينة ونرى كيف تعمل:

 function previewFile(file) { let reader = new FileReader() reader.readAsDataURL(file) reader.onloadend = function() { let img = document.createElement('img') img.src = reader.result document.getElementById('gallery').appendChild(img) } }

هنا نقوم بإنشاء new FileReader واستدعاء readAsDataURL عليه باستخدام كائن File . كما ذكرنا ، هذا غير متزامن ، لذلك نحتاج إلى إضافة معالج حدث onloadend للحصول على نتيجة القراءة. ثم نستخدم عنوان URL الأساسي 64 لبيانات باعتباره src لعنصر صورة جديد ونضيفه إلى عنصر gallery . هناك شيئان فقط يجب القيام بهما لإنجاز هذا العمل الآن: إضافة عنصر gallery ، والتأكد من previewFile بالفعل.

أولاً ، أضف HTML التالي بعد نهاية علامة form مباشرةً:

 <div></div>

لا شيء مميز؛ انها مجرد div. تم تحديد الأنماط بالفعل له وللصور الموجودة به ، لذلك لم يتبق شيء للقيام به هناك. لنقم الآن بتغيير وظيفة handleFiles إلى ما يلي:

 function handleFiles(files) { files = [...files] files.forEach(uploadFile) files.forEach(previewFile) }

هناك عدة طرق يمكنك القيام بها ، مثل التركيب ، أو رد اتصال واحد لـ forEach الذي قام بتشغيل uploadFile و previewFile فيه ، ولكن هذا يعمل أيضًا. وبهذا ، عندما تقوم بإسقاط أو تحديد بعض الصور ، يجب أن تظهر على الفور أسفل النموذج. الشيء المثير للاهتمام في هذا الأمر هو أنه - في بعض التطبيقات - قد لا ترغب في الواقع في تحميل الصور ، ولكن بدلاً من ذلك ، يمكنك تخزين عناوين URL للبيانات الخاصة بها في localStorage أو بعض ذاكرة التخزين المؤقت الأخرى من جانب العميل ليتمكن التطبيق من الوصول إليها لاحقًا. لا أستطيع أن أفكر شخصيًا في أي حالات استخدام جيدة لهذا ، لكنني على استعداد للمراهنة على وجود بعضها.

متابعة التقدم

إذا كان هناك شيء قد يستغرق بعض الوقت ، فيمكن أن يساعد شريط التقدم المستخدم على إدراك أن التقدم يتم إحرازه بالفعل ويعطي إشارة إلى المدة التي سيستغرقها إكماله. تعد إضافة مؤشر تقدم أمرًا سهلاً للغاية بفضل علامة progress HTML5. لنبدأ بإضافة ذلك إلى كود HTML هذه المرة.

 <progress max=100 value=0></progress>

يمكنك وضع ذلك مباشرة بعد label أو بين form ومعرض div ، أيهما تريد أكثر. لهذه المسألة ، يمكنك وضعها في أي مكان تريده داخل علامات النص body . لم تتم إضافة أي أنماط لهذا المثال ، لذلك سيُظهر التطبيق الافتراضي للمتصفح ، والذي يمكن صيانته. لنعمل الآن على إضافة JavaScript. سننظر أولاً في التنفيذ باستخدام fetch ثم سنعرض إصدارًا لـ XMLHttpRequest . للبدء ، سنحتاج إلى متغيرين جديدين في الجزء العلوي من البرنامج النصي:

 let filesDone = 0 let filesToDo = 0 let progressBar = document.getElementById('progress-bar')

عند استخدام fetch ، يمكننا فقط تحديد وقت انتهاء التحميل ، وبالتالي فإن المعلومات الوحيدة التي نتتبعها هي عدد الملفات التي تم تحديدها للتحميل (مثل filesToDo ) وعدد الملفات التي انتهى تحميلها (مثل filesDone ). نحتفظ أيضًا بمرجع إلى عنصر #progress-bar حتى نتمكن من تحديثه بسرعة. الآن دعنا ننشئ وظيفتين لإدارة التقدم:

 function initializeProgress(numfiles) { progressBar.value = 0 filesDone = 0 filesToDo = numfiles } function progressDone() { filesDone++ progressBar.value = filesDone / filesToDo * 100 }

عندما نبدأ في التحميل ، سيتم استدعاء initializeProgress لإعادة تعيين شريط التقدم. بعد ذلك ، مع كل تحميل مكتمل ، سنقوم باستدعاء progressDone "تم" لزيادة عدد التحميلات المكتملة وتحديث شريط التقدم لإظهار التقدم الحالي. لذلك دعونا نسمي هذه الوظائف بتحديث وظيفتين قديمتين:

 function handleFiles(files) { files = [...files] initializeProgress(files.length) // <- Add this line files.forEach(uploadFile) files.forEach(previewFile) } function uploadFile(file) { let url = 'YOUR URL HERE' let formData = new FormData() formData.append('file', file) fetch(url, { method: 'POST', body: formData }) .then(progressDone) // <- Add `progressDone` call here .catch(() => { /* Error. Inform the user */ }) }

وهذا كل شيء. الآن دعونا نلقي نظرة على تنفيذ XMLHttpRequest . يمكننا فقط إجراء تحديث سريع uploadFile ، لكن XMLHttpRequest يمنحنا في الواقع وظائف أكثر من fetch ، أي أننا قادرون على إضافة مستمع الحدث لتقدم التحميل في كل طلب ، والذي سيعطينا بشكل دوري معلومات حول مقدار الطلب تم الانتهاء من. لهذا السبب ، نحتاج إلى تتبع النسبة المئوية لإكمال كل طلب بدلاً من عدد الطلبات التي تم إجراؤها. لذلك ، لنبدأ باستبدال التصريحات الخاصة بـ filesDone و filesToDo بما يلي:

 let uploadProgress = []

ثم نحتاج إلى تحديث وظائفنا أيضًا. سنعيد تسمية progressDone الذي تم تنفيذه لتحديث التقدم updateProgress لتكون على النحو التالي:

 function initializeProgress(numFiles) { progressBar.value = 0 uploadProgress = [] for(let i = numFiles; i > 0; i--) { uploadProgress.push(0) } } function updateProgress(fileNumber, percent) { uploadProgress[fileNumber] = percent let total = uploadProgress.reduce((tot, curr) => tot + curr, 0) / uploadProgress.length progressBar.value = total }

تقوم الآن initializeProgress بتهيئة مصفوفة بطول يساوي numFiles مملوءة بالأصفار ، مما يشير إلى أن كل ملف مكتمل بنسبة 0٪. في updateProgress اكتشفنا الصورة التي يتم تحديث تقدمها وتغيير القيمة في هذا الفهرس إلى percent المتوفرة. نحسب بعد ذلك النسبة المئوية الإجمالية للتقدم عن طريق أخذ متوسط ​​جميع النسب المئوية وتحديث شريط التقدم ليعكس الإجمالي المحسوب. ما زلنا نسمي initializeProgress في handleFiles كما فعلنا في مثال fetch ، لذا كل ما نحتاجه الآن هو uploadFile لاستدعاء updateProgress .

 function uploadFile(file, i) { // <- Add `i` parameter var url = 'YOUR URL HERE' var xhr = new XMLHttpRequest() var formData = new FormData() xhr.open('POST', url, true) // Add following event listener xhr.upload.addEventListener("progress", function(e) { updateProgress(i, (e.loaded * 100.0 / e.total) || 100) }) xhr.addEventListener('readystatechange', function(e) { if (xhr.readyState == 4 && xhr.status == 200) { // Done. Inform the user } else if (xhr.readyState == 4 && xhr.status != 200) { // Error. Inform the user } }) formData.append('file', file) xhr.send(formData) }

أول شيء يجب ملاحظته هو أننا أضفنا معامل i . هذا هو فهرس الملف في قائمة الملفات. لا نحتاج إلى تحديث handleFiles لتمرير هذا المعامل لأنه يستخدم forEach ، والذي يعطي بالفعل فهرس العنصر باعتباره المعلمة الثانية لعمليات الاسترجاعات. أضفنا أيضًا مستمع حدث progress إلى xhr.upload حتى نتمكن من استدعاء updateProgress بالتقدم. يحتوي كائن الحدث (المشار إليه باسم e في الكود) على جزأين من المعلومات ذات الصلة به: تم loaded والذي يحتوي على عدد البايتات التي تم تحميلها حتى الآن total الذي يحتوي على إجمالي عدد وحدات البايت في الملف.

ال || 100 || 100 قطعة موجودة هناك لأنه في بعض الأحيان إذا كان هناك خطأ ، e.loaded و e.total سيكون صفراً ، مما يعني أن الحساب سيظهر كـ NaN ، لذلك يتم استخدام 100 بدلاً من ذلك للإبلاغ عن انتهاء الملف. يمكنك أيضًا استخدام 0 . في كلتا الحالتين ، سيظهر الخطأ في معالج readystatechange حتى تتمكن من إبلاغ المستخدم بها. هذا فقط لمنع طرح الاستثناءات لمحاولة حل الرياضيات باستخدام NaN .

خاتمة

هذه هي القطعة الأخيرة. لديك الآن صفحة ويب حيث يمكنك تحميل الصور عن طريق السحب والإفلات ، ومعاينة الصور التي يتم تحميلها على الفور ، ومشاهدة تقدم التحميل في شريط التقدم. يمكنك رؤية الإصدار النهائي (مع XMLHttpRequest ) قيد التشغيل على CodePen ، لكن كن على دراية بأن الخدمة التي أحملها الملفات لها حدود ، لذلك إذا اختبرها الكثير من الأشخاص ، فقد تنقطع لبعض الوقت.