Avatar ·

Загрузка файлов в Angular: как правильно отправить файл на сервер с прогресс-баром?

Всем привет! Я новичок в Angular (версия 15) и TypeScript, и никак не могу разобраться с загрузкой файлов. Мне нужно сделать форму, где пользователь выбирает файл (изображение или PDF), а затем отправляет его на сервер с отображением прогресса загрузки. Я пробовал использовать обычный FormData, но что-то идет не так. Вот мой текущий код компонента:
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 и обработку состояний.

Установка