Fortsetzbare Datei-Uploads in Angular
In heutigen Webanwendungen können unzuverlässige Netzwerke und große Dateien bei Datei-Uploads zu frustrierenden Nutzererlebnissen führen. Fortsetzbare Uploads ermöglichen es Ihren Nutzern, einen Upload nach einer Unterbrechung genau dort fortzusetzen, wo er abgebrochen wurde. In diesem Beitrag zeigen wir Ihnen, wie Sie tus-js-client in Ihre Angular-App integrieren, um robuste, fortsetzbare Datei-Uploads zu ermöglichen.
Warum fortsetzbare Uploads?
Fortsetzbare Uploads lösen häufige Probleme mit großen Dateien und instabilen Netzwerken. Indem Sie eine Datei in handhabbare Chunks aufteilen und fehlgeschlagene Segmente erneut übertragen lassen, sorgen Sie für ein zuverlässigeres Nutzererlebnis. Das standardisierte tus-Protokoll, das tus-js-client implementiert, macht diesen Prozess leichter handhabbar und skalierbar.
Ihr Angular-Projekt einrichten
Erstellen Sie zunächst mit der Angular CLI ein neues Angular-Projekt mit Standalone-Komponenten:
npm install -g @angular/cli
ng new resumable-upload-demo --standalone
cd resumable-upload-demo
Installieren Sie das tus-js-client-Paket mit TypeScript-Typen:
npm install tus-js-client@4.3.1
Einen Upload-Service erstellen
Erstellen wir einen Angular-Service, der Uploads mithilfe von tus-js-client abwickelt. Dieser
Service enthält passende TypeScript-Typen, begrenzte Retries und das Nachschlagen zuvor
gespeicherter Upload-URLs. Speichern Sie ihn als
upload.service.ts. Der Typ UploadOptions ist im gepinnten Paket 4.3.1 verfügbar.
import { Injectable } from '@angular/core'
import { DetailedError, Upload, type UploadOptions } from 'tus-js-client'
export interface UploadProgress {
bytesUploaded: number
bytesTotal: number
percentage: number
}
@Injectable({
providedIn: 'root',
})
export class UploadService {
#upload: Upload | null = null
#stopping = false
async startUpload(
file: File,
endpoint: string,
onProgress: (progress: UploadProgress) => void,
onSuccess: () => void,
onError: (error: Error) => void,
headers: Record<string, string> = {},
): Promise<void> {
if (this.#upload) {
onError(new Error('Stop the current upload before starting another.'))
return
}
const options: UploadOptions = {
endpoint,
headers,
retryDelays: [0, 1000, 3000, 5000],
removeFingerprintOnSuccess: true,
metadata: {
filename: file.name,
filetype: file.type,
},
onError: (error) => {
if (this.#upload !== upload || this.#stopping) return
this.#upload = null
const status = error instanceof DetailedError ? error.originalResponse?.getStatus() : null
const message = status === 401 || status === 403
? 'Upload authorization failed. Sign in again before retrying.'
: 'Upload stopped. Check the connection, then select Upload to retry.'
onError(new Error(message, { cause: error }))
},
onProgress: (bytesUploaded: number, bytesTotal: number) => {
if (this.#upload !== upload || this.#stopping) return
onProgress({
bytesUploaded,
bytesTotal,
percentage: bytesTotal > 0 ? (bytesUploaded / bytesTotal) * 100 : 0,
})
},
onSuccess: () => {
if (this.#upload !== upload || this.#stopping) return
this.#upload = null
onSuccess()
},
}
const upload = new Upload(file, options)
this.#upload = upload
try {
const previousUploads = await upload.findPreviousUploads()
if (this.#upload !== upload || this.#stopping) return
if (previousUploads[0]) upload.resumeFromPreviousUpload(previousUploads[0])
upload.start()
} catch (error) {
if (this.#upload !== upload || this.#stopping) return
this.#upload = null
onError(new Error('Unable to prepare the upload. Please try again.', { cause: error }))
}
}
async abortUpload(): Promise<void> {
const upload = this.#upload
if (!upload) return
this.#stopping = true
await upload.abort()
this.#upload = null
this.#stopping = false
}
}
Die tus-js-client API beschreibt die Werte von
DetailedError sowie die Retry-Richtlinie. onError meldet einen Fehler, der nicht weiter wiederholt wird;
es ist keine Zusage, dass die Wiederherstellung automatisch fortgesetzt wird. Das Delay-Array
begrenzt aufeinanderfolgende Retries, und Fortschritt kann dieses Budget zurücksetzen.
abort() gibt ein Promise zurück und pausiert den Upload,
ohne dessen Server-Ressource zu löschen. Ein abgelehnter Abbruch wird an den Aufrufer
weitergegeben, und weitere Starts bleiben blockiert, bis das Stoppen erfolgreich ist.
Nach einem Neuladen muss der Benutzer dieselbe Datei erneut auswählen. Der Service sucht die
gespeicherte Upload-URL und setzt sie vor dem Start fort. Dies setzt voraus, dass der URL-Speicher
des Browsers verfügbar ist, dass der Upload nicht abgelaufen ist und dass der Server diesen Benutzer
bei jeder Anfrage autorisiert. Dieses Beispiel wählt den ersten Treffer; Anwendungen mit mehreren
Treffern oder Konten sollten eine Auswahl anbieten und gespeicherte Einträge auf das aktive Konto
beschränken. Belassen Sie chunkSize beim Standardwert, sofern der Server oder Proxy keine
Begrenzung der Anfragegröße erfordert.
Die Upload-Komponente erstellen
Speichern Sie diese Standalone-Komponente als upload.component.ts, importieren Sie sie in
imports der Root-Komponente und fügen Sie <app-upload /> dem Root-Template hinzu. Konfigurieren Sie /files/ als Ihren eigenen tus-Endpunkt.
Binden Sie bei einem geschützten Endpunkt das Zugriffstoken der aktuellen Sitzung an [authToken]; hinterlegen
Sie keine Zugangsdaten fest im Code. Diese Komponente stellt eine eigene Service-Instanz bereit,
damit getrennte Upload-Widgets sich keine aktive Übertragung teilen.
import { CommonModule } from '@angular/common'
import { ChangeDetectorRef, Component, Input, type OnDestroy } from '@angular/core'
import { UploadService, type UploadProgress } from './upload.service'
@Component({
selector: 'app-upload',
standalone: true,
imports: [CommonModule],
providers: [UploadService],
template: `
<div class="upload-container">
<input
type="file"
[disabled]="isUploading"
(change)="onFileSelected($event)"
[attr.aria-label]="'Choose file to upload'"
/>
<button
(click)="startUpload()"
[disabled]="!selectedFile || isUploading"
class="upload-button"
>
{{ isUploading ? 'Uploading...' : 'Upload' }}
</button>
<button (click)="stopUpload()" [disabled]="!isUploading || isStopping">Stop</button>
<div *ngIf="progress" class="progress-container">
<div class="progress-bar" [style.width.%]="progress.percentage">
{{ progress.percentage | number: '1.0-0' }}%
</div>
</div>
<div *ngIf="message" [class]="messageType" role="alert">
{{ message }}
</div>
</div>
`,
styles: [
`
.upload-container {
padding: 1rem;
}
.progress-container {
margin-top: 1rem;
background: #f0f0f0;
border-radius: 4px;
}
.progress-bar {
height: 20px;
background: #4caf50;
border-radius: 4px;
text-align: center;
color: white;
transition: width 0.3s ease;
}
.error {
color: #d32f2f;
margin-top: 1rem;
}
.success {
color: #388e3c;
margin-top: 1rem;
}
`,
],
})
export class UploadComponent implements OnDestroy {
@Input() authToken: string | null = null
selectedFile: File | null = null
progress: UploadProgress | null = null
message = ''
messageType = ''
isUploading = false
isStopping = false
constructor(private uploadService: UploadService, private changeDetector: ChangeDetectorRef) {}
onFileSelected(event: Event): void {
const input = event.target
if (input instanceof HTMLInputElement) {
this.selectedFile = input.files?.[0] ?? null
this.progress = null
this.message = ''
this.messageType = ''
}
}
async startUpload(): Promise<void> {
if (!this.selectedFile || this.isUploading) return
this.isUploading = true
this.message = ''
this.progress = null
const uploadEndpoint = '/files/'
await this.uploadService.startUpload(
this.selectedFile,
uploadEndpoint,
(progress) => {
this.progress = progress
this.changeDetector.markForCheck()
},
() => {
this.message = 'Upload completed successfully!'
this.messageType = 'success'
this.isUploading = false
this.changeDetector.markForCheck()
},
(error: Error) => {
this.message = error.message
this.messageType = 'error'
this.isUploading = false
this.changeDetector.markForCheck()
},
this.authToken ? { Authorization: `Bearer ${this.authToken}` } : {},
)
}
async stopUpload(): Promise<void> {
this.isStopping = true
try {
await this.uploadService.abortUpload()
this.isUploading = false
this.message = 'Upload stopped. Select Upload to resume.'
this.messageType = ''
} catch {
this.message = 'Unable to stop the upload. Please try stopping it again.'
this.messageType = 'error'
} finally {
this.isStopping = false
this.changeDetector.markForCheck()
}
}
ngOnDestroy(): void {
void this.uploadService.abortUpload().catch(() => {
console.error('Unable to stop the upload during cleanup.')
})
}
}
CORS-Konfiguration
Stellen Sie sicher, dass Ihr tus-Server für einen reibungslosen Betrieb die folgenden CORS-Header sendet:
Access-Control-Allow-Origin: <your-domain>
Access-Control-Allow-Methods: POST, PATCH, HEAD, OPTIONS
Access-Control-Allow-Headers: Authorization, Content-Type, Tus-Resumable, Upload-Length, Upload-Metadata, Upload-Offset
Access-Control-Expose-Headers: Upload-Offset, Location, Upload-Length, Tus-Version, Tus-Resumable, Tus-Max-Size, Tus-Extension, Upload-Metadata
Bewährte Verfahren und Überlegungen
-
Authentifizierung: tus-js-client verwendet einen eigenen XHR-Transport im Browser, sodass
HttpInterceptorvon Angular diese Anfragen nicht sieht. Der Service übergibt die Autorisierung über die Optionheadersvon tus. Holen Sie bei ablaufenden Tokens für jede Anfrage ein aktuelles Token über den dokumentierten HookonBeforeRequestund beschränken Sie Upload-URLs auf Ihren vertrauenswürdigen Server. -
Dateivalidierung: Fügen Sie diese Methode in
UploadComponentein und rufen Sie sie instartUpload()auf, bevor SieisUploadingsetzen:if (!this.validateFile(this.selectedFile)) return. Prüfungen im Browser liefern Rückmeldung; der Server muss die Größe durchsetzen und den tatsächlichen Inhalt prüfen.
validateFile(file: File): boolean {
const maxSize = 100 * 1024 * 1024 // 100 MiB
const allowedTypes = ['image/jpeg', 'image/png', 'application/pdf']
if (file.size > maxSize) {
this.message = 'File size exceeds 100 MiB limit'
return false
}
if (!allowedTypes.includes(file.type)) {
this.message = 'Invalid file type'
return false
}
return true
}
- Fehlerbehebung: Implementieren Sie eine automatische Retry-Logik für Netzwerkprobleme
- Fortschrittsüberwachung: Verwenden Sie RxJS-Subjects, um den Upload-Fortschritt an andere Komponenten zu senden
- Cleanup: Behandeln Sie die Zerstörung von Komponenten sauber und brechen Sie laufende Uploads ab
Fazit
Fortsetzbare Uploads in Angular mit tus-js-client bieten eine robuste Lösung für den Umgang mit großen Dateien und Netzwerkunterbrechungen. Die Kombination aus TypeScript-Typen, sauberer Fehlerbehandlung und bewährten Verfahren sorgt für ein zuverlässiges Upload-Erlebnis für Ihre Nutzer.
Wenn Sie eine umfassendere Lösung für den Umgang mit Dateien benötigen, sehen Sie sich die API von Transloadit an, die fortschrittliche Funktionen für die Verarbeitung und Transformation von Dateien bietet.
