Загрузка файлов в Angular: как правильно отправить файл на сервер с прогресс-баром?
import { Component } from '@angular/core';
import { HttpClient } from '@angular/common/http';
@Component({
selector: 'app-upload',
template: `
<input type="file" (change)="onFileSelected($event)">
<button (click)="onUpload()">Загрузить</button>
`
})
export class UploadComponent {
selectedFile: File | null = null;
constructor(private http: HttpClient) {}
onFileSelected(event: any) {
this.selectedFile = event.target.files[0];
}
onUpload() {
const formData = new FormData();
formData.append('file', this.selectedFile!);
this.http.post('/api/upload', formData).subscribe({
next: (res) => console.log('Успех', res),
error: (err) => console.error('Ошибка', err)
});
}
}
Проблемы:
1. Запрос уходит, но сервер возвращает 400 Bad Request — пишет, что файл пустой.
2. Нет индикации прогресса загрузки — пользователь не видит, сколько осталось.
3. Не знаю, как обрабатывать ошибки и показывать пользователю понятные сообщения.
Подскажите, как правильно реализовать загрузку файлов в Angular с прогресс-баром и обработкой ошибок? Желательно с примером кода. Заранее спасибо!
<<Правильная загрузка файлов в Angular с прогресс-баром
Ваш код содержит несколько типичных ошибок начинающих. Давайте разберем их и создадим рабочее решение с прогресс-баром и обработкой ошибок.
Основные проблемы в вашем коде:
- Отсутствует проверка наличия файла перед отправкой
- Не настроены заголовки Content-Type — Angular сам должен установить multipart/form-data, но иногда нужна ручная настройка
- Нет подписки на события прогресса HttpRequest
- Нет обработки ошибок для пользователя
Полное решение с прогресс-баром:
Создадим сервис для загрузки и компонент с индикацией прогресса.
1. Сервис загрузки файлов (upload.service.ts):
import { Injectable } from '@angular/core';
import { HttpClient, HttpEventType, HttpRequest, HttpEvent } from '@angular/common/http';
import { Observable, map } from 'rxjs';
export interface UploadProgress {
status: 'progress' | 'complete' | 'error';
progress: number; // 0-100
message: string;
response?: any;
}
@Injectable({ providedIn: 'root' })
export class UploadService {
constructor(private http: HttpClient) {}
uploadFile(file: File, url: string): Observable<UploadProgress> {
if (!file) {
throw new Error('Файл не выбран');
}
const formData = new FormData();
formData.append('file', file, file.name);
const req = new HttpRequest('POST', url, formData, {
reportProgress: true, // Включаем отслеживание прогресса
headers: {
// Важно: не устанавливайте Content-Type вручную!
// Angular сам добавит multipart/form-data с boundary
}
});
return this.http.request(req).pipe(
map((event: HttpEvent<any>) => this.getUploadProgress(event))
);
}
private getUploadProgress(event: HttpEvent<any>): UploadProgress {
switch (event.type) {
case HttpEventType.UploadProgress:
const progress = Math.round((100 * event.loaded) / event.total!);
return {
status: 'progress',
progress: progress,
message: `Загружено ${progress}%`
};
case HttpEventType.Response:
return {
status: 'complete',
progress: 100,
message: 'Файл успешно загружен!',
response: event.body
};
default:
return {
status: 'progress',
progress: 0,
message: 'Начало загрузки...'
};
}
}
}
2. Компонент с формой и прогресс-баром (upload.component.ts):
import { Component } from '@angular/core';
import { UploadService, UploadProgress } from './upload.service';
@Component({
selector: 'app-upload',
template: `
<div class="upload-container">
<h2>Загрузка файла</h2>
<div class="file-input">
<input type="file"
(change)="onFileSelected($event)"
accept=".jpg,.png,.pdf"
#fileInput>
<button (click)="fileInput.click()"
[disabled]="uploadProgress?.status === 'progress'">
Выбрать файл
</button>
<span *ngIf="selectedFile">{{ selectedFile.name }}</span>
</div>
<!-- Прогресс-бар -->
<div *ngIf="uploadProgress" class="progress-section">
<div class="progress-bar">
<div class="progress-fill"
[style.width.%]="uploadProgress.progress">
{{ uploadProgress.progress }}%
</div>
</div>
<p class="progress-message">{{ uploadProgress.message }}</p>
</div>
<!-- Кнопка загрузки -->
<button (click)="onUpload()"
[disabled]="!selectedFile || uploadProgress?.status === 'progress'">
{{ uploadProgress?.status === 'progress' ? 'Загрузка...' : 'Загрузить' }}
</button>
<!-- Ошибки -->
<div *ngIf="errorMessage" class="error">
{{ errorMessage }}
</div>
</div>
`,
styles: [`
.upload-container { max-width: 400px; margin: 20px; padding: 20px; border: 1px solid #ccc; }
.progress-bar { height: 20px; background: #e0e0e0; border-radius: 10px; margin: 10px 0; }
.progress-fill { height: 100%; background: #4caf50; border-radius: 10px;
display: flex; align-items: center; justify-content: center;
color: white; font-size: 12px; transition: width 0.3s; }
.error { color: red; margin-top: 10px; }
`]
})
export class UploadComponent {
selectedFile: File | null = null;
uploadProgress: UploadProgress | null = null;
errorMessage: string | null = null;
constructor(private uploadService: UploadService) {}
onFileSelected(event: Event): void {
const input = event.target as HTMLInputElement;
if (input.files && input.files.length > 0) {
this.selectedFile = input.files[0];
this.errorMessage = null;
this.uploadProgress = null;
}
}
onUpload(): void {
if (!this.selectedFile) {
this.errorMessage = 'Пожалуйста, выберите файл';
return;
}
// Валидация размера файла (например, макс 5MB)
const maxSize = 5 * 1024 * 1024; // 5MB
if (this.selectedFile.size > maxSize) {
this.errorMessage = 'Файл слишком большой. Максимальный размер: 5MB';
return;
}
this.errorMessage = null;
this.uploadProgress = { status: 'progress', progress: 0, message: 'Начало загрузки...' };
this.uploadService.uploadFile(this.selectedFile, '/api/upload').subscribe({
next: (progress) => {
this.uploadProgress = progress;
if (progress.status === 'complete') {
alert('Файл успешно загружен!');
}
},
error: (err) => {
this.errorMessage = `Ошибка загрузки: ${err.message || 'Неизвестная ошибка'}`;
this.uploadProgress = null;
}
});
}
}
Ключевые моменты:
- reportProgress: true — обязательно для получения событий прогресса
- Не устанавливайте Content-Type вручную — Angular сам добавит multipart/form-data с правильным boundary
- Всегда проверяйте файл на null и валидируйте размер/тип перед отправкой
- Используйте HttpEventType для обработки разных этапов запроса
- Отписывайтесь от подписки в ngOnDestroy, чтобы избежать утечек памяти (в примере опущено для краткости)
Это решение полностью решает ваши проблемы: правильная отправка FormData, отображение прогресса в реальном времени и обработка ошибок с понятными сообщениями пользователю.
<<Альтернативный подход с использованием Angular Material и библиотеки ngx-file-upload
Если вы используете Angular Material в проекте, можно упростить задачу с помощью готовой библиотеки ngx-file-upload. Это сэкономит время на создание UI и обработку состояний.
Установка