Как обрабатывать ответы в Angular? - PullRequest
1 голос
/ 11 марта 2020

Это вопрос о фундаментальных вещах, но я не знаю, как вернуть ответ на компонент в зависимости от результата действия: success или error, чтобы я мог выполнять связанные с ними действия.

У меня есть компонент SignInComponent, который собирает всю необходимую информацию из формы и отправляет ее в мой AuthService, который обрабатывает мои запросы GraphQL (и на данный момент перенаправление и другие вещи). Для сценария, в котором запрос выполняется успешно, пока все работает нормально, но если есть какой-либо ответ об ошибке от API, мне нужно иметь ошибки и информацию о них в моем SignInComponent. (если введены неверные учетные данные, я должен сообщить об этом пользователю и т. д.)

Я попытался вернуть значение, например:

signIn.subscribe({
        next: // actions for successful response,
        error: (err) => {return 'errors'} // from here
      })

, а также попытался выдать ошибку, подобную этой:

signIn.subscribe({
        next: // actions for successful response,
        error: (err) => {throw new Error('Oops')} // The Error
      })

и попытался поймать это в моем SignInComponent следующим образом:

      try {
        this.authService.signIn(params);
      } catch (e) {
        // handling the error accordingly 
      }

Но все вышеперечисленное не помогло мне получить ответ в моем SignInComponent и получить уведомление о результате status.

Вот мой код, и я надеюсь, что кто-нибудь подскажет мне, как справиться с этой ситуацией, используя хорошие практики.

auth.service.ts

import { Injectable } from '@angular/core';
import { Router } from '@angular/router';
import { SignInMutation, SignInInput } from './sign-in.graphql';
import { SignUpMutation, SignUpInput } from './sign-up.graphql';
import { BehaviorSubject } from 'rxjs';

// TODO: To learn if observable is unsubscribed in services automatically. In Classes there is an Inteface OnDestroy for that

@Injectable({
  providedIn: 'root'
})
export class AuthService {

  auth: BehaviorSubject<boolean> = new BehaviorSubject(false);

  constructor(
    private router: Router,
    private signInMutation: SignInMutation,
    private signUpMutation: SignUpMutation
  ) {
    this.isSignedIn();
  }

  signIn(params: SignInInput) {

    this.signInMutation.mutate(params)
      .subscribe({

        next: ({ data }) => {

          const signIn = data.signIn;
          const token = signIn.token;

          const { payload } = JSON.parse(atob(token.split('.')[1]));
          const currentUser = payload.userData;

          const localData = {
            currentUser,
            token
          };

          this.setLocalData(localData);

          this.router.navigate(['/dashboard']);
          this.auth.next(true);

        },

        error: (err) => {
          throw new Error('Ooops...');
        }

      });



  }

  signUp(SignUpInput: SignUpInput) {

    this.signUpMutation.mutate({ SignUpInput })
      .subscribe({
        next: console.log,
        error: console.log,
      });

  }

  private getLocalData(name: string): null | object | string {
    const data = localStorage.getItem(name);
    if (!data) { return null; }

    if (/^(\{).*(\})$/i.test(data)) {
      return JSON.parse(data);
    } else {
      return data;
    }
  }

  private setLocalData(data: object) {

    for (const key in data) {
      if (data.hasOwnProperty(key)) {
        const value = data[key];

        if (typeof value === 'object') {
          localStorage.setItem(key, JSON.stringify(data[key]));
        } else {
          localStorage.setItem(key, data[key]);
        }

      }
    }

  }

  signOut() {
    localStorage.removeItem('token');
    localStorage.removeItem('currentUser');
    this.router.navigate(['/']);
    this.auth.next(false);
  }

  private isSignedIn() {
    const token = this.getLocalData('token') as string;
    const checkFormat = (token && token.split('.').length === 3) ? true : null;

    if (token && checkFormat) {
      this.auth.next(true);
    }

  }

}

signInComponent

import { Component, OnInit } from '@angular/core';
import { FormBuilder, FormGroup } from '@angular/forms';
import { AuthService } from '../auth.service';
import { emailFormat } from '../../_helpers/custom-validation';

@Component({
  selector: 'app-sign-in',
  templateUrl: './sign-in.component.html',
  styleUrls: ['./sign-in.component.scss']
})
export class SignInComponent implements OnInit {

  signInForm: FormGroup;
  submitted: boolean;

  constructor(
    private fb: FormBuilder,
    private authService: AuthService,
  ) { }

  ngOnInit(): void {
    this.signInForm = this.fb.group({
      email: ['', [emailFormat]],
      pwd: [''],
    });

    this.reset();
  }

  onSubmit(e) {
    e.preventDefault();
    this.submitted = true;

    if (this.signInForm.valid) {
      const params = {
        email: this.signInForm.value.email,
        password: this.signInForm.value.pwd
      };

      this.authService.signIn(params);

      this.reset();
    }

  }

  private reset() {
    this.signInForm.reset();
    this.submitted = false;
  }

}

Спасибо всем заранее!

Ответы [ 2 ]

2 голосов
/ 11 марта 2020

Так что это скорее проблема rx js, а не Angular.

this.signInMutation.mutate(params) возвращает Observable, который содержит упакованные данные ответа сервера. В вашем случае, иметь signin() метод подписки на эту наблюдаемую вещь не очень хорошая идея.

Вместо этого обычная практика будет просто сразу возвращать наблюдаемое:

signIn(params: SignInInput) {
  return this.signInMutation.mutate(params);
}

Теперь вы можете Допустим, вы хотите выполнить некоторую работу по пост-обработке данных, например, что вы делали в первоначальном предложении subscribe().

Хорошо, давайте переместим их в pipe()

function signIn(params: SignInInput) {
  return this.signInMutation.mutate(params).pipe(
    tap(data => {
      const signIn = data.signIn;
      const token = signIn.token;

      const { payload } = JSON.parse(atob(token.split('.')[1]));
      const currentUser = payload.userData;

      const localData = {
        currentUser,
        token,
      };

      this.setLocalData(localData);

      this.router.navigate(['/dashboard']);
      this.auth.next(true);
    }),
  );
}

Хорошо, теперь давайте вернемся к коду вашего компонента

onSubmit(e) {
  // ...

  this.authService.signIn(params).subscribe({
    error: err => {
      // handle your err here, where the handler belongs
      this.errorMessage = 'oops, errors here! ' + err.message
    }
  })
}

По сути, это один из наиболее распространенные методы обработки удаленных ответов на основе rx js в Angular. Конечно, есть несколько других вещей, о которых нужно беспокоиться, например, unsubscribe() к этой подписке, когда ваш компонент будет уничтожен. Но я не хочу, чтобы go из топи c.

Наличие у ваших Angular сервисов прямого возврата наблюдаемых также помогает вам делиться ими и составлять их далее с другими сервисами или кандидатами на основе rx js. такие как реактивные формы.

1 голос
/ 11 марта 2020

Поскольку у вас уже есть логический флаг auth в вашем AuthService, вы можете добавить флаг error с сообщением об ошибке.

Ваши компоненты или другие службы могут подписаться на него для получения текущего состояния.

Этот подход (state management) дает вам больше гибкости для извлечения другого состояния (аутентификация да / нет, ошибка / нет ошибки ...). Вы также можете добавить флаг loading, который устанавливается в yes во время процесса аутентификации.

export class AuthService {
  auth = new BehaviorSubject<boolean>(false);
  error = new BehaviorSubject<string>(null);

  auth$ = this.auth.asObservable();
  error$ = this.error.asObservable();

  ...

  signIn(params: SignInInput) {
    this.auth.next(false);

    this.signInMutation.mutate(params).subscribe(
      data => {
        ...
        this.auth.next(true);
        this.error.next(null);
      },

      error => {
        this.error.next('error message');
      }
    );
  }
} 

внутри component.ts, мы можем подписаться на сообщение об ошибке, чтобы выполнить определенные действия c, но это не обязательно. Просто используйте async pipe внутри шаблона, чтобы отобразить простое сообщение:

export class AppComponent {
  error$ = this.authService.error$;
  ...

  constructor(...) {
    // manually subscribe to do some other tasks...
    // but it's not required to only display inside template (see below)
    this.error$.subscribe(error => {
      if (error) {

      }
    });

    // becareful to manage unsubscription (not done here to keep code simple)
  }  
}

Внутри шаблона мы можем отобразить сообщение в случае ошибки:

 <p *ngIf="error$ | async as error">{{ error }}</p>
...