HTML5-Datei-Uploads per Drag-and-drop mit JavaScript umsetzen
Uploads per Drag-and-drop wirken natürlich, senken Hürden und lassen sich mit modernen Browser-APIs vollständig im Frontend umsetzen. In dieser Anleitung bauen Sie einen produktionsreifen HTML5-Dateiuploader, der Dateien validiert, den Fortschritt anzeigt und mehrere Dateien effizient verarbeitet.
Einführung in die Datei-Upload-Funktionen von HTML5
Mit HTML5 kamen die Schnittstellen File, FileList, FileReader und DataTransfer hinzu, die
JavaScript-Code vollwertigen Zugriff auf die von Nutzern ausgewählten Dateien geben. In Kombination
mit fetch, FormData und
XMLHttpRequest können Sie Uploads streamen, clientseitig validieren und
Dateivorschauen bereitstellen, ohne auf Plugins angewiesen zu sein.
Mit diesen APIs können Sie:
- Dateimetadaten wie
name,sizeundtypeprüfen. - Clientseitige Vorschauen für Bilder, Text oder andere Dateitypen erzeugen.
- Drag-and-drop-Funktionalität für ein besseres Nutzererlebnis umsetzen.
- Den Übertragungsfortschritt überwachen (vor allem mit
XMLHttpRequest) oder moderne Bibliotheken nutzen, die das abstrahieren.
Ein einfaches Drag-and-drop-Formular einrichten
Hier ist die HTML-Struktur für unseren Dateiuploader:
<form id="upload-form">
<input type="file" id="file-input" multiple hidden />
<div id="drop-zone" class="drop-zone" tabindex="0" role="button">Drop files here or click to browse</div>
<div id="file-preview"></div>
<progress id="upload-progress" value="0" max="100" hidden></progress>
<p id="upload-status" class="status-message" aria-live="polite"></p>
<button type="button" id="upload-button" disabled>Upload Files</button>
</form>
Wir haben tabindex="0" zu drop-zone hinzugefügt, damit dieses Element fokussierbar ist, und
aria-live="polite" zum Absatz upload-status, um die Barrierefreiheit zu verbessern.
Drag-and-drop-Interaktionen umsetzen
JavaScript übernimmt die Drag-and-drop-Events und die Dateiauswahl:
const dropZone = document.getElementById('drop-zone')
const fileInput = document.getElementById('file-input')
const uploadBtn = document.getElementById('upload-button')
const preview = document.getElementById('file-preview')
const uploadProgress = document.getElementById('upload-progress')
const uploadStatusEl = document.getElementById('upload-status')
let filesToUpload = [] // Renamed for clarity
// Helper functions for status messages
function showError(message) {
uploadStatusEl.textContent = message
uploadStatusEl.className = 'status-message error'
}
function showSuccess(message) {
uploadStatusEl.textContent = message
uploadStatusEl.className = 'status-message success'
}
// Click-to-browse fallback and keyboard accessibility
dropZone.addEventListener('click', () => fileInput.click())
dropZone.addEventListener('keydown', (e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault()
fileInput.click()
}
})
fileInput.addEventListener('change', () => {
if (fileInput.files.length > 0) {
handleFiles(fileInput.files)
}
})
// Prevent default browser behavior for drag events
;['dragenter', 'dragover', 'dragleave', 'drop'].forEach((eventName) => {
dropZone.addEventListener(eventName, (e) => {
e.preventDefault()
e.stopPropagation()
})
})
// Add visual feedback for drag events
;['dragenter', 'dragover'].forEach((eventName) => {
dropZone.addEventListener(eventName, () => dropZone.classList.add('drag-over'))
})
;['dragleave', 'drop'].forEach((eventName) => {
dropZone.addEventListener(eventName, () => dropZone.classList.remove('drag-over'))
})
dropZone.addEventListener('drop', (e) => {
if (e.dataTransfer.files.length > 0) {
handleFiles(e.dataTransfer.files)
fileInput.files = e.dataTransfer.files // Synchronize fileInput.files for consistency
}
})
Ausgewählte Dateien validieren
Die clientseitige Validierung gibt Nutzern sofortiges Feedback. Denken Sie daran, dass die serverseitige Validierung für die Sicherheit entscheidend ist.
function handleFiles(fileList) {
filesToUpload = Array.from(fileList)
preview.innerHTML = '' // Clear previous previews
uploadBtn.disabled = true
showError('') // Clear previous errors
const validTypes = ['image/jpeg', 'image/png', 'application/pdf']
const maxSize = 5 * 1024 * 1024 // 5 MB
const validatedFiles = filesToUpload.filter((file) => {
if (!validTypes.includes(file.type)) {
showError(`${file.name}: Invalid file type. Allowed types: JPEG, PNG, PDF.`)
return false
}
if (file.size > maxSize) {
showError(`${file.name}: File is too large. Maximum size is 5 MB.`)
return false
}
addPreview(file)
return true
})
filesToUpload = validatedFiles
uploadBtn.disabled = filesToUpload.length === 0
if (filesToUpload.length > 0 && filesToUpload.length === fileList.length) {
showSuccess(`${filesToUpload.length} file(s) ready for upload.`)
}
}
Inline-Vorschauen anzeigen
Vorschauen helfen Nutzern dabei, ihre Auswahl zu bestätigen:
function addPreview(file) {
const wrapper = document.createElement('div')
wrapper.className = 'file-item'
const fileName = document.createElement('span')
fileName.className = 'file-name'
fileName.textContent = file.name
const fileSize = document.createElement('span')
fileSize.className = 'file-size'
fileSize.textContent = `${(file.size / 1024).toFixed(1)} KB`
if (file.type.startsWith('image/')) {
const img = document.createElement('img')
img.alt = file.name
const reader = new FileReader()
reader.onload = (e) => {
img.src = e.target.result
}
reader.readAsDataURL(file)
wrapper.appendChild(img)
} else {
const extensionIcon = document.createElement('span')
extensionIcon.className = 'file-extension'
extensionIcon.textContent = file.name.split('.').pop().toUpperCase()
wrapper.appendChild(extensionIcon)
}
wrapper.appendChild(fileName)
wrapper.appendChild(fileSize)
preview.appendChild(wrapper)
}
Fetch API oder XMLHttpRequest für Uploads wählen
Die Fetch API ist der moderne Standard für Netzwerkanfragen. Zum Zeitpunkt der Erstellung dieses
Artikels unterstützt sie jedoch keine Events zum Upload-Fortschritt von Haus aus. Für den
Upload-Fortschritt kommt weiterhin häufig XMLHttpRequest (XHR) zum Einsatz, oder Sie greifen zu einer
Bibliothek wie Uppy, die das übernimmt.
// Attach event listener to the upload button
uploadBtn.addEventListener('click', () => {
if (filesToUpload.length === 0) {
showError('Please select valid files to upload.')
return
}
uploadBtn.disabled = true
uploadProgress.value = 0
uploadProgress.hidden = false
// For this demo, we use XMLHttpRequest to show progress.
// The uploadWithFetch function (shown below) is an alternative if progress isn't needed.
uploadWithXHR(filesToUpload)
})
Upload mit der Fetch API (ohne Fortschrittsanzeige)
async function uploadWithFetch(file) {
const formData = new FormData()
formData.append('file', file)
// Hide progress bar as Fetch API doesn't support upload progress
uploadProgress.hidden = true
try {
const response = await fetch('/upload', { method: 'POST', body: formData })
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`)
}
const responseData = await response.text() // Or response.json() if applicable
showSuccess(`${file.name} uploaded successfully. Server: ${responseData}`)
} catch (error) {
showError(`Upload of ${file.name} failed: ${error.message}`)
} finally {
uploadBtn.disabled = filesToUpload.length === 0 // Re-enable if there are still files or based on other logic
}
}
Upload mit XMLHttpRequest (mit Fortschrittsanzeige)
function uploadWithXHR(fileArray) {
const uploadPromises = fileArray.map((file) => {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest()
const formData = new FormData()
formData.append('file', file)
xhr.upload.onprogress = (event) => {
if (event.lengthComputable) {
const percentComplete = (event.loaded / event.total) * 100
uploadProgress.value = percentComplete
// Note: For multiple files, this updates based on the latest event.
// A more complex UI might show individual progress or aggregate total progress.
}
}
xhr.onload = () => {
if (xhr.status >= 200 && xhr.status < 300) {
resolve({ file, statusText: xhr.statusText })
} else {
reject(new Error(`Upload of ${file.name} failed: ${xhr.statusText || xhr.status}`))
}
}
xhr.onerror = () => {
reject(new Error(`Network error during upload of ${file.name}.`))
}
// To allow cancellation, you could store the xhr object and call xhr.abort().
// For example: file.xhr = xhr; then provide a UI to call file.xhr.abort();
xhr.open('POST', '/upload', true)
xhr.send(formData)
})
})
Promise.all(uploadPromises)
.then((results) => {
showSuccess(`${results.length} file(s) uploaded successfully.`)
uploadProgress.hidden = true
filesToUpload = [] // Clear the list after successful upload
preview.innerHTML = '' // Clear previews
})
.catch((error) => {
showError(error.message) // Shows the first error encountered
uploadProgress.hidden = true
})
.finally(() => {
uploadBtn.disabled = filesToUpload.length === 0
})
}
Eine moderne Bibliothek einbinden (Uppy)
Wenn Sie die Komplexität von Datei-Uploads nicht selbst verwalten möchten, bietet sich eine spezialisierte Bibliothek wie Uppy an. Uppy ist ein modularer Open-Source-Dateiuploader, der Oberfläche, Validierung, Retries, fortsetzbare Uploads (über tus) und die Anbindung an verschiedene Back-ends übernimmt.
import { Uppy } from '@uppy/core'
import Dashboard from '@uppy/dashboard'
import XHRUpload from '@uppy/xhr-upload'
// Assuming you have a div with id="uppy-dashboard-container"
// const uppy = new Uppy({
// restrictions: { maxFileSize: 5 * 1024 * 1024, allowedFileTypes: ['image/jpeg', 'image/png', '.pdf'] },
// autoProceed: false
// })
// .use(Dashboard, {
// inline: true,
// target: '#uppy-dashboard-container', // Or your existing drop-zone: '#drop-zone'
// proudlyDisplayPoweredByUppy: false,
// height: 300
// })
// .use(XHRUpload, {
// endpoint: '/upload',
// fieldName: 'file'
// });
// uppy.on('complete', (result) => {
// console.log('Uppy upload complete!', result.successful);
// result.successful.forEach(file => showSuccess(`${file.name} uploaded via Uppy.`));
// result.failed.forEach(file => showError(`Uppy upload of ${file.name} failed.`));
// });
Die Uppy-Nutzung ist auskommentiert, für die Imports müssen jedoch @uppy/core, @uppy/dashboard und
@uppy/xhr-upload installiert sein. Entfernen Sie die Kommentarzeichen und passen Sie die Nutzung an,
wenn Sie die eigene Oberfläche ersetzen, und binden Sie die Dashboard-Styles aus @uppy/dashboard/css/style.min.css ein.
Uppy bietet eine umfangreiche Benutzeroberfläche und deckt viele Sonderfälle von Haus aus ab.
Sicherheit härten
Clientseitige Prüfungen dienen dem Nutzererlebnis; validieren Sie immer auch auf dem Server:
- CSRF-Schutz: Setzen Sie CSRF-Tokens ein und senden Sie sie mit Ihren Upload-Anfragen.
- Serverseitige Validierung: Prüfen Sie Dateitypen, Dateigrößen und Dateiinhalte auf dem Server erneut. Verlassen Sie sich niemals allein auf clientseitige Prüfungen.
- Content-Type-Prüfung: Prüfen Sie auf dem Server den Header
Content-Typeund untersuchen Sie vor allem die tatsächlichen Magic Bytes der Datei, um ihren wahren Typ zu bestimmen. - Dateigrößenlimits: Erzwingen Sie maximale Dateigrößen auf dem Server.
- Sicherer Speicherort: Speichern Sie hochgeladene Dateien außerhalb des Web-Root-Verzeichnisses. Verwenden Sie nicht erratbare, zufällige Dateinamen, um direkten Zugriff oder Enumeration-Angriffe zu verhindern.
- Virenscan: Prüfen Sie alle hochgeladenen Dateien mit einer aktuellen Antiviren-Engine, bevor Sie sie weiterverarbeiten oder zugänglich machen.
Barrierefreiheit sicherstellen
Sorgen Sie dafür, dass Ihr Dateiuploader für alle nutzbar ist:
- Die Drop-Zone ist fokussierbar (
tabindex="0") und reagiert auf die TastenSpace/Enter, um die Dateiauswahl auszulösen. - Statusmeldungen (z. B. Fehler oder Erfolgsmeldungen) werden assistiven Technologien mithilfe von
aria-live="polite"am Container für Statusmeldungen angekündigt. - Bieten Sie deutliche visuelle Fokusindikatoren für die Tastaturnavigation.
- Achten Sie auf ausreichenden Farbkontrast bei Text und UI-Elementen.
Das Widget gestalten
Einfaches CSS verbessert die Optik und das Nutzererlebnis:
.drop-zone {
border: 2px dashed #ccc;
border-radius: 8px;
padding: 25px;
text-align: center;
font-family: Arial, sans-serif;
color: #555;
cursor: pointer;
transition:
border-color 0.3s ease,
background-color 0.3s ease;
background: #f8f9fa;
}
.drop-zone.drag-over {
border-color: #2196f3; /* Blue border when dragging over */
background-color: #e3f2fd; /* Light blue background */
}
.drop-zone:focus {
outline: 2px solid #2196f3;
outline-offset: 2px;
}
#file-preview {
margin-top: 15px;
display: flex;
flex-wrap: wrap;
gap: 10px;
}
.file-item {
width: 120px;
padding: 8px;
border: 1px solid #eee;
border-radius: 4px;
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
font-size: 12px;
word-break: break-all;
}
.file-item img {
max-width: 100px;
max-height: 80px;
margin-bottom: 5px;
object-fit: cover;
}
.file-extension {
display: flex;
align-items: center;
justify-content: center;
width: 80px;
height: 80px;
background: #f1f1f1;
color: #333;
font-size: 18px;
font-weight: bold;
border-radius: 4px;
margin-bottom: 5px;
}
.file-name {
font-weight: bold;
margin-bottom: 3px;
}
.file-size {
color: #777;
}
#upload-progress {
width: 100%;
margin-top: 10px;
}
.status-message {
margin-top: 10px;
font-weight: bold;
}
.status-message.success {
color: #2e7d32; /* Green for success */
}
.status-message.error {
color: #c62828; /* Red for error */
}
#upload-button {
margin-top: 15px;
padding: 10px 15px;
background-color: #007bff;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
transition: background-color 0.3s ease;
}
#upload-button:disabled {
background-color: #ccc;
cursor: not-allowed;
}
#upload-button:hover:not(:disabled) {
background-color: #0056b3;
}
Fazit
Sie haben nun einen funktionsfähigen HTML5-Dateiuploader mit Drag-and-drop, clientseitiger Validierung, Dateivorschauen und Fortschrittsanzeige. Das ist eine solide Grundlage für den Umgang mit Datei-Uploads in modernen Webanwendungen. Denken Sie in Produktionsumgebungen daran, robuste serverseitige Validierung und Sicherheitsmaßnahmen umzusetzen. Um die Möglichkeiten um Funktionen wie fortsetzbare Uploads und fortgeschrittene UI-Komponenten zu erweitern, sollten Sie eine Bibliothek wie Uppy einbinden, die nahtlos mit Diensten wie Transloadit für leistungsstarke Dateiverarbeitung im Back-end zusammenspielt.
