So erstellen Sie einen Drag-and-Drop-Datei-Uploader mit Vanilla JavaScript

Veröffentlicht: 2022-03-10
Kurze Zusammenfassung ↬ In diesem Artikel verwenden wir „Vanilla“ ES2015+ JavaScript (keine Frameworks oder Bibliotheken), um dieses Projekt abzuschließen, und es wird davon ausgegangen, dass Sie über ausreichende Kenntnisse in JavaScript im Browser verfügen. Dieses Beispiel sollte mit jedem Evergreen-Browser plus IE 10 und 11 kompatibel sein.

Es ist eine bekannte Tatsache, dass Eingaben zur Dateiauswahl schwierig so zu gestalten sind, wie Entwickler es möchten, daher blenden viele sie einfach aus und erstellen stattdessen eine Schaltfläche, die den Dateiauswahldialog öffnet. Heutzutage haben wir jedoch eine noch ausgefallenere Art, mit der Dateiauswahl umzugehen: Drag & Drop.

Technisch gesehen war dies bereits möglich, da die meisten (wenn nicht alle ) Implementierungen der Dateiauswahleingabe es Ihnen ermöglichten, Dateien darüber zu ziehen, um sie auszuwählen, aber dazu müssen Sie das file tatsächlich anzeigen. Lassen Sie uns also die vom Browser bereitgestellten APIs verwenden, um eine Drag-and-Drop-Dateiauswahl und einen Uploader zu implementieren.

In diesem Artikel verwenden wir „Vanilla“ ES2015+ JavaScript (keine Frameworks oder Bibliotheken), um dieses Projekt abzuschließen, und es wird davon ausgegangen, dass Sie über ausreichende Kenntnisse von JavaScript im Browser verfügen. Dieses Beispiel – abgesehen von der ES2015+-Syntax, die leicht in die ES5-Syntax geändert oder von Babel transpiliert werden kann – sollte mit jedem Evergreen-Browser plus IE 10 und 11 kompatibel sein.

Hier ist ein kurzer Blick auf das, was Sie machen werden:

Drag-and-Drop-Bild-Uploader in Aktion
Eine Demonstration einer Webseite, auf der Sie Bilder per Drag-and-Drop hochladen, eine Vorschau der hochgeladenen Bilder sofort anzeigen und den Fortschritt des Hochladens in einem Fortschrittsbalken sehen können.

Drag-and-Drop-Ereignisse

Das erste, was wir besprechen müssen, sind die Ereignisse im Zusammenhang mit Drag-and-Drop, da sie die treibende Kraft hinter dieser Funktion sind. Insgesamt gibt es acht Ereignisse, die der Browser im Zusammenhang mit Drag & Drop auslöst: drag , dragend , dragenter , dragexit , dragleave , dragover , dragstart und drop . Wir werden nicht alle durchgehen, da drag , dragend , dragexit und dragstart alle auf dem Element ausgelöst werden, das gezogen wird, und in unserem Fall werden wir Dateien aus unserem Dateisystem hineinziehen und nicht DOM-Elemente , sodass diese Ereignisse niemals angezeigt werden.

Wenn Sie neugierig darauf sind, können Sie einige Dokumentationen zu diesen Ereignissen auf MDN lesen.

Mehr nach dem Sprung! Lesen Sie unten weiter ↓

Wie Sie vielleicht erwarten, können Sie Ereignishandler für diese Ereignisse auf die gleiche Weise registrieren, wie Sie Ereignishandler für die meisten Browserereignisse registrieren: über 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)

Hier ist eine kleine Tabelle, die beschreibt, was diese Ereignisse tun, wobei dropArea aus dem Codebeispiel verwendet wird, um die Sprache klarer zu machen:

Fall Wann wird gefeuert?
dragenter Das gezogene Element wird über dropArea gezogen, wodurch es zum Ziel für das Drop-Ereignis wird, wenn der Benutzer es dort ablegt.
dragleave Das gezogene Element wird von dropArea weg und auf ein anderes Element gezogen, wodurch es stattdessen zum Ziel des drop-Ereignisses wird.
dragover Alle paar hundert Millisekunden, während sich das gezogene Element über dropArea befindet und sich bewegt.
drop Der Benutzer lässt seine Maustaste los und legt das gezogene Element auf dropArea ab.

Beachten Sie, dass das gezogene Element über ein untergeordnetes Element von dropArea gezogen wird, dragleave auf dropArea ausgelöst wird und dragenter auf diesem untergeordneten Element ausgelöst wird, da es das neue target ist. Das drop -Ereignis wird bis zu dropArea (es sei denn, die Weitergabe wird von einem anderen Ereignis-Listener gestoppt, bevor es dort ankommt), sodass es immer noch auf dropArea , obwohl es nicht das target für das Ereignis ist.

Beachten Sie auch, dass Sie zum Erstellen benutzerdefinierter Drag-and-Drop-Interaktionen event.preventDefault() in jedem der Listener für diese Ereignisse aufrufen müssen. Wenn Sie dies nicht tun, öffnet der Browser am Ende die Datei, die Sie abgelegt haben, anstatt sie an den drop -Event-Handler weiterzuleiten.

Einrichten unseres Formulars

Bevor wir mit dem Hinzufügen von Drag-and-Drop-Funktionen beginnen, benötigen wir ein einfaches file mit einer Standarddateieingabe. Technisch ist dies nicht notwendig, aber es ist eine gute Idee, es als Alternative bereitzustellen, falls der Benutzer einen Browser ohne Unterstützung für die Drag-and-Drop-API hat.

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

Ziemlich einfacher Aufbau. Möglicherweise bemerken Sie einen onchange -Handler für die input . Das schauen wir uns später an. Es wäre auch eine gute Idee, dem form eine action und eine submit -Schaltfläche hinzuzufügen, um den Leuten zu helfen, die JavaScript nicht aktiviert haben. Dann können Sie JavaScript verwenden, um sie für ein saubereres Formular zu entfernen. In jedem Fall benötigen Sie ein serverseitiges Skript, um den Upload zu akzeptieren, unabhängig davon, ob es sich um eine Eigenentwicklung handelt oder Sie einen Dienst wie Cloudinary verwenden, um dies für Sie zu tun. Abgesehen von diesen Notizen gibt es hier nichts Besonderes, also werfen wir einige Stile ein:

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

Viele dieser Stile kommen noch nicht ins Spiel, aber das ist in Ordnung. Die Highlights sind vorerst, dass die file ausgeblendet ist, aber ihre label so gestaltet ist, dass sie wie eine Schaltfläche aussieht, sodass die Benutzer erkennen, dass sie darauf klicken können, um den Dateiauswahldialog aufzurufen. Wir folgen auch einer Konvention, indem wir den Drop-Bereich mit gestrichelten Linien umreißen.

Hinzufügen der Drag-and-Drop-Funktion

Jetzt kommen wir zum Kern der Situation: Drag and Drop. Lassen Sie uns ein Skript unten auf der Seite oder in einer separaten Datei einfügen, ganz wie Sie möchten. Das erste, was wir im Skript brauchen, ist ein Verweis auf den Drop-Bereich, damit wir einige Ereignisse daran anhängen können:

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

Lassen Sie uns nun einige Ereignisse hinzufügen. Wir beginnen damit, allen Ereignissen Handler hinzuzufügen, um Standardverhalten zu verhindern und zu verhindern, dass die Ereignisse höher als nötig sprudeln:

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

Lassen Sie uns nun einen Indikator hinzufügen, der den Benutzer darüber informiert, dass er das Element tatsächlich über den richtigen Bereich gezogen hat, indem er CSS verwendet, um die Farbe der Rahmenfarbe des Drop-Bereichs zu ändern. Die Stile sollten bereits unter dem #drop-area.highlight Selektor vorhanden sein, also verwenden wir JS, um diese highlight -Klasse bei Bedarf hinzuzufügen und zu entfernen.

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

Aufgrund dessen, was ich zuvor erwähnt habe, mussten wir dragenter und dragover für die Hervorhebung verwenden. Wenn Sie direkt über dropArea schweben und dann über eines seiner Kinder schweben, wird dragleave ausgelöst und die Hervorhebung wird entfernt. Das dragover Ereignis wird nach den dragenter und dragleave Ereignissen ausgelöst, sodass die Hervorhebung wieder zu dropArea hinzugefügt wird, bevor wir sehen, dass sie entfernt wird.

Wir entfernen auch die Hervorhebung, wenn das gezogene Element den festgelegten Bereich verlässt oder wenn Sie das Element ablegen.

Jetzt müssen wir nur noch herausfinden, was zu tun ist, wenn einige Dateien gelöscht werden:

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

Dies bringt uns nicht annähernd zum Abschluss, aber es bewirkt zwei wichtige Dinge:

  1. Veranschaulicht, wie die Daten für die gelöschten Dateien abgerufen werden.
  2. Bringt uns mit seinem input -Handler an die gleiche Stelle, an der sich die file onchange : Warten auf handleFiles .

Denken Sie daran, dass files kein Array ist, sondern eine FileList . Wenn wir handleFiles implementieren, müssen wir es in ein Array konvertieren, um es einfacher durchlaufen zu können:

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

Das war enttäuschend. Kommen wir zu uploadFile für die wirklich fleischigen Sachen.

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

Hier verwenden wir FormData , eine integrierte Browser-API zum Erstellen von Formulardaten, die an den Server gesendet werden. Wir verwenden dann die fetch -API, um das Bild tatsächlich an den Server zu senden. Stellen Sie sicher, dass Sie die URL so ändern, dass sie mit Ihrem Back-End oder Dienst funktioniert, und formData.append alle zusätzlichen Formulardaten an, die Sie möglicherweise benötigen, um dem Server alle erforderlichen Informationen zu geben. Wenn Sie Internet Explorer unterstützen möchten, können Sie alternativ XMLHttpRequest verwenden, was bedeutet, dass uploadFile stattdessen so aussehen würde:

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

Je nachdem, wie Ihr Server eingerichtet ist, möchten Sie möglicherweise nach verschiedenen Bereichen von status statt nur nach 200 , aber für unsere Zwecke wird dies funktionieren.

Zusatzfunktionen

Das ist die gesamte Basisfunktionalität, aber oft möchten wir mehr Funktionalität. Insbesondere fügen wir in diesem Tutorial ein Vorschaufenster hinzu, das dem Benutzer alle ausgewählten Bilder anzeigt, und fügen dann einen Fortschrittsbalken hinzu, mit dem der Benutzer den Fortschritt der Uploads sehen kann. Beginnen wir also mit der Vorschau von Bildern.

Bildvorschau

Dazu gibt es mehrere Möglichkeiten: Sie könnten warten, bis das Bild hochgeladen wurde, und den Server bitten, die URL des Bilds zu senden, aber das bedeutet, dass Sie warten müssen und Bilder manchmal ziemlich groß sein können. Die Alternative – die wir heute untersuchen werden – besteht darin, die FileReader-API für die Dateidaten zu verwenden, die wir vom drop -Ereignis erhalten haben. Dies ist asynchron, und Sie könnten alternativ FileReaderSync verwenden, aber wir könnten versuchen, mehrere große Dateien hintereinander zu lesen, sodass dies den Thread für eine ganze Weile blockieren und die Erfahrung wirklich ruinieren könnte. Lassen Sie uns also eine previewFile -Funktion erstellen und sehen, wie sie funktioniert:

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

Hier erstellen wir einen new FileReader und rufen readAsDataURL mit dem File -Objekt auf. Wie bereits erwähnt, ist dies asynchron, daher müssen wir einen onloadend -Ereignishandler hinzufügen, um das Ergebnis des Lesevorgangs zu erhalten. Wir verwenden dann die Basis-64-Daten-URL als Quelle für ein neues gallery und fügen sie dem src hinzu. Es müssen nur zwei Dinge getan werden, damit dies jetzt funktioniert: Fügen Sie das gallery hinzu und stellen Sie sicher, dass previewFile tatsächlich aufgerufen wird.

Fügen Sie zunächst den folgenden HTML-Code direkt nach dem Ende des form -Tags hinzu:

 <div></div>

Nichts Besonderes; es ist nur ein div. Die Stile dafür und die darin enthaltenen Bilder sind bereits festgelegt, sodass dort nichts mehr zu tun ist. Lassen Sie uns nun die Funktion handleFiles wie folgt ändern:

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

Es gibt einige Möglichkeiten, wie Sie dies hätten tun können, z. B. Komposition oder ein einzelner Rückruf an forEach , der uploadFile und previewFile darin ausgeführt hat, aber das funktioniert auch. Und wenn Sie einige Bilder ablegen oder auswählen, sollten sie fast sofort unter dem Formular angezeigt werden. Das Interessante daran ist, dass Sie in bestimmten Anwendungen möglicherweise keine Bilder hochladen möchten, sondern stattdessen die Daten-URLs davon in localStorage oder einem anderen clientseitigen Cache speichern, auf den später von der App zugegriffen werden kann. Ich kann mir persönlich keine guten Anwendungsfälle dafür vorstellen, aber ich bin bereit zu wetten, dass es einige gibt.

Fortschritt verfolgen

Wenn etwas eine Weile dauern könnte, kann ein Fortschrittsbalken einem Benutzer helfen, zu erkennen, dass tatsächlich Fortschritte erzielt werden, und einen Hinweis darauf geben, wie lange es dauern wird, bis es abgeschlossen ist. Das Hinzufügen einer Fortschrittsanzeige ist dank des HTML5- progress -Tags ziemlich einfach. Beginnen wir damit, dies diesmal zum HTML-Code hinzuzufügen.

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

Sie können das direkt nach dem label oder zwischen dem form und dem Galerie- div einfügen, je nachdem, was Ihnen mehr gefällt. Sie können es innerhalb der body -Tags an beliebiger Stelle platzieren. Für dieses Beispiel wurden keine Stile hinzugefügt, daher wird die Standardimplementierung des Browsers angezeigt, die gewartet werden kann. Lassen Sie uns nun daran arbeiten, das JavaScript hinzuzufügen. Wir sehen uns zuerst die Implementierung mit fetch an und zeigen dann eine Version für XMLHttpRequest . Zu Beginn benötigen wir ein paar neue Variablen am Anfang des Skripts:

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

Bei der Verwendung von fetch können wir nur feststellen, wann ein Upload abgeschlossen ist, sodass die einzigen Informationen, die wir verfolgen, darin bestehen, wie viele Dateien zum Hochladen ausgewählt wurden (als filesToDo ) und wie viele Dateien hochgeladen wurden (als filesDone ). Wir behalten auch einen Verweis auf das Element #progress-bar bei, damit wir es schnell aktualisieren können. Lassen Sie uns nun ein paar Funktionen zum Verwalten des Fortschritts erstellen:

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

Wenn wir mit dem Hochladen beginnen, wird initializeProgress aufgerufen, um den Fortschrittsbalken zurückzusetzen. Dann rufen wir bei jedem abgeschlossenen Upload progressDone auf, um die Anzahl der abgeschlossenen Uploads zu erhöhen und den Fortschrittsbalken zu aktualisieren, um den aktuellen Fortschritt anzuzeigen. Rufen wir also diese Funktionen auf, indem wir ein paar alte Funktionen aktualisieren:

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

Und das ist es. Werfen wir nun einen Blick auf die XMLHttpRequest -Implementierung. Wir könnten einfach ein schnelles Update an uploadFile , aber XMLHttpRequest bietet uns tatsächlich mehr Funktionalität als fetch , nämlich wir können einen Ereignis-Listener für den Upload-Fortschritt bei jeder Anfrage hinzufügen, der uns regelmäßig Informationen darüber gibt, wie viel von der Anfrage ist beendet. Aus diesem Grund müssen wir den prozentualen Abschluss jeder Anfrage nachverfolgen, anstatt nur, wie viele erledigt sind. Beginnen wir also damit, die Deklarationen für filesDone und filesToDo durch Folgendes zu ersetzen:

 let uploadProgress = []

Dann müssen wir auch unsere Funktionen aktualisieren. Wir benennen progressDone in updateProgress um und ändern sie wie folgt:

 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 }

Jetzt initializeProgress initialisiert ein Array mit einer Länge gleich numFiles , das mit Nullen gefüllt ist, was bedeutet, dass jede Datei zu 0 % vollständig ist. In updateProgress finden wir heraus, bei welchem ​​Bild der Fortschritt aktualisiert wird, und ändern den Wert an diesem Index in den angegebenen percent . Wir berechnen dann den Gesamtfortschrittsprozentsatz, indem wir einen Durchschnitt aller Prozentsätze nehmen und den Fortschrittsbalken aktualisieren, um die berechnete Gesamtsumme widerzuspiegeln. Wir rufen immer noch initializeProgress in handleFiles genauso auf wie im fetch , also müssen wir jetzt nur uploadFile aktualisieren, um 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) }

Das erste, was zu beachten ist, ist, dass wir einen i -Parameter hinzugefügt haben. Dies ist der Index der Datei in der Liste der Dateien. Wir müssen handleFiles nicht aktualisieren, um diesen Parameter zu übergeben, da es forEach verwendet, das den Index des Elements bereits als zweiten Parameter für Rückrufe angibt. Wir haben auch den progress -Ereignis-Listener zu xhr.upload , damit wir updateProgress mit dem Fortschritt aufrufen können. Das Ereignisobjekt (im Code als e bezeichnet) enthält zwei relevante Informationen: „ loaded “ enthält die Anzahl der bisher hochgeladenen Bytes und „ total “ enthält die Anzahl der Bytes, aus denen die Datei insgesamt besteht.

Die || 100 || 100 Stück sind drin, denn manchmal, wenn ein Fehler auftritt, sind e.loaded und e.total Null, was bedeutet, dass die Berechnung als NaN herauskommt, also wird die 100 stattdessen verwendet, um zu melden, dass die Datei fertig ist. Du könntest auch 0 verwenden. In beiden Fällen wird der Fehler im readystatechange Handler angezeigt, sodass Sie den Benutzer darüber informieren können. Dies soll lediglich verhindern, dass Ausnahmen ausgelöst werden, wenn versucht wird, mit NaN zu rechnen.

Fazit

Das ist das letzte Stück. Sie haben jetzt eine Webseite, auf der Sie Bilder per Drag-and-Drop hochladen, eine Vorschau der hochgeladenen Bilder sofort anzeigen und den Fortschritt des Hochladens in einem Fortschrittsbalken sehen können. Sie können die endgültige Version (mit XMLHttpRequest ) in Aktion auf CodePen sehen, aber seien Sie sich bewusst, dass der Dienst, zu dem ich die Dateien hochlade, Grenzen hat. Wenn also viele Leute ihn testen, kann es eine Zeit lang brechen.