كيفية إنشاء برنامج تحميل ملفات السحب والإفلات باستخدام Vanilla JavaScript
نشرت: 2022-03-10من الحقائق المعروفة أن مدخلات اختيار الملفات يصعب تحديد نمطها بالطريقة التي يريدها المطورون ، لذلك يخفيها الكثيرون وينشئون زرًا يفتح مربع حوار اختيار الملف بدلاً من ذلك. في الوقت الحاضر ، على الرغم من ذلك ، لدينا طريقة أكثر دقة للتعامل مع اختيار الملف: السحب والإفلات.
من الناحية الفنية ، كان هذا ممكنًا بالفعل لأن معظم (إن لم يكن كل ) تطبيقات إدخال تحديد الملف سمحت لك بسحب الملفات فوقها لتحديدها ، لكن هذا يتطلب منك إظهار عنصر 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) }
هذا لا يقربنا من الاكتمال ، لكنه يقوم بأمرين مهمين:
- يوضح كيفية الحصول على البيانات الخاصة بالملفات التي تم إسقاطها.
- يأخذنا إلى نفس المكان الذي كان فيه
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 ، لكن كن على دراية بأن الخدمة التي أحملها الملفات لها حدود ، لذلك إذا اختبرها الكثير من الأشخاص ، فقد تنقطع لبعض الوقت.