Cum să faci un instrument de încărcare de fișiere prin glisare și plasare cu Vanilla JavaScript

Publicat: 2022-03-10
Rezumat rapid ↬ În acest articol, vom folosi „vanilla” ES2015+ JavaScript (fără cadre sau biblioteci) pentru a finaliza acest proiect și se presupune că aveți cunoștințe de lucru despre JavaScript în browser. Acest exemplu ar trebui să fie compatibil cu fiecare browser evergreen plus IE 10 și 11.

Este un fapt cunoscut că intrările de selecție a fișierelor sunt dificil de stilat așa cum doresc dezvoltatorii, așa că mulți pur și simplu îl ascund și creează un buton care deschide dialogul de selecție a fișierelor. În zilele noastre, totuși, avem o modalitate și mai sofisticată de a gestiona selecția fișierelor: trageți și plasați.

Din punct de vedere tehnic, acest lucru a fost deja posibil deoarece majoritatea (dacă nu toate ) implementările de intrare de selecție a fișierelor v-au permis să trageți fișierele peste el pentru a le selecta, dar acest lucru necesită să afișați de fapt elementul file . Deci, să folosim de fapt API-urile oferite de browser pentru a implementa un selector de fișiere și un dispozitiv de încărcare cu drag-and-drop.

În acest articol, vom folosi „vanilla” ES2015+ JavaScript (fără cadre sau biblioteci) pentru a finaliza acest proiect și se presupune că aveți cunoștințe de lucru despre JavaScript în browser. Acest exemplu – în afară de sintaxa ES2015+, care poate fi schimbată cu ușurință în sintaxa ES5 sau transpilată de Babel – ar trebui să fie compatibil cu orice browser evergreen plus IE 10 și 11.

Iată o privire rapidă asupra a ceea ce vei face:

Glisați și plasați dispozitivul de încărcare a imaginilor în acțiune
O demonstrație a unei pagini web în care puteți încărca imagini prin glisare și plasare, previzualizați imaginile care sunt încărcate imediat și puteți vedea progresul încărcării într-o bară de progres.

Evenimente drag-and-drop

Primul lucru pe care trebuie să-l discutăm sunt evenimentele legate de drag-and-drop, deoarece ele sunt forța motrice din spatele acestei funcții. În total, există opt evenimente declanșate de browser legate de drag and drop: drag , dragend , dragenter , dragexit , dragleave , dragstart , dragover și drop . Nu le vom analiza pe toate, deoarece drag , dragend , dragexit și dragstart sunt toate declanșate pe elementul care este tras și, în cazul nostru, vom trage fișiere din sistemul nostru de fișiere, mai degrabă decât elementele DOM , astfel încât aceste evenimente nu vor apărea niciodată.

Dacă sunteți curios despre ele, puteți citi câteva documentații despre aceste evenimente pe MDN.

Mai multe după săritură! Continuați să citiți mai jos ↓

După cum v-ați putea aștepta, puteți înregistra handlere de evenimente pentru aceste evenimente în același mod în care înregistrați handlere de evenimente pentru majoritatea evenimentelor din browser: prin 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)

Iată un mic tabel care descrie ce fac aceste evenimente, folosind dropArea din exemplul de cod pentru a face limbajul mai clar:

Eveniment Când este concediat?
dragenter Elementul tras este tras peste dropArea, făcându-l ținta pentru evenimentul drop dacă utilizatorul îl aruncă acolo.
dragleave Elementul tras este tras din dropArea și pe alt element, făcându-l ținta pentru evenimentul drop.
dragover La fiecare câteva sute de milisecunde, în timp ce elementul tras este peste dropArea și se mișcă.
drop Utilizatorul eliberează butonul mouse-ului, plasând elementul tras pe dropArea.

Rețineți că elementul tras este tras peste un element copil al dropArea , dragleave se va declanșa pe dropArea și dragenter se va declanșa pe acel element copil deoarece este noua target . Evenimentul drop se va propaga până la dropArea (cu excepția cazului în care propagarea este oprită de un alt ascultător de eveniment înainte de a ajunge acolo), așa că se va declanșa în continuare pe dropArea , deși nu este target evenimentului.

De asemenea, rețineți că, pentru a crea interacțiuni personalizate de tip drag-and-drop, va trebui să apelați event.preventDefault() în fiecare dintre ascultătorii acestor evenimente. Dacă nu o faceți, browserul va ajunge să deschidă fișierul pe care l-ați aruncat în loc să-l trimită la handlerul de evenimente de drop .

Configurarea formularului nostru

Înainte de a începe să adăugăm funcționalitatea drag-and-drop, vom avea nevoie de un formular de bază cu o intrare file standard. Din punct de vedere tehnic, acest lucru nu este necesar, dar este o idee bună să-l oferiți ca alternativă în cazul în care utilizatorul are un browser fără suport pentru API-ul drag-and-drop.

 <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>

Structură destul de simplă. Este posibil să observați un handler onchange pe input . Ne vom uita la asta mai târziu. De asemenea, ar fi o idee bună să adăugați o action în form și un buton de submit pentru a ajuta persoanele care nu au JavaScript activat. Apoi puteți folosi JavaScript pentru a scăpa de ele pentru o formă mai curată. În orice caz, veți avea nevoie de un script pe partea de server pentru a accepta încărcarea, fie că este ceva dezvoltat intern, fie că utilizați un serviciu precum Cloudinary pentru a face acest lucru pentru dvs. În afară de acele note, nu este nimic special aici, așa că hai să aruncăm câteva stiluri în:

 #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; }

Multe dintre aceste stiluri nu intră încă în joc, dar este în regulă. Cele mai importante momente sunt că intrarea file este ascunsă, dar label acestuia este stilată astfel încât să arate ca un buton, astfel încât oamenii își vor da seama că pot face clic pe el pentru a afișa dialogul de selecție a fișierului. De asemenea, urmărim o convenție prin conturarea zonei de picătură cu linii întrerupte.

Adăugarea funcției de glisare și plasare

Acum ajungem la miezul situației: drag and drop. Să aruncăm un script în partea de jos a paginii sau într-un fișier separat, oricum ai vrea să o faci. Primul lucru de care avem nevoie în script este o referință la zona de drop, astfel încât să putem atașa câteva evenimente la aceasta:

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

Acum să adăugăm câteva evenimente. Vom începe cu adăugarea de handlere la toate evenimentele pentru a preveni comportamentele implicite și pentru a opri evenimentele să apară mai mult decât este necesar:

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

Acum să adăugăm un indicator pentru a-i anunța utilizatorului că într-adevăr a tras elementul peste zona corectă, folosind CSS pentru a schimba culoarea culorii marginii zonei de fixare. Stilurile ar trebui să fie deja acolo sub selectorul #drop-area.highlight , așa că haideți să folosim JS pentru a adăuga și elimina acea clasă de highlight atunci când este necesar.

 ;['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') }

A trebuit să folosim atât dragenter , cât și dragover pentru evidențiere din cauza a ceea ce am menționat mai devreme. Dacă începeți să treceți cu mouse-ul direct peste dropArea și apoi să treceți peste unul dintre copiii săi, atunci dragleave va fi declanșat și evidențierea va fi eliminată. Evenimentul dragover este declanșat după evenimentele dragenter și dragleave , astfel încât evidențierea va fi adăugată înapoi în dropArea înainte de a vedea că este eliminat.

De asemenea, eliminăm evidențierea atunci când elementul tras părăsește zona desemnată sau când plasați elementul.

Acum tot ce trebuie să facem este să ne dăm seama ce să facem când unele fișiere sunt aruncate:

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

Acest lucru nu ne aduce aproape de finalizare, dar face două lucruri importante:

  1. Demonstrează cum să obțineți datele pentru fișierele care au fost aruncate.
  2. Ne duce în același loc în care a fost input file cu handlerul său onchange : așteptând handleFiles .

Rețineți că files nu sunt o matrice, ci o listă de FileList . Deci, când implementăm handleFiles , va trebui să-l convertim într-o matrice pentru a itera mai ușor peste el:

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

A fost anticlimatic. Să intrăm în uploadFile pentru lucruri adevărate .

 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 */ }) }

Aici folosim FormData , un browser API încorporat pentru crearea datelor de formular pentru a le trimite către server. Apoi folosim API-ul fetch pentru a trimite efectiv imaginea către server. Asigurați-vă că modificați adresa URL pentru a funcționa cu back-end-ul sau serviciul dvs. și formData.append orice date suplimentare de formular de care aveți nevoie pentru a oferi serverului toate informațiile de care are nevoie. Ca alternativă, dacă doriți să acceptați Internet Explorer, poate doriți să utilizați XMLHttpRequest , ceea ce înseamnă că uploadFile ar arăta astfel:

 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) }

În funcție de modul în care este configurat serverul dvs., este posibil să doriți să verificați diferite intervale de numere de status , mai degrabă decât doar 200 , dar pentru scopurile noastre, acest lucru va funcționa.

Caracteristici suplimentare

Aceasta este toată funcționalitatea de bază, dar adesea dorim mai multe funcționalități. Mai exact, în acest tutorial, vom adăuga un panou de previzualizare care afișează utilizatorului toate imaginile alese, apoi vom adăuga o bară de progres care îi permite utilizatorului să vadă progresul încărcărilor. Deci, să începem cu previzualizarea imaginilor.

Previzualizare imagine

Există câteva moduri în care puteți face acest lucru: puteți aștepta până după ce imaginea a fost încărcată și să cereți serverului să trimită adresa URL a imaginii, dar asta înseamnă că trebuie să așteptați, iar imaginile pot fi destul de mari uneori. Alternativa – pe care o vom explora astăzi – este să folosim API-ul FileReader pe datele fișierului pe care le-am primit de la evenimentul de drop . Acesta este asincron și, în mod alternativ, puteți utiliza FileReaderSync, dar am putea încerca să citim mai multe fișiere mari la rând, astfel încât acest lucru ar putea bloca firul pentru o perioadă de timp și ar putea strica experiența. Deci, să creăm o funcție de previewFile și să vedem cum funcționează:

 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) } }

Aici creăm un new FileReader și apelăm readAsDataURL pe el cu obiectul File . După cum am menționat, acesta este asincron, așa că trebuie să adăugăm un handler de evenimente onloadend pentru a obține rezultatul citirii. Apoi folosim URL-ul de date de bază 64 ca src pentru un nou element de imagine și îl adăugăm la elementul gallery . Există doar două lucruri care trebuie făcute pentru ca acest lucru să funcționeze acum: adăugați elementul gallery și asigurați-vă că previewFile este de fapt apelat.

Mai întâi, adăugați următorul cod HTML imediat după sfârșitul etichetei form :

 <div></div>

Nimic special; este doar un div. Stilurile sunt deja specificate pentru el și imaginile din el, așa că nu mai e nimic de făcut acolo. Acum să schimbăm funcția handleFiles în următoarea:

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

Există câteva moduri în care ați fi putut face acest lucru, cum ar fi compoziția sau un singur apel înapoi la forEach care a rulat uploadFile și previewFile în el, dar funcționează și acest lucru. Și cu asta, atunci când aruncați sau selectați unele imagini, acestea ar trebui să apară aproape instantaneu sub formular. Lucrul interesant este că, în anumite aplicații, s-ar putea să nu doriți să încărcați imagini, ci să stocați adresele URL de date ale acestora în localStorage sau într-un alt cache la nivelul clientului pentru a fi accesat de aplicație mai târziu. Nu mă pot gândi personal la cazuri bune de utilizare pentru asta, dar sunt dispus să pariez că există unele.

Urmărirea progresului

Dacă ceva ar putea dura ceva timp, o bară de progres poate ajuta utilizatorul să realizeze că progresul este de fapt realizat și să ofere o indicație despre cât timp va dura să fie finalizat. Adăugarea unui indicator de progres este destul de ușoară datorită etichetei de progress HTML5. Să începem prin a adăuga asta la codul HTML de data aceasta.

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

Puteți plasa asta imediat după label sau între form și galerie div , oricare doriți mai mult. De altfel, îl puteți plasa oriunde doriți în etichetele body . Nu au fost adăugate stiluri pentru acest exemplu, așa că va afișa implementarea implicită a browserului, care este funcțională. Acum să lucrăm la adăugarea JavaScript. Mai întâi ne vom uita la implementare folosind fetch și apoi vom afișa o versiune pentru XMLHttpRequest . Pentru a începe, vom avea nevoie de câteva variabile noi în partea de sus a scriptului:

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

Când folosim fetch , putem stabili doar când se termină încărcarea, astfel încât singura informație pe care o urmărim este câte fișiere sunt selectate pentru a încărca (ca filesToDo ) și numărul de fișiere care s-au terminat încărcat (ca filesDone ). De asemenea, păstrăm o referință la elementul #progress-bar , astfel încât să îl putem actualiza rapid. Acum să creăm câteva funcții pentru gestionarea progresului:

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

Când începem încărcarea, va fi apelat initializeProgress pentru a reseta bara de progres. Apoi, cu fiecare încărcare finalizată, vom apela progressDone pentru a crește numărul de încărcări finalizate și vom actualiza bara de progres pentru a afișa progresul curent. Deci, să numim aceste funcții actualizând câteva funcții vechi:

 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 */ }) }

Si asta e. Acum să aruncăm o privire la implementarea XMLHttpRequest . Am putea doar să facem o actualizare rapidă pentru uploadFile , dar XMLHttpRequest ne oferă de fapt mai multe funcționalități decât fetch , și anume putem adăuga un ascultător de evenimente pentru progresul încărcării la fiecare solicitare, care ne va oferi periodic informații despre cât de mult este solicitată. terminat. Din acest motiv, trebuie să urmărim procentul de finalizare a fiecărei solicitări în loc de doar câte sunt efectuate. Deci, să începem cu înlocuirea declarațiilor pentru filesDone și filesToDo cu următoarele:

 let uploadProgress = []

Apoi, trebuie să ne actualizăm și funcțiile. Vom redenumi progressDone la updateProgress și le vom modifica astfel:

 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 }

Acum initializeProgress inițializează o matrice cu o lungime egală cu numFiles care este umplută cu zerouri, indicând faptul că fiecare fișier este 0% complet. În updateProgress aflăm ce imagine are progresul actualizat și modificăm valoarea la acel index la percent furnizat. Apoi calculăm procentul total de progres luând o medie a tuturor procentelor și actualizăm bara de progres pentru a reflecta totalul calculat. În continuare apelăm initializeProgress în handleFiles la fel ca și în exemplul de fetch , așa că acum tot ce trebuie să actualizăm este uploadFile pentru a apela 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) }

Primul lucru de remarcat este că am adăugat un parametru i . Acesta este indexul fișierului din lista de fișiere. Nu trebuie să actualizăm handleFiles pentru a transmite acest parametru deoarece folosește forEach , care oferă deja indexul elementului ca al doilea parametru pentru apeluri inverse. De asemenea, am adăugat ascultătorul de evenimente de progress la xhr.upload , astfel încât să putem apela updateProgress cu progresul. Obiectul eveniment (denumit e în cod) are două informații relevante pe el: loaded care conține numărul de octeți care au fost încărcați până acum și total care conține numărul de octeți în care este fișierul în total.

Cel || 100 || 100 piese sunt acolo, deoarece uneori, dacă există o eroare, e.loaded și e.total vor fi zero, ceea ce înseamnă că calculul va ieși ca NaN , așa că 100 este folosit în schimb pentru a raporta că fișierul este gata. Puteți folosi și 0 . În ambele cazuri, eroarea va apărea în handlerul readystatechange , astfel încât să puteți informa utilizatorul despre acestea. Acest lucru este doar pentru a preveni aruncarea de excepții pentru că încercați să faceți matematică cu NaN .

Concluzie

Asta e piesa finală. Acum aveți o pagină web în care puteți încărca imagini prin glisare și plasare, previzualizați imaginile care sunt încărcate imediat și puteți vedea progresul încărcării într-o bară de progres. Puteți vedea versiunea finală (cu XMLHttpRequest ) în acțiune pe CodePen, dar fiți conștienți de faptul că serviciul în care încarc fișierele are limite, așa că dacă mulți oameni îl testează, se poate întrerupe pentru o perioadă.