Angular пример ожидания HTTP-запроса
Часто в одностраничных приложениях мы хотим показать состояние, когда что-то загружается, а также показать пользователю, когда что-то идет не так. Состояния ожидания могут быть довольно сложными при работе с асинхронным JavaScript. В Angular у нас есть RxJS Observables, которые помогают нам управлять асинхронной сложностью. В этом посте я покажу шаблон, который придумал, чтобы решить что-то, над чем я работал, что помогло мне отобразить состояние запроса API, а также любые ошибки.
Этот шаблон я называю шаблоном отложенного запроса. Вероятно, не очень хорошее имя, но вот как это работает. Обычно мы делаем HTTP-запрос в Angular и возвращаем один Observable, который будет выдавать значение запроса по завершении. У нас нет простого способа показать пользователю, что мы загружаем данные или когда что-то идет не так, без большого количества кода, определенного в наших компонентах. С помощью этого шаблона вместо службы, возвращающей отклик Observable, мы возвращаем новый объект, который содержит два Observable. Один для ответа HTTP, а другой с обновлениями статуса запроса.
export interface Pending {
data: Observable;
status: Observable;
}
export enum Status {
LOADING = 'LOADING',
SUCCESS = 'SUCCESS',
ERROR = 'ERROR'
}
В наших сервисах мы можем вернуть Observable объект вместо необработанного HTTP Observable.
@Injectable({ providedIn: 'root' })
export class UserService {
constructor(private http: HttpClient) {}
load(userId: number): Pending { ... }
}
В рамках нашего метода load
мы можем отправить обновления статуса любому, кто использует наш объект Pending
. Этот шаблон делает наш код чище в компонентах. Давайте посмотрим на пример компонента, и мы вернемся к реализации load()
.
import { Component } from '@angular/core';
import { UserService, User, Pending, Status } from './user.service';
@Component({
selector: 'my-app',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent {
readonly Status = Status;
readonly user: Pending;
constructor(private userService: UserService) {
this.user = this.userService.load(1);
}
}
В компоненте мы используем UserService
для вызова метода load()
и присваиваем объект Pending
свойству user
. Я также установил enum Status
как свойство класса, чтобы мог ссылаться на него в своем шаблоне.
{{user.name}}
Height: {{user.height}}
Mass: {{user.mass}}
Homeworld: {{user.homeworld}}
В шаблоне мы используем async pipe
, чтобы подписаться на мои пользовательские данные от объекта Pending
. После подписки мы можем отображать мои данные как обычно согласно шаблону Angular.
Для отображения сообщений о состоянии мы также можем подписаться на статус Observable внутри шаблона.
{{user.name}}
Height: {{user.height}}
Mass: {{user.mass}}
Homeworld: {{user.homeworld}}
Loading User...
There was an error loading the user.
Теперь, когда пользовательский статус возвращает обновление о том, что запрос запущен, мы покажем сообщение Loading User...
. Когда статус выдает обновление, в котором произошла какая-либо ошибка, мы можем показать сообщение об ошибке пользователю. С помощью объекта Pending
мы можем довольно легко показать статус текущего запроса в нашем шаблоне.
Теперь вернемся к нашему UserService
, мы можем увидеть, как мы реализовали объект Pending
в методе load()
.
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable, ReplaySubject, defer } from 'rxjs';
@Injectable({ providedIn: 'root' })
export class UserService {
constructor(private http: HttpClient) {}
load(userId: number): Pending {
const status = new ReplaySubject();
const data = this.http.get(`https://swapi.co/api/people/${userId}`);
return { data, status };
}
}
Вот наша отправная точка для метода load()
. У нас есть две наблюдаемые, составляющие наш ожидающий объект. Во-первых status
, это особый вид наблюдаемого, называемый ReplaySubject
. ReplaySubject позволит любому, кто подписывается после того, как события уже запущены, получить последнее событие, которое было отправлено. Во-вторых, наш стандартный HTTP Observable от клиентской службы Angular HTTP.
Во-первых, мы хотим иметь возможность уведомлять о начале запроса. Для этого нам нужно обернуть нашу HTTP Observable, чтобы мы могли выдавать новый статус при подписке.
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable, ReplaySubject, defer } from 'rxjs';
@Injectable({ providedIn: 'root' })
export class UserService {
constructor(private http: HttpClient) {}
load(userId: number): Pending {
const status = new ReplaySubject();
const request = this.http.get(
`https://swapi.co/api/people/${userId}`
);
const data = defer(() => {
status.next(Status.LOADING);
return request;
});
return { data, status };
}
}
Используя из RxJS функцию defer
, мы можем обернуть существующую HTTP Observable, выполнить некоторый код, а затем вернуть новый Observable. Используя defer
, мы можем инициировать событие загрузки статуса, только когда кто-то подписывается. Использование defer
важно, потому что Observables, по умолчанию, ленивы и не будут выполнять наш HTTP-запрос, пока не подписаны.
Затем мы должны обработать ошибки, если что-то пойдет не так с нашим запросом.
@Injectable({ providedIn: 'root' })
export class UserService {
constructor(private http: HttpClient) {}
load(userId: number): Pending {
const status = new ReplaySubject();
const request = this.http
.get(`https://swapi.co/api/people/${userId}`)
.pipe(
retry(2),
catchError(error => {
status.next(Status.ERROR);
throw 'error loading user';
})
);
const data = defer(() => {
status.next(Status.LOADING);
return request;
});
return { data, status };
}
}
Используя операторы catchError
и retry
, мы можем перехватывать исключения и повторять указанное количество раз. После того как у нас появятся события LOADING
и ERROR
, нам нужно добавить статус SUCCESS
.
@Injectable({ providedIn: 'root' })
export class UserService {
constructor(private http: HttpClient) {}
load(userId: number): Pending {
const status = new ReplaySubject();
const request = this.http
.get(`https://swapi.co/api/people/${userId}`)
.pipe(
retry(2),
catchError(error => {
status.next(Status.ERROR);
throw 'error loading user';
}),
tap(() => status.next(Status.SUCCESS))
);
const data = defer(() => {
status.next(Status.LOADING);
return request;
});
return { data, status };
}
}
Используя оператор tap
, мы можем вызвать функцию побочного эффекта (код вне наблюдаемой) всякий раз, когда событие возвращается из HTTP Observable. С помощью tap
вызвать событие состояния успеха для нашего компонента.
В Angular HTTP Client Service есть функция, которая может сделать нечто подобное, прослушивая обновления HTTP-запроса из запроса. Однако это может быть дорогостоящим, поскольку вызывает обнаружение изменений для каждого события.