DevGang
Авторизоваться

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-запроса из запроса. Однако это может быть дорогостоящим, поскольку вызывает обнаружение изменений для каждого события.

Источник:

#Angular #RxJS
Комментарии
Чтобы оставить комментарий, необходимо авторизоваться

Присоединяйся в тусовку

Поделитесь своим опытом, расскажите о новом инструменте, библиотеке или фреймворке. Для этого не обязательно становится постоянным автором.

Попробовать

Оплатив хостинг 25$ в подарок вы получите 100$ на счет

Получить