Upload файла в Angular по клику кнопки
Подготовка
В качестве бекенда принимающего файлы будет создано nestjs приложение.
$ npm i -g @nestjs/cli
$ nest new server
Приложение по-умолчанию работает на 3000 порту, чтобы разрешить запросы с 4200 необходимо включить CORS, для этого достаточно добавить строчку app.enableCors();
в основной файл приложения.
main.ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.enableCors(); // <===== 2 часа искал это решение
await app.listen(3000);
}
bootstrap();
В контреллер добавляется хендлер upload
, который ожидает файл в свойстве upload_file
.
@Post('upload')
@UseInterceptors(FileInterceptor('upload_file'))
uploadFile(@UploadedFile() file) {
console.log(file);
return { id: '1', name: file.originalname };
}
С бекендом завершено, можно запускать:
npm start
Выбор и отправка файла по клику
Выбор и отправка файла по клику подразумевает, что на форме отсутствует input
для файла.
- Следует динамически создать
input
- Установить тип
file
- Повешать обработчик на событие
change
- В свойстве события
event.target.files[0]
будут содержаться выбранный пользователем файл. - Подготовить multipart
- Отправить POST запрос на сервер.
- Убрать ссылку на созданный input, чтобы сборщик мусора его удалил
- В свойстве события
- Кликнуть на созданный
input
Получается примерно такой обработчик:
uploadFile(): void {
let fileInput = this.document.createElement('input');
fileInput.type = 'file';
fileInput.addEventListener('change', event => {
const target = event.target as HTMLInputElement;
const selectedFile = target.files[0];
const uploadData = new FormData();
uploadData.append('upload_file', selectedFile, selectedFile.name);
// непосредственно отправить файл (post запрос)
fileInput = null;
});
fileInput.click();
}
Отправка запроса с отслеживанием прогресса
Чтобы получить прогресс, нужно установить 2 свойства в опциях post запроса http клиента:
this.http.post('http://localhost:3000/upload', uploadData, {
reportProgress: true, // Без observe: 'events' не работает
observe: 'events', // без reportProgress: true только HttpEventType.Sent и HttpEventType.Response
})
Результат
Написано в реактивном стиле. Полностью можно посмотреть в репозитарии.
import { Component, Inject, OnDestroy, ChangeDetectionStrategy } from '@angular/core';
import { HttpClient, HttpEventType } from '@angular/common/http';
import { DOCUMENT } from '@angular/common';
import { fromEvent, Subject } from 'rxjs';
import { mergeMap, finalize, takeUntil, first } from 'rxjs/operators';
@Component({
selector: 'app-root',
template: `
<h1>Welcome to File upload!</h1>
<button (click)="chooseAndUploadFile()">Upload!</button>
`,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class AppComponent implements OnDestroy {
private destroy$ = new Subject<void>();
constructor(
@Inject(DOCUMENT) private document: Document,
private http: HttpClient
) {}
/**
* Open file dialog and upload file to server
*/
public chooseAndUploadFile(): void {
let fileInput = this.document.createElement('input');
fileInput.type = 'file';
fromEvent(fileInput, 'change')
.pipe(
first(),
mergeMap(event => {
const target = event.target as HTMLInputElement;
const selectedFile = target.files[0];
// formData обязательно в 2 строчки
const uploadData = new FormData();
uploadData.append('upload_file', selectedFile, selectedFile.name);
return this.http.post('http://localhost:3000/upload', uploadData, {
reportProgress: true, // Без observe: 'events' не работает
observe: 'events', // без reportProgress: true только HttpEventType.Sent и HttpEventType.Response
});
}),
finalize(() => {
// должен быть удален, т.к. счетчик ссылок обнулится
fileInput = null;
console.log('fileInput = null');
}),
takeUntil(this.destroy$)
)
.subscribe(
event => {
// console.log(event);
switch (event.type) {
case HttpEventType.Sent:
console.log('Request sent!');
break;
case HttpEventType.ResponseHeader:
console.log('Response header received!');
break;
case HttpEventType.UploadProgress:
const kbLoaded = Math.round(event.loaded / 1024 / 1024);
const percent = Math.round((event.loaded * 100) / event.total);
console.log(
`Upload in progress! ${kbLoaded}Mb loaded (${percent}%)`
);
break;
case HttpEventType.Response:
console.log('Done!', event.body);
}
},
() => console.log('Upload error'),
() => console.log('Upload complete')
);
fileInput.click();
}
ngOnDestroy(): void {
this.destroy$.next();
this.destroy$.complete();
}
}
Единственный момент требующий пояснения это first()
. Несмотря на то, что в реальности получается только 1 change
событие из input'a и затем он удаляется, подписка на его событие change
будет жить в ожидании следующих событий. Поэтому необходимо добавить оператор, который завершит поток. Подойдет first()
или take(1)
.
- https://www.joshmorony.com/an-introduction-to-nestjs-for-ionic-developers/
- https://academind.com/learn/angular/snippets/angular-image-upload-made-easy/
- https://alligator.io/angular/httpclient-intro/
- https://developer.mozilla.org/ru/docs/Web/JavaScript/Memory_Management
- https://interworks.com/blog/mgardner/2009/08/31/avoiding-memory-leaks-and-javascript-best-practices/