Dateien in Angular mit Fortschritt und lokalem Server hochladen
Senden Sie eine ausgewählte Datei als FormData mit
HttpClient von Angular und zeigen Sie den Erfolg erst an, wenn der Server sie
akzeptiert. Diese Anleitung erstellt einen Dateiuploader für einzelne Dateien mit Fortschrittsanzeige,
sichtbaren Fehlern und einem ausführbaren Node.js-Empfänger. Der Empfänger prüft die Anfrage und gibt
eine Prüfsumme zurück; er speichert die Datei nicht.
Erforderliche Einrichtung
Verwenden Sie Node.js 26.8.2, Corepack mit Yarn 4.12.0 und einen modernen Browser. Das Beispiel legt Angular 22.1.7, Angular CLI/Build 22.1.8 und TypeScript 6.0.3 als Versionen fest. Angular 22 ist eine unterstützte Version; die zugehörige Kompatibilitätstabelle erläutert die Anforderungen an Node und TypeScript. Dies ist eine eigenständige Browser-App ohne serverseitiges Rendering. Das Beispiel wurde unter macOS mit Chromium 145 getestet. Die Befehle verwenden eine POSIX-Shell, etwa Bash unter macOS, Linux oder WSL.
Führen Sie dies in einem übergeordneten Verzeichnis aus, in dem Sie einen neuen Ordner namens
angular-upload-demo erstellen möchten. mkdir lehnt ein bereits
vorhandenes Ziel ab, und jeder abhängige Befehl wird nur ausgeführt, wenn der vorherige erfolgreich
war. Bewahren Sie die erzeugte Lockdatei auf, damit spätere Installationen dieselbe
Abhängigkeitsauflösung verwenden.
(
mkdir angular-upload-demo &&
cd angular-upload-demo &&
NG_CLI_ANALYTICS=false corepack yarn@4.12.0 dlx @angular/cli@22.1.8 new upload-demo \
--directory . --standalone --routing=false --ssr=false --style=css \
--skip-tests --skip-git --skip-install --package-manager=yarn --defaults &&
printf 'nodeLinker: node-modules\n' > .yarnrc.yml &&
touch yarn.lock &&
corepack yarn@4.12.0 add --exact \
@angular/common@22.1.7 @angular/compiler@22.1.7 @angular/core@22.1.7 \
@angular/forms@22.1.7 @angular/platform-browser@22.1.7 @angular/router@22.1.7 \
rxjs@7.8.2 fastify@5.12.5 @fastify/multipart@10.1.1 &&
corepack yarn@4.12.0 add --dev --exact \
@angular/build@22.1.8 @angular/cli@22.1.8 @angular/compiler-cli@22.1.7 typescript@6.0.3
)
Öffnen Sie nach erfolgreichem Abschluss angular-upload-demo in Ihrem Editor. Alle folgenden
Dateinamen sind relativ zu diesem Ordner. Ersetzen Sie nur die erzeugte Datei
src/main.ts; erstellen Sie die anderen drei Dateien unter den angegebenen Namen.
Multipart-Codierung des Browsers verwenden
Das native Dateiauswahlfeld liefert Ihnen ein Objekt vom Typ File;
FormData.append('file', file) legt dessen Bytes in das Multipart-Feld, das unser Server erwartet.
Lassen Sie Content-Type ungesetzt. Der Browser fügt die Multipart-Grenze hinzu, wie
im FormData-Leitfaden von MDN erläutert.
Für das Änderungsereignis dieses Eingabefelds ist weder FormsModule noch
ngModel erforderlich.
Die Anzeige des Upload-Fortschritts erfordert das XHR-Backend von Angular. Konfigurieren Sie
provideHttpClient(withXhr()) und fordern Sie dann Ereignisse mit observe: 'events' und
reportProgress: true an. Das standardmäßige Fetch-Backend meldet keinen Upload-Fortschritt.
Angular dokumentiert sowohl die XHR-Konfiguration als auch die
Abfolge der Anfrageereignisse.
Ein Fortschrittsereignis beschreibt die Übertragung des Anfrageinhalts einschließlich der
Multipart-Rahmendaten. Selbst eine Übertragung von 100 % bedeutet nicht, dass der Server die Datei
akzeptiert hat.
Upload-Komponente hinzufügen
Ersetzen Sie src/main.ts durch diesen vollständigen Einstiegspunkt. Signals
aktualisieren die Ansicht, wenn HTTP-Ereignisse eintreffen. Solange eine Anfrage aussteht, sind beide
Bedienelemente deaktiviert und die Handler verhindern wiederholte Aufrufe. Bei einer fehlgeschlagenen
Anfrage bleibt die ausgewählte Datei für einen manuellen erneuten Versuch erhalten; bei Erfolg wird
das native Eingabefeld geleert, damit sich dieselbe Datei erneut auswählen lässt.
import { HttpClient, HttpErrorResponse, HttpEventType, provideHttpClient, withXhr } from '@angular/common/http'
import { Component, DestroyRef, inject, signal } from '@angular/core'
import { takeUntilDestroyed } from '@angular/core/rxjs-interop'
import { bootstrapApplication } from '@angular/platform-browser'
import { finalize } from 'rxjs'
@Component({
selector: 'app-root',
standalone: true,
templateUrl: './upload.html',
})
class UploadDemo {
readonly selected = signal<File | null>(null)
readonly busy = signal(false)
readonly progress = signal<number | null>(null)
readonly message = signal('Choose a file to upload.')
readonly #http = inject(HttpClient)
readonly #destroyRef = inject(DestroyRef)
select(event: Event): void {
if (this.busy()) return
const input = event.target
if (!(input instanceof HTMLInputElement)) return
const file = input.files?.[0] ?? null
this.progress.set(null)
this.selected.set(null)
if (file === null) {
this.message.set('Choose a file to upload.')
return
}
if (file.size > 1024 * 1024) {
input.value = ''
this.message.set('Choose a file of 1 MiB or smaller.')
return
}
this.selected.set(file)
this.message.set(`Ready to upload “${file.name}”.`)
}
upload(input: HTMLInputElement): void {
const file = this.selected()
if (file === null || this.busy()) return
this.busy.set(true)
this.progress.set(null)
this.message.set(`Sending “${file.name}”…`)
const body = new FormData()
body.append('file', file)
this.#http.post('/api/upload', body, {
observe: 'events',
reportProgress: true,
timeout: 30_000,
}).pipe(
takeUntilDestroyed(this.#destroyRef),
finalize(() => this.busy.set(false)),
).subscribe({
next: (event) => {
if (event.type === HttpEventType.UploadProgress) {
const total = event.total
this.progress.set(total != null && total > 0
? Math.floor(100 * event.loaded / total)
: null)
if (total != null && total > 0 && event.loaded >= total) {
this.message.set('Transfer complete. Waiting for server acceptance…')
}
} else if (event.type === HttpEventType.Response) {
this.selected.set(null)
input.value = ''
this.message.set(`Server accepted “${file.name}”. This demo did not save it.`)
}
},
error: (error: unknown) => {
this.progress.set(null)
this.message.set(error instanceof HttpErrorResponse && error.status !== 0
? `Upload failed (HTTP ${error.status}). Try again or choose another file.`
: 'Network error or timeout. The server may have received the file; check before retrying.')
},
})
}
}
bootstrapApplication(UploadDemo, {
providers: [provideHttpClient(withXhr())],
}).catch(console.error)
Bedienelemente und Rückmeldungen beschriften
Erstellen Sie src/upload.html. Das native Eingabefeld und die Schaltfläche unterstützen
die Tastaturnavigation. Der Statusbereich gibt Rückmeldungen aus, ohne den Fokus zu verschieben.
Ist die Gesamtgröße unbekannt, bleibt der Fortschrittsbalken unbestimmt; ein kleiner lokaler Upload
kann zu schnell abgeschlossen sein, um Zwischenstände in Prozent anzuzeigen.
<main>
<h1>Upload a file</h1>
<p id="file-hint">Up to 1 MiB. The server rejects empty files and does not save uploads.</p>
<label for="upload-file">Choose a file</label>
<input #fileInput id="upload-file" type="file" aria-describedby="file-hint"
[disabled]="busy()" (change)="select($event)" />
<button type="button" [disabled]="selected() === null || busy()"
(click)="upload(fileInput)">Upload</button>
@if (busy()) {
<p>
<progress aria-label="Request upload progress" max="100" [attr.value]="progress()"></progress>
@if (progress() !== null) {
<span>{{ progress() }}% transferred</span>
}
</p>
}
<p role="status" aria-atomic="true">{{ message() }}</p>
</main>
Einen Empfänger ausführen, der den Upload prüft
Erstellen Sie server.ts im Projektstammverzeichnis. Der Empfänger akzeptiert
POST /api/upload mit genau einem Multipart-Dateifeld namens
file, lehnt leere Dateien mit HTTP 422 ab und begrenzt Dateien auf 1 MiB.
Das Multipart-Plugin setzt Grenzen für Größe und Anzahl der Teile durch;
toBuffer() liest die Datei vollständig ein, bevor die Antwort erzeugt wird.
Der Empfänger gibt den empfangenen Dateinamen, den Feldnamen, die Byteanzahl und die SHA-256-Prüfsumme zurück. Er puffert die Datei im Arbeitsspeicher und verwirft sie nach der Anfrage. Er hat keinen Speicherpfad und überschreibt daher niemals eine vorhandene Datei. Diese lokale Demo akzeptiert jeden Dateityp und bindet sich ausschließlich an die Loopback-Adresse.
import { createHash } from 'node:crypto'
import multipart from '@fastify/multipart'
import Fastify from 'fastify'
const server = Fastify()
await server.register(multipart, {
limits: { files: 1, fields: 0, parts: 1, fileSize: 1024 * 1024 },
})
server.setErrorHandler((error, _request, reply) => {
// A part-count limit can interrupt toBuffer() before the parser's own error surfaces.
if (error instanceof Error && 'code' in error && error.code === 'ERR_STREAM_PREMATURE_CLOSE') {
return reply.code(400).send({ error: 'Incomplete multipart upload.' })
}
const status = error instanceof Error && 'statusCode' in error
&& typeof error.statusCode === 'number'
&& error.statusCode >= 400 && error.statusCode <= 599
? error.statusCode : 500
reply.code(status).send({ error: 'Upload failed.' })
})
server.post('/api/upload', async (request, reply) => {
let receipt: { field: string; filename: string; bytes: number; sha256: string } | null = null
for await (const part of request.parts()) {
if (part.type !== 'file') {
return reply.code(400).send({ error: 'Send one file field named file.' })
}
const bytes = await part.toBuffer()
if (part.fieldname !== 'file') {
return reply.code(400).send({ error: 'Send one file field named file.' })
}
if (bytes.length === 0) {
return reply.code(422).send({ error: 'Empty files are not accepted.' })
}
receipt = {
field: part.fieldname,
filename: part.filename,
bytes: bytes.length,
sha256: createHash('sha256').update(bytes).digest('hex'),
}
}
if (receipt === null) {
return reply.code(400).send({ error: 'Send one file field named file.' })
}
return receipt
})
const address = await server.listen({ host: '127.0.0.1', port: Number(process.env.PORT ?? '3000') })
console.log(`Receiver listening at ${address}`)
Erstellen Sie proxy.json im Projektstammverzeichnis, damit der
Angular-Entwicklungsserver Anfragen an /api an Node weiterleitet.
Der Browser verwendet seinen eigenen Ursprung, daher benötigt dieses Beispiel keine
CORS-Konfiguration.
{
"/api/**": {
"target": "http://127.0.0.1:3000"
}
}
Starten Sie den Empfänger in einem Terminal aus dem übergeordneten Verzeichnis, das Sie bei der Einrichtung verwendet haben:
cd angular-upload-demo && node server.ts
Erstellen und starten Sie die App in einem zweiten Terminal, ebenfalls aus diesem übergeordneten Verzeichnis:
cd angular-upload-demo &&
corepack yarn@4.12.0 ng build &&
NG_CLI_ANALYTICS=false corepack yarn@4.12.0 ng serve --host 127.0.0.1 --port 4200 --proxy-config proxy.json
Öffnen Sie http://127.0.0.1:4200. Falls einer der Ports belegt ist, verwenden Sie einen
anderen freien Port: Setzen Sie PORT beim Start von Node und passen Sie das
Proxy-Ziel entsprechend an; ändern Sie bei Bedarf --port von Angular.
Beenden Sie beide Server mit Ctrl+C, wenn Sie fertig sind. Ein erneuter Build ersetzt die Ausgabe
unter dist/ dieser erzeugten App; hochgeladene Dateien werden dort niemals
geschrieben.
Erfolg und Fehler getrennt prüfen
Wählen Sie eine nicht leere Datei unter 1 MiB aus und betätigen Sie Upload. Die abschließende
Meldung sollte „Server accepted“ lauten und erklären, dass die Demo die Datei nicht gespeichert hat.
Prüfen Sie im Netzwerkbereich des Browsers die Antwort auf /api/upload:
field sollte file sein, und
bytes sowie sha256 beschreiben den empfangenen
Dateiinhalt ohne Multipart-Rahmendaten. Wählen Sie dieselbe Datei erneut aus, um eine weitere Anfrage
zu senden.
Testen Sie eine leere Datei, um die Meldung zu HTTP 422 zu sehen. Eine Datei über 1 MiB sollte in der Benutzeroberfläche vor dem Senden abgelehnt werden; der Server setzt seine eigene Grenze auch für Aufrufer durch, die die Benutzeroberfläche umgehen. Solange ein Upload aussteht, bleiben die Dateiauswahl und die Schaltfläche „Upload“ deaktiviert, auch nachdem die Übertragung 100 % erreicht hat. Eine verzögerte Serverantwort oder eine Ablehnung durch den Server darf niemals zu einer Meldung führen, dass der Upload akzeptiert wurde.
Um einen Verbindungsfehler zu testen, lassen Sie die Seite laden, schalten Sie den Netzwerkbereich des Browsers auf offline und laden Sie dann eine Datei hoch. Stellen Sie die Verbindung vor einem erneuten Versuch wieder her. Wenn Sie nur den Node-Empfänger beenden, kann stattdessen ein HTTP-Fehler des Entwicklungsproxys auftreten. Ein Netzwerkfehler oder eine Zeitüberschreitung zeigt Ihnen nicht, ob der Server die Anfrage vor dem Verbindungsabbruch verarbeitet hat; dieses Beispiel versucht es nicht automatisch erneut.
Die Größenprüfung dient dem Benutzerkomfort und bildet keine Sicherheitsgrenze. Wenn Ihre Anwendung
JPEG-, PNG- oder PDF-Uploads benötigt, kann das Attribut accept des
Eingabefelds die Auswahl lenken. Sowohl dieses Attribut als auch File.type sind
jedoch Hinweise, die vom Client kontrolliert werden. Validieren Sie den tatsächlichen Inhalt auf dem
empfangenden Server. MDN erläutert, warum
Einschränkungen der Dateiauswahl keine Uploads validieren.
Ein bereitgestellter Empfänger benötigt außerdem Authentifizierung, Autorisierung, Schutz vor
gefälschten Anfragen und eine Speicherrichtlinie für Ihre Anwendung; der lokale Empfänger bietet
diese Funktionen nicht.
Bei Bedarf fortsetzbare Uploads ergänzen
Diese Multipart-Anfrage beginnt nach einer fehlgeschlagenen Übertragung von vorn. Wenn Sie unterbrochene Uploads fortsetzen müssen, lesen Sie die Anleitung zu fortsetzbaren Datei-Uploads in Angular mit tus-js-client. Für eine fertige Dateiauswahl und Upload-Oberfläche dokumentiert der offizielle Angular-Leitfaden von Uppy dessen Komponenten und kompatible Versionen. Kombinieren Sie diese Oberfläche mit einem Uploader-Plugin und einem Empfänger, der dasselbe Protokoll unterstützt.
