Angular - обработка ошибок

Внимание

Данный материал является частью цикла статей «Frontend приложение на Angular». Не забудьте посмотреть другие статьи по этой теме :-)

  1. Angular - Подготовка проекта для нового приложения
  2. Angular - навигация и маршрутизация
  3. Angular - HttpClient, сервисы и файлы окружения
  4. Angular - Ленивая загрузка данных (lazy loading)
  5. Angular - обработка ошибок
  6. Angular - декораторы и директивы @Input и @Output
  7. Angular - валидация формы и POST запрос
  8. Angular - Работа с запросами PUT
  9. Angular - Работа с DELETE запросами

Создание компонента для ошибки 500 (внутренняя ошибка сервера)

В папке error-pages мы собираемся создать новый компонент, набрав команду AngularCLI:

ng g component error-pages/internal-server --skipTests

Давайте изменим internal-server.component.ts:

import { Component, OnInit } from '@angular/core';

@Component({
  selector: 'app-internal-server',
  templateUrl: './internal-server.component.html',
  styleUrls: ['./internal-server.component.css']
})
export class InternalServerComponent implements OnInit {
  public errorMessage: string = "500 SERVER ERROR, CONTACT ADMINISTRATOR!!!!";

  constructor() { }

  ngOnInit() {
  }

}

Затем изменим internal-server.component.html:

<p>
  {{errorMessage}}
</p>

Кроме того, мы собираемся модифицировать internal-server.component.css:

p{
    font-weight: bold;
    font-size: 50px;
    text-align: center;
    color: #c72d2d;
}

Наконец, давайте изменим app.module.ts:

RouterModule.forRoot([
      { path: 'home', component: HomeComponent },
      { path: 'owner', loadChildren: () => import('./owner/owner.module').then(m => m.OwnerModule) },
      { path: '404', component: NotFoundComponent},
      { path: '500', component: InternalServerComponent },
      { path: '', redirectTo: '/home', pathMatch: 'full' },
      { path: '**', redirectTo: '/404', pathMatch: 'full'}
    ])

Мы создали наш компонент, и пора создать сервис для обработки ошибок.

Создание службы для обработки ошибок в Angular

В папке shared/services создайте новую службу и назовите ее error-handler.service.ts:

ng g service shared/services/error-handler --skipTests

Давайте изменим этот файл error-handler.service.ts:

import { Injectable } from '@angular/core';
import { HttpErrorResponse } from '@angular/common/http';
import { Router } from '@angular/router';

@Injectable({
  providedIn: 'root'
})
export class ErrorHandlerService {
  public errorMessage: string = '';
 
  constructor(private router: Router) { }
 
  public handleError = (error: HttpErrorResponse) => {
    if(error.status === 500){
      this.handle500Error(error);
    }
    else if(error.status === 404){
      this.handle404Error(error)
    }
    else{
      this.handleOtherError(error);
    }
  }
 
  private handle500Error = (error: HttpErrorResponse) => {
    this.createErrorMessage(error);
    this.router.navigate(['/500']);
  }
 
  private handle404Error = (error: HttpErrorResponse) => {
    this.createErrorMessage(error);
    this.router.navigate(['/404']);
  }
 
  private handleOtherError = (error: HttpErrorResponse) => {
    this.createErrorMessage(error);
    //TODO: this will be fixed later;
  }
 
  private createErrorMessage = (error: HttpErrorResponse) => {
    this.errorMessage = error.error ? error.error : error.statusText;
  }
}

Прежде всего, мы вводим Router, который мы используем для перенаправления пользователя на другие страницы в коде. В методе handleError() мы проверяем код состояния ошибки и на основе этого вызываем правильный частный метод для обработки этой ошибки. Функции handle404Error() и handle500Error() отвечают за заполнение свойства errorMessage. Мы собираемся использовать это свойство как модальное сообщение об ошибке или сообщение об ошибке на странице. Позже мы поговорим о функции handleOtherError(), поэтому с комментарием внутри.

Если вы помните в файле owner-list.component.ts, мы извлекаем всех владельцев с сервера. Но в этом файле нет обработки ошибок. Итак, давайте продолжим изменение этого файла owner-list.component.ts для реализации функции обработки ошибок Angular:

import { Component, OnInit } from '@angular/core';
import { RepositoryService } from './../../shared/services/repository.service';
import { Owner } from './../../_interfaces/owner.model';
import { ErrorHandlerService } from './../../shared/services/error-handler.service';

@Component({
  selector: 'app-owner-list',
  templateUrl: './owner-list.component.html',
  styleUrls: ['./owner-list.component.css']
})
export class OwnerListComponent implements OnInit {
  public owners: Owner[];
  public errorMessage: string = '';

  constructor(private repository: RepositoryService, private errorHandler: ErrorHandlerService) { }

  ngOnInit() {
    this.getAllOwners();
  }

  public getAllOwners = () => {
    let apiAddress: string = "api/owner";
    this.repository.getData(apiAddress)
      .subscribe(res => {
        this.owners = res as Owner[];
      },
      (error) => {
        this.errorHandler.handleError(error);
        this.errorMessage = this.errorHandler.errorMessage;
      })
  }
}

Вы можете проверить это, изменив код в методе сервера GetAllOwners. В качестве первой строки кода добавьте return NotFound() или return StatusCode(500, “Some message”), и вы наверняка будете перенаправлены на правильную страницу с ошибкой.

Подготовка компонента "Информация о владельце"

Давайте продолжим, создав компонент сведений о владельце:

ng g component owner/owner-details --skipTests

Нам понадобится, чтобы изменить файл owner.module.ts:

RouterModule.forChild([
      { path: 'list', component: OwnerListComponent },
      { path: 'details/:id', component: OwnerDetailsComponent }
    ])

Как видите, у нового пути есть идентификатор параметра. Итак, когда мы нажимаем кнопку Details, мы собираемся передать этот идентификатор нашему маршруту, и мы собираемся получить владельца с этим точным идентификатором в компоненте OwnerDetails.

Нам нужно добавить новый интерфейс в папку _interfaces:

export interface Account{
    id: string;
    dateCreated: Date;
    accountType: string;
    ownerId?: string;
}

И измените интерфейс Owner:

import { Account } from './account.model';

export interface Owner{
    id: string;
    name: string;
    dateOfBirth: Date;
    address: string;

    accounts?: Account[];
}

Используя вопросительный знак, мы делаем поле необязательным.

Чтобы продолжить, изменим owner-list.component.html:

<button type="button" id="details" class="btn btn-light" 
       (click)="getOwnerDetails(owner.id)">Details</button>

При событии щелчка мы вызываем функцию getOwnerDetails и передаем идентификатор владельца в качестве параметра. Итак, нам нужно обработать это событие щелчка в нашем файле owner-list.component.ts.

Добавьте оператор импорта:

import { Router } from '@angular/router';

Затем измените конструктор и добавьте функцию getOwnerDetails(id):

constructor(private repository: RepositoryService, private errorHandler: ErrorHandlerService, 
    private router: Router) { }
public getOwnerDetails = (id) => { 
  const detailsUrl: string = `/owner/details/${id}`; 
  this.router.navigate([detailsUrl]); 
}

Реализация компонента "Информация о владельце"

У нас есть весь код для поддержки компонента сведений о владельце. Пришло время реализовать бизнес-логику внутри этого компонента.

Сначала измените файл owner-details.component.ts:

import { Component, OnInit } from '@angular/core';
import { Owner } from './../../_interfaces/owner.model';
import { Router, ActivatedRoute } from '@angular/router';
import { RepositoryService } from './../../shared/services/repository.service';
import { ErrorHandlerService } from './../../shared/services/error-handler.service';

@Component({
  selector: 'app-owner-details',
  templateUrl: './owner-details.component.html',
  styleUrls: ['./owner-details.component.css']
})
export class OwnerDetailsComponent implements OnInit {
  public owner: Owner;
  public errorMessage: string = '';

  constructor(private repository: RepositoryService, private router: Router, 
              private activeRoute: ActivatedRoute, private errorHandler: ErrorHandlerService) { }

  ngOnInit() {
    this.getOwnerDetails()
  }

  getOwnerDetails = () => {
    let id: string = this.activeRoute.snapshot.params['id'];
    let apiUrl: string = `api/owner/${id}/account`;

    this.repository.getData(apiUrl)
    .subscribe(res => {
      this.owner = res as Owner;
    },
    (error) =>{
      this.errorHandler.handleError(error);
      this.errorMessage = this.errorHandler.errorMessage;
    })
  }

}

Это в значительной степени та же логика, что и в файле owner-list.component.ts, за исключением того, что теперь у нас есть импортированный ActivatedRoute, потому что нам нужно получить наш идентификатор из маршрута.

После выполнения функции getOwnerDetails мы собираемся сохранить объект-владелец со всеми связанными учетными записями внутри свойства owner.

Все, что нам нужно сделать, это изменить файл owner-details.component.html:

<div class="card card-body bg-light mb-2 mt-2">
  <div class="row">
    <div class="col-md-3">
      <strong>Owner name:</strong>
    </div>
    <div class="col-md-3">
      {{owner?.name}}
    </div>
  </div>
  <div class="row">
    <div class="col-md-3">
      <strong>Date of birth:</strong>
    </div>
    <div class="col-md-3">
      {{owner?.dateOfBirth | date: 'dd/MM/yyyy'}}
    </div>
  </div>
  <div class="row" *ngIf='owner?.accounts.length <= 2; else advancedUser'>
    <div class="col-md-3">
      <strong>Type of user:</strong>
    </div>
    <div class="col-md-3">
      <span class="text-success">Beginner user.</span>
    </div>
  </div>
  <ng-template #advancedUser>
    <div class="row">
      <div class="col-md-3">
        <strong>Type of user:</strong>
      </div>
      <div class="col-md-3">
        <span class="text-info">Advanced user.</span>
      </div>
    </div>
  </ng-template>
</div>

<div class="row">
  <div class="col-md-12">
    <div class="table-responsive">
      <table class="table table-striped">
        <thead>
          <tr>
            <th>Account type</th>
            <th>Date created</th>
          </tr>
        </thead>
        <tbody>
          <tr *ngFor="let account of owner?.accounts">
            <td>{{account?.accountType}}</td>
            <td>{{account?.dateCreated | date: 'dd/MM/yyyy'}}</td>
          </tr>
        </tbody>
      </table>
    </div>
  </div>
</div>

В этом примере кода мы условно отображаем сущность владельца. Более того, мы отображаем все аккаунты, связанные с этим владельцем:

Заключение

Прочитав этот пост, вы узнали:

  • Как обрабатывать ошибки в отдельной службе с помощью обработки ошибок Angular
  • Способ использования условного рендеринга HTML-страницы.
  • Как создать страницу сведений о вашей организации

Спасибо, что прочитали этот пост, надеюсь, он был вам полезен.

В следующей части статье я покажу вам, как создавать дочерние компоненты и как использовать @Input, @Output и EventEmmiters в Angular. Поступая таким образом, вы научитесь разбивать компоненты на более мелкие части (родительские и дочерние компоненты).