ohne externe Upload-Bibliothek, mit echtem Fortschritt
Ein Datei-Upload, der nur eine graue Datei-Auswahl-Schaltfläche zeigt, lässt Nutzer bei großen Bildern oder Dokumenten im Ungewissen. Mit FileReader, Drag and Drop und XMLHttpRequest baust du einen Datei-Upload mit sofortiger Bildvorschau, sauberer Validierung und einem Fortschrittsbalken, der echte Upload-Bytes anzeigt statt einer simulierten Animation.
Inhaltsverzeichnis
- 1. Warum ein einfaches File-Input nicht ausreicht
- 2. Grundgerüst: Datei-Upload-Komponente mit x-data
- 3. Bildvorschau mit FileReader ohne Server-Roundtrip
- 4. Drag and Drop: Dateien per Ziehen statt nur per Klick
- 5. Validierung von Dateigröße, Typ und Anzahl
- 6. Echter Fortschrittsbalken mit XMLHttpRequest
- 7. Mehrere Dateien parallel hochladen mit Einzelstatus
- 8. Fehlerbehandlung, Abbruch und erneuter Versuch
- 9. Upload-Ansätze im Vergleich
- 10. Zusammenfassung
- 11. FAQ
1. Warum ein einfaches File-Input nicht ausreicht
Ein natives <input type="file"> zeigt nach der Auswahl nur einen kryptischen Dateinamen an, ohne Vorschau und ohne Rückmeldung während des eigentlichen Hochladens. Bei einem Datei-Upload für Produktbilder, Profilfotos oder Dokumente erwarten Nutzer heute mindestens eine visuelle Bestätigung, was sie ausgewählt haben, und eine sichtbare Fortschrittsanzeige, solange die Datei noch überträgt.
Alpine.js eignet sich gut für einen eigenen Datei-Upload, weil die nötigen Bausteine, FileReader für die Vorschau, Drag-Events für die Ziehfunktion und XMLHttpRequest für echten Fortschritt, alle native Browser-APIs sind. Es braucht keine zusätzliche Upload-Bibliothek, um ein Ergebnis zu bauen, das sich mit kommerziellen Lösungen messen kann.
Der größte Denkfehler bei selbstgebauten Upload-Komponenten ist, den Fortschrittsbalken über setInterval zu simulieren, statt echte Bytes zu messen. Ein simulierter Balken, der bei neunzig Prozent stehen bleibt, während der Server noch verarbeitet, wirkt unfertig und untergräbt das Vertrauen in den Datei-Upload. Die folgenden Abschnitte zeigen, wie man stattdessen echten Fortschritt anzeigt.
2. Grundgerüst: Datei-Upload-Komponente mit x-data
Das Grundgerüst einer Alpine-Komponente für Datei-Upload braucht mindestens vier Zustände: die ausgewählte Datei, eine Vorschau-URL, den Fortschrittswert in Prozent und einen Status-String für lädt, fertig oder Fehler. Diese Trennung erlaubt es der Vorlage, jeden Zustand unabhängig darzustellen, ohne verschachtelte Bedingungen.
Wichtig ist, die Datei selbst nicht direkt in reaktiven Alpine-Daten zu speichern, sondern nur die für die Anzeige relevanten abgeleiteten Werte. Ein File-Objekt lässt sich zwar in x-data ablegen, sollte aber wie ein unveränderliches Referenzobjekt behandelt werden, das nur an FormData und XMLHttpRequest weitergereicht wird.
// Base skeleton for a file upload component with preview and progress
function fileUpload() {
return {
file: null,
previewUrl: null,
progress: 0,
status: 'idle', // idle | reading | uploading | success | error
errorMessage: '',
onFileSelected(event) {
const selected = event.target.files[0];
if (!selected) return;
this.setFile(selected);
},
setFile(selected) {
this.file = selected;
this.status = 'idle';
this.progress = 0;
this.errorMessage = '';
},
reset() {
this.file = null;
this.previewUrl = null;
this.progress = 0;
this.status = 'idle';
this.errorMessage = '';
},
};
}
3. Bildvorschau mit FileReader ohne Server-Roundtrip
Die Bildvorschau eines Datei-Uploads muss nicht auf den Server warten. Die FileReader-API liest eine ausgewählte Datei direkt im Browser als Data-URL ein und liefert sie in Millisekunden zurück, lange bevor überhaupt ein Netzwerk-Request gestartet wurde. Das readAsDataURL()-Ergebnis lässt sich direkt als src-Attribut eines img-Elements binden.
Für sehr große Bilddateien lohnt sich eine Alternative: URL.createObjectURL(file) erzeugt eine Blob-URL, die schneller ist als das Einlesen der kompletten Datei als Base64-String, weil kein Encoding stattfindet. Der Nachteil ist, dass die Blob-URL mit URL.revokeObjectURL() explizit wieder freigegeben werden muss, sobald die Vorschau nicht mehr gebraucht wird, sonst hält der Browser den Speicher unnötig lange belegt.
function fileUpload() {
return {
file: null,
previewUrl: null,
status: 'idle',
setFile(selected) {
this.file = selected;
this.status = 'reading';
// Revoke the previous object URL before creating a new one
if (this.previewUrl) URL.revokeObjectURL(this.previewUrl);
if (selected.type.startsWith('image/')) {
this.previewUrl = URL.createObjectURL(selected);
} else {
this.previewUrl = null; // No preview for non-image files
}
this.status = 'idle';
},
destroy() {
// Cleanup when the component is removed from the DOM
if (this.previewUrl) URL.revokeObjectURL(this.previewUrl);
},
};
}
4. Drag and Drop: Dateien per Ziehen statt nur per Klick
Drag and Drop verbessert die Bedienbarkeit eines Datei-Uploads erheblich, besonders auf Desktop-Geräten. Die Umsetzung braucht drei Ereignisse: dragover muss preventDefault() aufrufen, damit der Browser den Drop überhaupt erlaubt, dragleave setzt den visuellen Hover-Zustand zurück, und drop liest die Dateien aus event.dataTransfer.files aus, genau wie bei einer regulären Dateiauswahl.
Ein häufiger Fehler ist, den dragover-Handler nur auf dem äußersten Container zu registrieren, aber verschachtelte Kindelemente lösen bei jedem Übergang eigene dragenter/dragleave-Ereignisse aus, was zu flackerndem Hover-Zustand führt. Ein Zähler, der bei dragenter erhöht und bei dragleave verringert wird, löst dieses Problem zuverlässig.
function fileUpload() {
return {
file: null,
isDragging: false,
dragCounter: 0,
onDragEnter() {
this.dragCounter++;
this.isDragging = true;
},
onDragLeave() {
this.dragCounter--;
if (this.dragCounter === 0) this.isDragging = false;
},
onDrop(event) {
this.dragCounter = 0;
this.isDragging = false;
const dropped = event.dataTransfer.files[0];
if (dropped) this.setFile(dropped);
},
};
}
5. Validierung von Dateigröße, Typ und Anzahl
Bevor ein Datei-Upload überhaupt gestartet wird, sollte er drei Dinge prüfen: die maximale Dateigröße, den erlaubten MIME-Typ und, bei Mehrfach-Uploads, die maximale Anzahl gleichzeitiger Dateien. Diese Prüfung gehört auf den Client, um Nutzern sofortiges Feedback zu geben, ersetzt aber nicht die serverseitige Validierung, weil file.type im Browser leicht manipulierbar ist und niemals als alleinige Sicherheitsgrenze gelten darf.
Für die Größenprüfung reicht ein Vergleich von file.size in Bytes gegen eine konfigurierte Obergrenze, üblicherweise fünf bis zehn Megabyte für Bilder. Für den Typ ist eine Allowlist aus erlaubten MIME-Typen robuster als eine Blocklist, weil neue, unerwartete Dateitypen sonst standardmäßig durchgelassen würden.
function fileUpload() {
return {
file: null,
errorMessage: '',
maxSizeBytes: 8 * 1024 * 1024, // 8 MB
allowedTypes: ['image/jpeg', 'image/png', 'image/webp'],
setFile(selected) {
const validationError = this.validate(selected);
if (validationError) {
this.errorMessage = validationError;
this.file = null;
return;
}
this.errorMessage = '';
this.file = selected;
},
validate(candidate) {
if (!this.allowedTypes.includes(candidate.type)) {
return `Dateityp ${candidate.type} wird nicht unterstützt`;
}
if (candidate.size > this.maxSizeBytes) {
const maxMb = (this.maxSizeBytes / 1024 / 1024).toFixed(0);
return `Datei ist größer als ${maxMb} MB`;
}
return null;
},
};
}
6. Echter Fortschrittsbalken mit XMLHttpRequest
Die moderne fetch-API bietet keinen eingebauten Upload-Fortschritt, weil ihr Streaming-Modell dafür keinen Standard-Hook vorsieht. Für einen Datei-Upload mit echtem Fortschrittsbalken bleibt XMLHttpRequest deshalb weiterhin die richtige Wahl, konkret über das upload.onprogress-Ereignis, das loaded und total in Bytes liefert.
Der Fortschritt wird als Prozentsatz berechnet und direkt an eine reaktive Alpine-Eigenschaft gebunden, die die Breite eines Balkenelements per :style steuert. Da onprogress sehr häufig feuert, reicht die native Häufigkeit meist aus, ohne dass ein zusätzlicher Debounce nötig wäre, weil Alpines Reaktivitätssystem die DOM-Updates ohnehin bündelt.
function fileUpload() {
return {
file: null,
progress: 0,
status: 'idle',
errorMessage: '',
upload() {
if (!this.file) return;
this.status = 'uploading';
this.progress = 0;
const formData = new FormData();
formData.append('file', this.file);
const xhr = new XMLHttpRequest();
xhr.open('POST', '/api/upload');
// Real byte-level progress, not a simulated animation
xhr.upload.addEventListener('progress', (event) => {
if (event.lengthComputable) {
this.progress = Math.round((event.loaded / event.total) * 100);
}
});
xhr.addEventListener('load', () => {
if (xhr.status >= 200 && xhr.status < 300) {
this.status = 'success';
this.progress = 100;
} else {
this.status = 'error';
this.errorMessage = 'Upload fehlgeschlagen';
}
});
xhr.addEventListener('error', () => {
this.status = 'error';
this.errorMessage = 'Netzwerkfehler beim Upload';
});
xhr.send(formData);
},
};
}
7. Mehrere Dateien parallel hochladen mit Einzelstatus
Bei mehreren Dateien braucht jeder Datei-Upload-Eintrag seinen eigenen Fortschritt, Status und Fehlerzustand, unabhängig von den übrigen Dateien. Ein Array von Upload-Objekten, jedes mit eigener progress- und status-Eigenschaft, erfüllt diese Anforderung. Die Vorlage iteriert dann mit x-for über dieses Array und zeigt für jede Datei einen eigenen Fortschrittsbalken.
Ob Uploads parallel oder nacheinander gestartet werden, hängt von der Serverkapazität ab. Für die meisten Anwendungsfälle reicht eine Begrenzung auf drei bis vier gleichzeitige Uploads, mit einer Warteschlange für die restlichen Dateien, ähnlich dem Muster begrenzter Parallelität bei Hintergrundprozessen.
8. Fehlerbehandlung, Abbruch und erneuter Versuch
Ein robuster Datei-Upload muss drei Fehlerszenarien abdecken: der Nutzer bricht den Upload manuell ab, die Verbindung bricht ab, oder der Server lehnt die Datei aus fachlichen Gründen ab. Für den manuellen Abbruch reicht xhr.abort(), gebunden an einen Abbrechen-Button, der während des Uploads sichtbar wird.
Nach einem fehlgeschlagenen Upload sollte ein Wiederholen-Button erscheinen, der denselben Request erneut mit derselben Datei startet, ohne dass der Nutzer die Datei ein zweites Mal auswählen muss. Das setzt voraus, dass das File-Objekt auch nach einem Fehler in der Komponente erhalten bleibt und nicht vorzeitig verworfen wird.
9. Upload-Ansätze im Vergleich
Die folgende Tabelle vergleicht typische Entscheidungen beim Bau eines eigenen Datei-Uploads in Alpine.js.
| Aspekt | Unzureichend | Empfohlener Datei-Upload | Vorteil |
|---|---|---|---|
| Fortschritt | setInterval-Simulation | xhr.upload.onprogress | echte Bytes, kein Stocken bei 90 Prozent |
| Vorschau | Server-Roundtrip nötig | FileReader oder Object URL | sofortige Anzeige im Browser |
| Validierung | nur serverseitig geprüft | Client-Check plus Server-Check | sofortiges Feedback, sichere Grenze |
| Drag-Hover | flackert bei verschachtelten Kindern | Zähler für dragenter/dragleave | stabiler visueller Zustand |
| Abbruch | kein Weg, Upload zu stoppen | xhr.abort() an Abbrechen-Button | Kontrolle für den Nutzer |
Dieses Muster funktioniert unabhängig davon, ob der Datei-Upload für einzelne Profilbilder oder für Mehrfach-Uploads in einem Dokumentenverwaltungssystem eingesetzt wird. Die Kernbausteine, Vorschau, Validierung und echter Fortschritt, bleiben identisch.
Mironsoft
Alpine.js Upload-Komponenten und Frontend-Entwicklung
Datei-Upload, der wie ein Fremdkörper wirkt?
Wir bauen euch einen Datei-Upload mit Vorschau, Drag and Drop und echtem Fortschrittsbalken, der sich nahtlos in euer bestehendes Alpine.js-Frontend einfügt.
Upload-Komponente
Vorschau, Drag and Drop und Validierung nach Maß
Fortschrittsanzeige
Echter XMLHttpRequest-Fortschritt statt simulierter Balken
Server-Integration
Serverseitige Validierung für Magento und eigene APIs
10. Zusammenfassung
Ein durchdachter Datei-Upload in Alpine.js kombiniert vier Bausteine: eine sofortige Bildvorschau über FileReader oder Object URLs, Drag and Drop mit stabilem Hover-Zustand über einen Zähler, clientseitige Validierung von Größe und Typ als Ergänzung zur serverseitigen Prüfung, und einen echten Fortschrittsbalken über XMLHttpRequest statt einer simulierten Animation.
Diese Bausteine benötigen keine externe Upload-Bibliothek, weil alle nötigen Fähigkeiten native Browser-APIs sind. Bei mehreren gleichzeitigen Dateien braucht jeder Datei-Upload-Eintrag seinen eigenen Zustand, damit ein fehlgeschlagener Upload die übrigen Dateien nicht blockiert. Wer diese Struktur konsequent umsetzt, bekommt ein Upload-Erlebnis, das sich mit kommerziellen Lösungen messen kann.
Datei-Upload mit Vorschau und Fortschritt — Das Wichtigste auf einen Blick
Vorschau
FileReader oder URL.createObjectURL für sofortige Bildanzeige ohne Server-Roundtrip.
Fortschritt
xhr.upload.onprogress liefert echte Bytes, fetch bietet dafür keinen Standard-Hook.
Validierung
Größe und MIME-Typ clientseitig prüfen, serverseitig zwingend erneut absichern.
Drag and Drop
Zähler für dragenter/dragleave verhindert flackernden Hover-Zustand bei Kindelementen.