Wiederaufnehmbarer Dateiupload
Wiederaufnehmbare Dateiuploads in JavaScript: Chunks, Fortsetzung nach Unterbrechung, Node.js-Server, resumable.js und File.slice + fetch.
Ein 2-GB-Video über eine unzuverlässige Mobilfunkverbindung mit einer einzigen fetch-Anfrage hochzuladen ist fehleranfällig: eine unterbrochene Verbindung bei 95 % und der Nutzer muss von vorne anfangen. Wiederaufnehmbare Dateiuploads lösen dieses Problem, indem die Datei in kleine Teile aufgeteilt wird, die einzeln hochgeladen werden, wobei gespeichert wird, welche Teile bereits angekommen sind — so kann ein unterbrochener Upload genau dort fortgesetzt werden, wo er aufgehört hat.
Diese Seite behandelt das vollständige Bild: wie fragmentierte, wiederaufnehmbare Uploads konzeptionell funktionieren, einen funktionierenden Node.js- und Express-Server, der Chunks speichert und zusammensetzt, einen Client mit der resumable.js-Bibliothek sowie eine abhängigkeitsfreie native Version mit File.slice und fetch. Sie werden auch den häufigen Fehler beim Zusammensetzen kennenlernen und Tipps zur Produktionshärtung erhalten.
Wie wiederaufnehmbare Uploads funktionieren
Der Kerngedanke ist einfach und beruht auf drei zusammenwirkenden Komponenten:
- Datei in Chunks aufteilen. Der Browser teilt die ausgewählte Datei mithilfe der
Blob.slice-Methode, dieFileerbt, in Stücke fester Größe auf (z. B. je 1 MB). Die Datei selbst wird dabei nie vollständig in den Speicher geladen. - Chunks einzeln (oder in kleinen Gruppen) hochladen. Jeder Chunk ist eine separate HTTP-Anfrage, die seinen Index (
Chunk 3 von 17), die Gesamtanzahl der Chunks, den Dateinamen und eine stabile Kennung enthält, die diese Upload-Sitzung eindeutig identifiziert. - Auf dem Server zusammensetzen. Der Server speichert jeden Chunk auf der Festplatte, geordnet nach Index. Sobald alle Chunks angekommen sind, werden sie in der richtigen Reihenfolge zur endgültigen Datei zusammengefügt.
Die Wiederaufnahme ergibt sich aus Schritt 3 zusammen mit einem Prüfen-vor-Senden-Schritt auf dem Client. Bevor ein Chunk hochgeladen wird, fragt der Client den Server: „Hast du Chunk N bereits?" (typischerweise per HTTP-HEAD-Anfrage). Falls ja, wird dieser Chunk übersprungen. Nach einem Absturz oder Neuladen scannt der Client die Datei erneut und sendet nur die fehlenden Teile nach. Die stabile Kennung ermöglicht es dem Server, einen zurückkehrenden, halb fertigen Upload wiederzuerkennen.
File (2.5 MB)
└─ slice into 1 MB chunks ──► [chunk 1] [chunk 2] [chunk 3 (0.5 MB)]
│ │ │
HEAD /upload?chunk=N (already there? skip : send)
▼ ▼ ▼
POST /upload (one request per missing chunk)
└────────┬─────────┘
server saves chunk-N.bin, then concatenates in orderVorteile wiederaufnehmbarer Dateiuploads
- Verbesserte Nutzererfahrung: Nutzer können Uploads fortsetzen, ohne von vorne anfangen zu müssen.
- Effizienz: Nach einem Fehler werden nur die fehlenden Teile übertragen, nicht die gesamte Datei.
- Zuverlässigkeit bei schlechten Netzwerken: Netzwerkunterbrechungen werden problemlos behandelt, was bei großen Dateien und Mobilverbindungen besonders wichtig ist.
- Geringerer Speicherdruck: Die Arbeit mit kleinen Slices vermeidet das Puffern einer mehrere Gigabyte großen Datei im Speicher.
Wiederaufnehmbare Dateiuploads in JavaScript implementieren
Einrichten der Umgebung
Bevor Sie mit der Implementierung beginnen, stellen Sie sicher, dass Sie die folgenden Tools und Bibliotheken zur Verfügung haben:
- Einen modernen Webbrowser mit JavaScript-Unterstützung.
- Einen Server, der Dateiuploads verarbeiten kann.
- Die
resumable.js-Bibliothek (oder eine ähnliche Bibliothek) zur Verwaltung der clientseitigen Logik.
Installieren Sie die erforderlichen Node.js-Abhängigkeiten:
npm install express corsServerseitige Konfiguration
Konfigurieren Sie zunächst Ihren Server, um Datei-Chunks zu verarbeiten und Metadaten über die hochgeladenen Dateien zu speichern. Hier ist ein Beispiel mit Node.js und Express. Beachten Sie, dass resumable.js Chunk-Metadaten standardmäßig im Query-String sendet, daher lesen wir aus req.query und verwenden ein temporäres Verzeichnis pro Datei, um die ungeordnete Chunk-Ankunft sicher zu handhaben.
const express = require('express');
const cors = require('cors');
const fs = require('fs');
const path = require('path');
const app = express();
const port = 3000;
app.use(cors());
// Handle chunk verification for testChunks: true
app.head('/upload', (req, res) => {
res.set('Access-Control-Allow-Origin', '*');
const chunkNumber = parseInt(req.query.resumableChunkNumber);
const identifier = req.query.resumableIdentifier;
const chunkPath = path.join('uploads', identifier, `chunk-${chunkNumber}.bin`);
fs.promises.access(chunkPath)
.then(() => res.status(200).end())
.catch(() => res.status(404).end());
});
app.post('/upload', async (req, res) => {
try {
const chunkNumber = parseInt(req.query.resumableChunkNumber);
const totalChunks = parseInt(req.query.resumableTotalChunks);
const identifier = req.query.resumableIdentifier;
const fileName = req.query.resumableFilename;
const chunkDir = path.join('uploads', identifier);
await fs.promises.mkdir(chunkDir, { recursive: true });
// Read raw body (resumable.js sends chunks as application/octet-stream)
const buffer = await new Promise((resolve, reject) => {
const chunks = [];
req.on('data', chunk => chunks.push(chunk));
req.on('end', () => resolve(Buffer.concat(chunks)));
req.on('error', reject);
});
const chunkPath = path.join(chunkDir, `chunk-${chunkNumber}.bin`);
await fs.promises.writeFile(chunkPath, buffer);
const receivedChunks = (await fs.promises.readdir(chunkDir)).length;
if (receivedChunks === totalChunks) {
// Concatenate chunks IN ORDER, one at a time (see warning below).
const finalPath = path.join('uploads', fileName);
await fs.promises.writeFile(finalPath, ''); // start with an empty file
for (let i = 1; i <= totalChunks; i++) {
const data = await fs.promises.readFile(
path.join(chunkDir, `chunk-${i}.bin`)
);
await fs.promises.appendFile(finalPath, data);
}
await fs.promises.rm(chunkDir, { recursive: true, force: true });
res.status(200).send('File uploaded successfully');
} else {
// resumable.js expects a 200 OK for successful chunk uploads
res.status(200).send('Chunk uploaded successfully');
}
} catch (error) {
console.error('Upload error:', error);
res.status(500).send('Server error during upload');
}
});
app.listen(port, () => {
console.log(`Server running at http://localhost:${port}`);
});Chunks sequenziell, nicht gleichzeitig zusammensetzen. Ein häufiger Fehler besteht darin, den Lese-Stream jedes Chunks gleichzeitig in einen einzigen Schreib-Stream zu leiten (fs.createReadStream(...).pipe(writeStream) innerhalb einer Schleife). Die Streams konkurrieren miteinander, sodass die Bytes in der falschen Reihenfolge vermischt werden und der erste Stream, der fertig ist, den Schreib-Stream vorzeitig schließt — was zu einer beschädigten Datei führt. Lesen und anhängen Sie jeweils einen Chunk nach dem anderen, wie oben gezeigt.
Clientseitige Implementierung
Jetzt implementieren wir die clientseitige Logik mit JavaScript und der resumable.js-Bibliothek. Stellen Sie sicher, dass Sie die resumable.js-Bibliothek in Ihr Projekt einbinden. Wir verwenden v2.1.0 für moderne Kompatibilität. Für Produktionsumgebungen sollten Sie das standardisierte tus-Protokoll oder natives File.slice mit fetch für bessere Kontrolle und plattformübergreifende Unterstützung in Betracht ziehen.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Resumable File Upload</title>
</head>
<body>
<input type="file" id="fileInput" />
<button id="uploadButton">Upload</button>
<p id="progress">Ready</p>
<script src="https://unpkg.com/[email protected]/resumable.min.js"></script>
<script>
const fileInput = document.getElementById('fileInput');
const uploadButton = document.getElementById('uploadButton');
const progressEl = document.getElementById('progress');
const r = new Resumable({
target: '/upload',
chunkSize: 1 * 1024 * 1024, // 1MB chunks
simultaneousUploads: 1,
testChunks: true,
throttleProgressCallbacks: 1,
});
r.assignBrowse(fileInput);
uploadButton.addEventListener('click', () => {
if (r.files.length > 0) {
r.upload();
} else {
alert('Please select a file to upload.');
}
});
r.on('progress', (file, loaded, total) => {
const percent = Math.round((loaded / total) * 100);
progressEl.textContent = `Uploading ${file.fileName}: ${percent}%`;
});
r.on('fileSuccess', (file, message) => {
console.log(`File ${file.fileName} uploaded successfully.`);
progressEl.textContent = 'Upload complete!';
});
r.on('fileError', (file, message) => {
console.error(`Error uploading file ${file.fileName}: ${message}`);
progressEl.textContent = 'Upload failed.';
});
</script>
</body>
</html>Native Alternative: File.slice + fetch
Für Projekte, die keine externen Abhängigkeiten bevorzugen, können Sie wiederaufnehmbare Uploads nativ mit der File.slice-Methode und fetch implementieren. Dadurch erhalten Sie volle Kontrolle über Header, Wiederholungsversuche und — entscheidend — die Wiederaufnahme-Logik. Die folgende Funktion erstellt den Query-String für jeden Chunk, fragt den Server per HEAD-Anfrage, ob der Chunk bereits vorhanden ist, und lädt nur die fehlenden hoch. Ein erneuter Aufruf nach einer Unterbrechung überspringt alles, was bereits erfolgreich übertragen wurde:
async function uploadFileNative(file) {
const chunkSize = 1 * 1024 * 1024; // 1MB
const totalChunks = Math.ceil(file.size / chunkSize);
// A stable identifier so a re-run resumes the same upload session.
const identifier = `${file.name}-${file.size}`;
for (let i = 0; i < totalChunks; i++) {
const params = new URLSearchParams({
resumableChunkNumber: i + 1,
resumableTotalChunks: totalChunks,
resumableIdentifier: identifier,
resumableFilename: file.name,
});
const url = `/upload?${params}`;
// Resume support: skip chunks the server already has.
const probe = await fetch(url, { method: 'HEAD' });
if (probe.status === 200) continue;
const start = i * chunkSize;
const end = Math.min(start + chunkSize, file.size);
const chunk = file.slice(start, end); // a Blob, sent as the request body
await fetch(url, { method: 'POST', body: chunk });
}
console.log('Native upload complete');
}Um diesen Code produktionsreif zu machen, würden Sie jeden POST-Aufruf in einer Wiederholungsschleife mit exponentiellem Backoff kapseln und die Abbruchmöglichkeit mit einem AbortController unterstützen.
Metadaten verwalten
Es ist entscheidend, Metadaten über die hochgeladene Datei und ihre Chunks zu verwalten — den Chunk-Index, die Gesamtanzahl, den Dateinamen und die stabile Kennung. Diese Informationen ermöglichen es dem Server, einen Upload nach einer Unterbrechung ab dem richtigen Chunk fortzusetzen. Die Serverlogik zum Verfolgen und Zusammensetzen von Chunks wurde im vorherigen Abschnitt behandelt.
Verlassen Sie sich für die Produktion nicht allein auf das Dateisystem, um den Fortschritt zu verfolgen: Es bietet keine Persistenzgarantien und ist nicht sicher, wenn mehrere Chunks gleichzeitig eintreffen (die readdir-Längenprüfung kann zu Race Conditions führen). Verwenden Sie eine Datenbank oder einen Cache (wie Redis), um festzuhalten, welche Chunks abgeschlossen wurden, und setzen Sie die Datei erst zusammen, wenn jeder Index bestätigt ist. Wenn Sie zusätzliche strukturierte Metadaten zusammen mit einem Chunk senden müssen, ermöglicht die FormData-API das Bündeln von Feldern und dem binären Blob in einer einzigen Anfrage.
Beispiel: Große Dateien hochladen
Die Client-Konfiguration bleibt identisch mit dem vorherigen Beispiel. Um große Dateien zu optimieren, können Sie die chunkSize erhöhen (z. B. auf 5 MB) und simultaneousUploads basierend auf der Kapazität Ihres Servers und den Netzwerkbedingungen anpassen.
Professionelle Tipps für wiederaufnehmbare Dateiuploads
- Chunk-Größe optimieren: Passen Sie die Chunk-Größe basierend auf der durchschnittlichen Netzwerkgeschwindigkeit und Dateigröße an, um eine Balance zwischen Upload-Geschwindigkeit und Zuverlässigkeit zu finden.
- Fehlerbehandlung: Implementieren Sie robuste Fehlerbehandlungsmechanismen für Netzwerkunterbrechungen und Serverprobleme.
- Nutzerfeedback: Geben Sie Nutzern in Echtzeit Rückmeldung über den Upload-Fortschritt und aufgetretene Probleme.
- Sicherheit: Stellen Sie sicher, dass der Dateiupload-Prozess sicher ist, indem Sie Dateitypen validieren und eine ordnungsgemäße Authentifizierung und Autorisierung implementieren.
- Moderne Alternativen: Erwägen Sie für Produktionsumgebungen standardisierte Protokolle wie
tusoder nativesFile.slicemitfetchfür bessere Kontrolle, Wiederaufnahme-Fähigkeit und plattformübergreifende Kompatibilität.
Wenn Sie diese Richtlinien und Beispiele befolgen, können Sie ein robustes, effizientes wiederaufnehmbares Dateiupload-System in JavaScript implementieren — eines, das instabile Netzwerke übersteht und Nutzern die Gewissheit gibt, dass ein großer Upload nicht vergebens war.
Verwandte Themen
- Fetch API — die moderne Möglichkeit, jeden Chunk an den Server zu senden.
- Fetch: Download-Fortschritt — einen gestreamten Antwort-Body lesen, um den Fortschritt zu melden.
- Fetch: Abbruch — einen laufenden Upload mit
AbortControllerabbrechen. - Blob — der von
File.slicezurückgegebene Typ, der jeden Chunk repräsentiert. - File und FileReader — die vom Nutzer ausgewählte Datei lesen.
- FormData — Binärdaten mit zusätzlichen Feldern in einer Anfrage bündeln.