Einen eigenen Dateiuploader mit JavaScript und HTML bauen
Das Hochladen von Dateien ist in vielen Web-Anwendungen eine entscheidende Funktion. Das native Datei-Eingabefeld der Browser bietet jedoch oft nicht die Optik und den erweiterten Funktionsumfang, den moderne Nutzerinnen und Nutzer erwarten. In diesem DevTip zeigen wir, wie Sie mit JavaScript und HTML einen eigenen Dateiuploader bauen, der ein elegantes, robustes und benutzerfreundliches Erlebnis schafft.

Dateiuploader verstehen
Ein Dateiuploader ist eine Komponente, mit der Nutzer Dateien auswählen und auf einen Server hochladen können. Das standardmäßige HTML-Datei-Eingabefeld ist zwar funktional, bietet aber kaum Anpassungsmöglichkeiten oder ein reichhaltiges Nutzererlebnis. Mit einem eigenen Dateiuploader können Sie:
- Ein einheitliches Erscheinungsbild bieten, das zum Design Ihrer Anwendung passt.
- Die Bedienbarkeit mit Funktionen wie Drag-and-drop-Unterstützung verbessern.
- Echtzeit-Feedback mit Fortschrittsanzeigen und Fehlermeldungen geben.
Die HTML-Struktur einrichten
Erstellen Sie zunächst die grundlegende HTML-Struktur für Ihren eigenen Dateiuploader. Das folgende Beispiel zeigt einen anklickbaren Drag-and-drop-Bereich für die Dateiauswahl. Ihr Server muss den Platzhalter für das CSRF-Token durch das Token der aktuellen Sitzung ersetzen, bevor er dieses HTML an den Browser sendet:
<!-- index.html -->
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="csrf-token" content="REPLACE_WITH_SERVER_RENDERED_TOKEN" />
<title>Custom File Uploader</title>
<link rel="stylesheet" href="styles.css" />
</head>
<body>
<div class="file-uploader">
<p>Drag and drop files here or click to upload</p>
<input type="file" id="file-input" multiple />
<div class="upload-progress"></div>
</div>
<script src="script.js"></script>
</body>
</html>
In diesem Aufbau gilt:
- Das div-Element
.file-uploaderdient sowohl als Ablagebereich als auch als anklickbare Fläche. - Das versteckte Element
inputermöglicht die Dateiauswahl. - Das div-Element
.upload-progresszeigt den Upload-Fortschritt für jede Datei an.
Den Dateiuploader mit CSS gestalten
Verbessern Sie die Optik und das interaktive Feedback mit folgendem CSS:
/* styles.css */
.file-uploader {
border: 2px dashed #ccc;
padding: 20px;
text-align: center;
cursor: pointer;
position: relative;
border-radius: 8px;
transition: background-color 0.3s ease;
}
.file-uploader.dragover {
background-color: #f0f0f0;
border-color: #007bff;
}
.file-uploader input[type='file'] {
display: none;
}
.upload-progress {
margin-top: 20px;
}
.progress-bar {
background-color: #007bff;
height: 24px;
margin-bottom: 8px;
color: #fff;
line-height: 24px;
padding: 0 12px;
width: 0%;
transition: width 0.3s ease;
border-radius: 4px;
font-size: 14px;
}
.progress-bar.upload-complete {
background-color: #28a745;
}
.progress-bar.upload-error {
background-color: #dc3545;
}
JavaScript für Dateiauswahl und Upload implementieren
Das JavaScript unten sendet aufeinanderfolgende Chunks mit einem begrenzten Retry-Budget. Es setzt
einen Server voraus, der /upload/chunk und /upload/finalize implementiert; diese Endpunkte sind
hier nicht enthalten. Geben Sie ein CSRF-Token der Sitzung in einem Element meta mit dem Namen
csrf-token aus und prüfen Sie es an beiden Endpunkten. Liefern Sie die Seite über HTTPS aus oder
verwenden Sie während der Entwicklung localhost, damit crypto.randomUUID() verfügbar ist.
Jeder Upload verfügt über eine eigene uploadId. Der Server muss diese ID an den
authentifizierten Nutzer binden, Chunk-Indizes, Anzahl, Gesamtgröße und Inhalte validieren und die
Daten unter servergesteuerten Pfaden speichern. Behandeln Sie fileName als Anzeige-Metadaten,
niemals als Speicherpfad. Ein Wiederholungsversuch für einen Chunk muss denselben
(uploadId, chunkIndex) ersetzen oder bestätigen, ohne Bytes zu duplizieren. Die Finalisierung muss alle
Chunks prüfen und idempotent sein, damit ein erneuter Versuch nach einer verlorenen Antwort keine
doppelten Dateien erzeugen kann. Lassen Sie verwaiste Uploads auf dem Server verfallen.
Dieses Beispiel wiederholt Versuche nur innerhalb der aktuellen Seite. Es implementiert weder einen Abbruch noch eine Wiederherstellung nach einem Neuladen. Für diese Funktionen verwenden Sie einen tus-Client und -Server, die Upload-Identitäten dauerhaft speichern und den akzeptierten Offset aushandeln.
class FileUploader {
constructor() {
this.fileUploader = document.querySelector('.file-uploader')
this.fileInput = document.getElementById('file-input')
this.progressContainer = document.querySelector('.upload-progress')
this.maxFileSize = 10 * 1024 * 1024 // 10MB
this.allowedTypes = ['image/jpeg', 'image/png', 'application/pdf']
this.csrfToken = document.querySelector('meta[name="csrf-token"]')?.content
if (!this.csrfToken) {
throw new Error('The server must provide a CSRF token before uploads can start.')
}
this.retryAttempts = 3
this.chunkSize = 1024 * 1024 // 1MB chunks
this.initializeEventListeners()
}
initializeEventListeners() {
this.fileUploader.addEventListener('click', (event) => {
if (event.target !== this.fileInput) this.fileInput.click()
})
this.fileInput.addEventListener('change', (event) => this.handleFiles(event.target.files))
this.fileUploader.addEventListener('dragover', this.handleDragOver.bind(this))
this.fileUploader.addEventListener('dragleave', this.handleDragLeave.bind(this))
this.fileUploader.addEventListener('drop', this.handleDrop.bind(this))
}
handleDragOver(event) {
event.preventDefault()
this.fileUploader.classList.add('dragover')
}
handleDragLeave() {
this.fileUploader.classList.remove('dragover')
}
handleDrop(event) {
event.preventDefault()
this.fileUploader.classList.remove('dragover')
this.handleFiles(event.dataTransfer.files)
}
async handleFiles(files) {
for (const file of files) {
if (this.validateFile(file)) {
await this.uploadFileWithChunks(file)
}
}
}
validateFile(file) {
if (!this.allowedTypes.includes(file.type)) {
this.showError(`${file.name} is not an allowed file type.`)
return false
}
if (file.size === 0 || file.size > this.maxFileSize) {
this.showError(`${file.name} must be nonempty and no larger than 10MB.`)
return false
}
return true
}
async uploadFileWithChunks(file) {
const uploadId = crypto.randomUUID()
const progressBar = this.createProgressBar(file.name)
const chunks = Math.ceil(file.size / this.chunkSize)
let uploadedChunks = 0
for (let i = 0; i < chunks; i++) {
const start = i * this.chunkSize
const end = Math.min(start + this.chunkSize, file.size)
const chunk = file.slice(start, end)
try {
await this.uploadChunk(chunk, i, chunks, file, uploadId)
uploadedChunks++
const progress = (uploadedChunks / chunks) * 100
this.updateProgress(progressBar, progress)
} catch (error) {
if (await this.handleUploadError(error, progressBar)) {
i-- // Retry the current chunk
} else {
break
}
}
}
if (uploadedChunks === chunks) {
await this.finalizeUpload(file.name, uploadId, progressBar)
}
}
async uploadChunk(chunk, chunkIndex, totalChunks, file, uploadId) {
const formData = new FormData()
formData.append('chunk', chunk)
formData.append('chunkIndex', chunkIndex)
formData.append('totalChunks', totalChunks)
formData.append('fileName', file.name)
formData.append('fileSize', file.size)
formData.append('uploadId', uploadId)
const response = await fetch('/upload/chunk', {
method: 'POST',
headers: {
'X-CSRF-Token': this.csrfToken,
},
body: formData,
})
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`)
}
}
async finalizeUpload(fileName, uploadId, progressBar) {
while (true) {
try {
const response = await fetch('/upload/finalize', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': this.csrfToken,
},
body: JSON.stringify({ uploadId }),
})
if (!response.ok) throw new Error('Failed to finalize upload')
progressBar.classList.add('upload-complete')
progressBar.textContent = `${fileName} uploaded successfully`
return
} catch (error) {
if (!(await this.handleUploadError(error, progressBar))) return
}
}
}
createProgressBar(fileName) {
const progressBar = document.createElement('div')
progressBar.classList.add('progress-bar')
progressBar.dataset.fileName = fileName
progressBar.textContent = `Uploading ${fileName}`
this.progressContainer.appendChild(progressBar)
return progressBar
}
updateProgress(progressBar, percent) {
progressBar.style.width = `${percent}%`
progressBar.textContent = `${progressBar.dataset.fileName} - ${percent.toFixed(1)}%`
}
async handleUploadError(error, progressBar) {
console.error('Upload error:', error)
progressBar.classList.add('upload-error')
if (progressBar.dataset.retryCount === undefined) {
progressBar.dataset.retryCount = '0'
}
const retryCount = parseInt(progressBar.dataset.retryCount)
if (retryCount < this.retryAttempts) {
progressBar.dataset.retryCount = (retryCount + 1).toString()
progressBar.textContent = `Retrying… (${retryCount + 1}/${this.retryAttempts})`
await new Promise((resolve) => setTimeout(resolve, 1000))
progressBar.classList.remove('upload-error')
return true
}
progressBar.textContent = 'Upload failed after multiple attempts'
return false
}
showError(message) {
const errorBar = document.createElement('div')
errorBar.classList.add('progress-bar', 'upload-error')
errorBar.textContent = message
this.progressContainer.appendChild(errorBar)
setTimeout(() => errorBar.remove(), 5000)
}
}
// Initialize the uploader when the DOM is ready
document.addEventListener('DOMContentLoaded', () => new FileUploader())
Den Dateiuploader testen
So sorgen Sie für zuverlässige Leistung:
- Testen Sie mit verschiedenen Dateitypen und -größen.
- Überprüfen Sie die Korrektheit der Chunked-Upload-Funktion.
- Simulieren Sie Netzwerkfehler, um die Retry-Logik zu validieren.
- Bestätigen Sie die korrekte Handhabung des CSRF-Tokens.
- Testen Sie die Drag-and-drop-Funktion in verschiedenen Browsern.
- Prüfen Sie, ob die Fortschrittsanzeigen korrekt aktualisiert werden.
- Stellen Sie die Kompatibilität mit modernen Browsern wie Chrome, Firefox, Edge und Safari sicher.
Fazit
Dieser Client veranschaulicht Chunked Uploads, Retries und den Fortschritt nach jedem akzeptierten Chunk. Das Budget von drei Wiederholungsversuchen gilt pro Datei gemeinsam für Fehler bei Chunks und bei der Finalisierung. Ein funktionierendes Deployment benötigt zusätzlich den oben beschriebenen Server-Vertrag, eine Inhaltsvalidierung, eine Autorisierung und CSRF-Prüfungen.
Für eine produktionsreife Lösung mit zusätzlichen Funktionen wie fortsetzbaren Uploads und Cloud-Storage-Integration sollten Sie Uppy 4.x in Betracht ziehen (siehe den Migrationsleitfaden) – ein leistungsstarker Open-Source-Dateiuploader, der von Transloadit gepflegt wird.
