So erstellen Sie einen Drag-and-Drop-Datei-Uploader mit Vanilla JavaScript
Veröffentlicht: 2022-03-10Es 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-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.
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:
- Veranschaulicht, wie die Daten für die gelöschten Dateien abgerufen werden.
- Bringt uns mit seinem
input
-Handler an die gleiche Stelle, an der sich diefile
onchange
: Warten aufhandleFiles
.
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.