Acum aproximativ un an, am implementat primele teste e2e pe un proiect. A fost o aplicație destul de mare folosind JAVA SpringBoot pe back-end și Angular pe front-end. Am folosit Protractor ca instrument de testare, care utilizează Seleniu. În codul front-end exista un serviciu, care avea o metodă de gestionare a erorilor. Când a fost apelată acea metodă, a apărut un dialog modal și utilizatorul a putut vedea detaliile erorilor și urmărirea stivei.

Problema a fost că, deși a urmărit fiecare eroare care a avut loc pe back-end, front-end-ul a eșuat în tăcere. TypeErrors, ReferenceErrors și alte excepții neprinse au fost înregistrate numai pe consolă. Când ceva nu a funcționat în timpul testului e2e rulează captura de ecran, care a fost făcută atunci când pasul de testare a eșuat, nu a arătat absolut nimic. Distrează-te depanând asta!

Din fericire, Angular are un mod încorporat de gestionare a erorilor și este extrem de ușor de utilizat. Trebuie doar să ne creăm propriul serviciu, care îl implementează pe Angular ErrorHandler interfață:

import { ErrorHandler, Injectable } from '@angular/core';

@Injectable({
    providedIn: 'root'
})
export class ErrorHandlerService implements ErrorHandler{
    constructor() {}

    handleError(error: any) {
        // Implement your own way of handling errors
    }
}

În timp ce ne-am putea oferi cu ușurință serviciul nostru în AppModule, ar putea fi o idee bună să furnizați acest serviciu într-un modul separat. Astfel, ne-am putea crea propria bibliotecă și o putem folosi și în proiectele noastre viitoare:

// ERROR HANDLER MODULE
import {ErrorHandler, ModuleWithProviders, NgModule} from '@angular/core';
import {ErrorHandlerComponent} from './components/error-handler.component';
import {FullscreenOverlayContainer, OverlayContainer, OverlayModule} from '@angular/cdk/overlay';
import {ErrorHandlerService} from './error-handler.service';
import {A11yModule} from '@angular/cdk/a11y';

@NgModule({
  declarations: [ErrorHandlerComponent],
  imports: [CommonModule, OverlayModule, A11yModule],
  entryComponents: [ErrorHandlerComponent]
})
export class ErrorHandlerModule {
  public static forRoot(): ModuleWithProviders {
    return {
      ngModule: ErrorHandlerModule,
      providers: [
        {provide: ErrorHandler, useClass: ErrorHandlerService},
        {provide: OverlayContainer, useClass: FullscreenOverlayContainer},
      ]
    };
  }
}

Am folosit CLI unghiular pentru generarea ErrorHandlerModule, deci avem deja o componentă generată, care poate fi conținutul dialogului nostru modal. Pentru ca noi să o putem pune într-o suprapunere CDK angulară, trebuie să fie o componentă entryComponent. De aceea l-am pus în ErrorHandlerModulematricea entryComponents.

Am adăugat și câteva importuri. OverlayModule și A11yModule provine din modulul CDK. Acestea sunt necesare pentru a crea suprapunerea noastră și pentru a capta focalizarea atunci când dialogul nostru de erori este deschis. După cum puteți vedea, vă oferim OverlayContainer folosind FullscreenOverlayContainer clasă deoarece, dacă apare o eroare, vrem să restricționăm interacțiunile utilizatorilor noștri la modalitatea noastră de eroare. Dacă nu avem un fundal cu ecran complet, utilizatorii ar putea să interacționeze cu aplicația și să provoace alte erori. Să adăugăm modulul nostru nou creat la AppModule:

// APP MODULE
import {BrowserModule} from '@angular/platform-browser';
import {NgModule} from '@angular/core';

import {AppRoutingModule} from './app-routing.module';
import {AppComponent} from './app.component';
import {MainComponent} from './main/main.component';
import {ErrorHandlerModule} from '@btapai/ng-error-handler';
import {HttpClientModule} from '@angular/common/http';

@NgModule({
  declarations: [ AppComponent, MainComponent ],
  imports: [
    BrowserModule,
    HttpClientModule,
    ErrorHandlerModule.forRoot(),
    AppRoutingModule,
  ],
  bootstrap: [AppComponent]
})
export class AppModule {
}

Acum că avem „ErrorHandlerService” în loc, putem începe să implementăm logica. Vom crea un dialog modal, care afișează eroarea într-un mod curat, lizibil. Acest dialog va avea o suprapunere / fundal și va fi plasat dinamic în DOM cu ajutorul CDK angular. Să-l instalăm:

npm install @angular/cdk --save

In conformitate cu documentație, Acoperire componentă are nevoie de câteva fișiere CSS pre-construite. Acum, dacă am folosi material angular în proiectul nostru, nu ar fi necesar, dar nu este întotdeauna cazul. Să importăm css-ul suprapus în stiluri.css fişier. Rețineți că, dacă utilizați deja material angular în aplicația dvs., nu trebuie să importați acest css.

@import '~@angular/cdk/overlay-prebuilt.css';

Să ne folosim de noi handleError metoda de a crea dialogul nostru modal. Este important să știm că ErrorHandler serviciul face parte din faza de inițializare a aplicației Angular. Pentru a evita un lucru destul de urât eroare de dependență ciclică, folosim injectorul ca singurul său parametru constructor. Folosim sistemul de injectare a dependenței Angular atunci când se apelează metoda reală. Să importăm suprapunerea din CDK și să le atașăm ErrorHandlerComponent în DOM:

// ... imports

@Injectable({
   providedIn: 'root'
})
export class ErrorHandlerService implements ErrorHandler {
   constructor(private injector: Injector) {}

   handleError(error: any) {
       const overlay: Overlay = this.injector.get(Overlay);
       const overlayRef: OverlayRef = overlay.create();
       const ErrorHandlerPortal: ComponentPortal<ErrorHandlerComponent> = new ComponentPortal(ErrorHandlerComponent);
       const compRef: ComponentRef<ErrorHandlerComponent> = overlayRef.attach(ErrorHandlerPortal);
   }
}

Să ne îndreptăm atenția către modalitatea noastră de gestionare a erorilor. O soluție de lucru destul de simplă ar fi afișarea mesajului de eroare și a stivei. Să adăugăm și un buton „Renunță” în partea de jos.

// imports
export const ERROR_INJECTOR_TOKEN: InjectionToken<any> = new InjectionToken('ErrorInjectorToken');

@Component({
  selector: 'btp-error-handler',
  // TODO: template will be implemented later
  template: `${error.message}<br><button (click)="dismiss()">DISMISS</button>`
  styleUrls: ['./error-handler.component.css'],
})
export class ErrorHandlerComponent {
  private isVisible = new Subject();
  dismiss$: Observable<{}> = this.isVisible.asObservable();

  constructor(@Inject(ERROR_INJECTOR_TOKEN) public error) {
  }

  dismiss() {
    this.isVisible.next();
    this.isVisible.complete();
  }
}

După cum puteți vedea, componenta în sine este destul de simplă. Vom folosi două directive destul de importante în șablon, pentru a face dialogul accesibil. Primul este cdkTrapFocus care va captura focalizarea atunci când dialogul este redat. Aceasta înseamnă că utilizatorul nu poate focaliza elemente în spatele dialogului nostru modal. A doua directivă este cdkTrapFocusAutoCapture care va focaliza automat primul element focalizabil din capcana noastră de focalizare. De asemenea, va restabili automat focalizarea la elementul anterior focalizat, când dialogul nostru este închis.

Pentru a putea afișa proprietățile erorii, trebuie să o injectăm folosind constructorul. Pentru asta, avem nevoie de ale noastre jeton injecție. De asemenea, am creat o logică destul de simplă pentru emiterea unui eveniment de renunțare folosind un subiect și renunță la $ proprietate. Să conectăm acest lucru cu handleError metoda în serviciul nostru și să facă unele refactoring.

// imports
export const DEFAULT_OVERLAY_CONFIG: OverlayConfig = {
  hasBackdrop: true,
};

@Injectable({
  providedIn: 'root'
})
export class ErrorHandlerService implements ErrorHandler {

  private overlay: Overlay;

  constructor(private injector: Injector) {
    this.overlay = this.injector.get(Overlay);
  }

  handleError(error: any): void {
    const overlayRef = this.overlay.create(DEFAULT_OVERLAY_CONFIG);
    this.attachPortal(overlayRef, error).subscribe(() => {
      overlayRef.dispose();
    });
  }

  private attachPortal(overlayRef: OverlayRef, error: any): Observable<{}> {
    const ErrorHandlerPortal: ComponentPortal<ErrorHandlerComponent> = new ComponentPortal(
      ErrorHandlerComponent,
      null,
      this.createInjector(error)
    );
    const compRef: ComponentRef<ErrorHandlerComponent> = overlayRef.attach(ErrorHandlerPortal);
    return compRef.instance.dismiss$;
  }

  private createInjector(error: any): PortalInjector {
    const injectorTokens = new WeakMap<any, any>([
      [ERROR_INJECTOR_TOKEN, error]
    ]);

    return new PortalInjector(this.injector, injectorTokens);
  }
}

Să ne concentrăm mai întâi pe furnizarea erorii ca parametru injectat. După cum puteți vedea, ComponentPortal clasa se așteaptă la un parametru obligatoriu, care este componenta în sine. Al doilea parametru este a ViewContainerRef care ar avea un efect al locului logic al componentei în arborele componentei. Al treilea parametru este al nostru createInejctor metodă. După cum puteți vedea, returnează un nou PortalInjector instanță. Să aruncăm o privire rapidă asupra implementării sale de bază:

export class PortalInjector implements Injector {
 constructor(
   private _parentInjector: Injector,
   private _customTokens: WeakMap<any, any>) { }

 get(token: any, notFoundValue?: any): any {
   const value = this._customTokens.get(token);

   if (typeof value !== 'undefined') {
     return value;
   }

   return this._parentInjector.get<any>(token, notFoundValue);
 }
}

După cum puteți vedea, se așteaptă o Injector ca prim parametru și un WeakMap pentru jetoanele personalizate. Am făcut exact asta folosind a noastră ERROR_INJECTOR_TOKEN care este asociat cu eroarea noastră în sine. Cel creat PortalInjector este folosit pentru instanțierea corectă a noastră ErrorHandlerComponent, se va asigura că eroarea în sine va fi prezentă în componentă.

În cele din urmă, a noastră attachPortal metoda returnează componentele instanțate recent renunță la $ proprietate. Ne abonăm la el și, atunci când se schimbă, apelăm .dispune() pe al nostru overlayRef. Iar dialogul nostru modal de eroare este respins. Rețineți că, de asemenea, apelăm complet la subiectul nostru din interiorul componentei, prin urmare, nu trebuie să ne dezabonați de la aceasta.


Acum, acest lucru este excelent pentru erorile care sunt aruncate atunci când există o problemă în codul lateral al clinetei. Dar creăm aplicații web și folosim puncte finale API. Deci, ce se întâmplă atunci când o imprimare finală REST redă o eroare?

Putem rezolva fiecare eroare în propriul serviciu, dar chiar ne dorim? Dacă totul este în regulă, erorile nu vor fi aruncate. Dacă există cerințe specifice, de exemplu, de gestionat 418 cod de stare cu un unicorn zburător ai putea să-l implementezi pe handler în serviciul său. Dar când ne confruntăm cu erori destul de frecvente, cum ar fi 404 sau 503, am putea dori să afișăm acest lucru în același dialog de eroare.

Să adunăm rapid ce se întâmplă când un HttpErrorResponse este aruncat. Se va întâmpla asincron, deci probabil ne vom confrunta cu unele probleme de detectare a modificărilor. Acest tip de eroare are proprietăți diferite decât o eroare simplă, prin urmare, s-ar putea să avem nevoie de o metodă de dezinfectare. Acum să intrăm în el creând o interfață destul de simplă pentru SanitisedError:

export interface SanitizedError {
  message: string;
  details: string[];
}

Să creăm un șablon pentru ErrorHandlerComponent:

// Imports

@Component({
  selector: 'btp-error-handler',
  template: `
    <section cdkTrapFocus [cdkTrapFocusAutoCapture]="true" class="btp-error-handler__container">
      <h2>Error</h2>
      <p>{{error.message}}</p>
      <div class="btp-error-handler__scrollable">
        <ng-container *ngFor="let detail of error.details">
          <div>{{detail}}</div>
        </ng-container>
      </div>
      <button class="btp-error-handler__dismiss button red" (click)="dismiss()">DISMISS</button>
    </section>`,
  styleUrls: ['./error-handler.component.css'],
})
export class ErrorHandlerComponent implements OnInit {
 // ...
}

Am înfășurat întregul modal într-un și am adăugat cdkTrapFocus directivă la aceasta. Această directivă va împiedica utilizatorul să navigheze în DOM din spatele suprapunerii / modului nostru. [cdkTrapFocusAutoCapture]= „adevărat” se asigură că butonul de închidere este focalizat imediat. Când modalitatea este închisă, elementul anterior focalizat va readuce focalizarea. Afișăm pur și simplu mesajul de eroare și detaliile folosind * ngPentru. Să sărim înapoi în a noastră ErrorHandlerService:

// Imports

@Injectable({
  providedIn: 'root'
})
export class ErrorHandlerService implements ErrorHandler {
  // Constructor

  handleError(error: any): void {
    const sanitised = this.sanitiseError(error);
    const ngZone = this.injector.get(NgZone);
    const overlayRef = this.overlay.create(DEFAULT_OVERLAY_CONFIG);

    ngZone.run(() => {
      this.attachPortal(overlayRef, sanitised).subscribe(() => {
        overlayRef.dispose();
      });
    });
  }
  
  // ...

  private sanitiseError(error: Error | HttpErrorResponse): SanitizedError {
    const sanitisedError: SanitizedError = {
      message: error.message,
      details: []
    };
    if (error instanceof Error) {
      sanitisedError.details.push(error.stack);
    } else if (error instanceof HttpErrorResponse) {
      sanitisedError.details = Object.keys(error)
        .map((key: string) => `${key}: ${error[key]}`);
    } else {
      sanitisedError.details.push(JSON.stringify(error));
    }
    return sanitisedError;
  }
  // ...
}

Cu un destul de simplu sanitiseError metoda creăm un obiect care se bazează pe interfața noastră definită anterior. Verificăm tipurile de erori și completăm datele în consecință. Partea mai interesantă este utilizarea injectorului pentru a obține ngZone. Când o eroare se produce asincron, se întâmplă de obicei în afara detectării modificărilor. Ne înfășurăm attachPortal cu ngZone.run (/ * … * /), deci atunci când un HttpErrorResponse este prins, este redat corect în modalitatea noastră.

În timp ce starea actuală funcționează frumos, îi lipsește încă personalizarea. Folosim Overlay din modulul CDK, așa că expunerea unui jeton de injecție pentru configurații personalizate ar fi bine. Un alt neajuns important al acestui modul este că atunci când este utilizat acest modul, un alt modul nu poate fi utilizat pentru tratarea erorilor. De exemplu, integrarea Sentry ar necesita implementarea unui modul ErrorHandler similar, dar ușor. Pentru a putea folosi ambele, ar trebui să punem în aplicare posibilitatea de a folosi cârlige în interiorul handler-ului nostru de erori. În primul rând, să creăm Indicativ injecție și configurația noastră implicită:

import {InjectionToken} from '@angular/core';
import {DEFAULT_OVERLAY_CONFIG} from './constants/error-handler.constants';
import {ErrorHandlerConfig} from './interfaces/error-handler.interfaces';

export const DEFAULT_ERROR_HANDLER_CONFIG: ErrorHandlerConfig = {
  overlayConfig: DEFAULT_OVERLAY_CONFIG,
  errorHandlerHooks: []
};

export const ERROR_HANDLER_CONFIG: InjectionToken<ErrorHandlerConfig> = new InjectionToken('btp-eh-conf');

Apoi furnizați-l cu modulul nostru, folosind cel existent forRoot metodă:

@NgModule({
  declarations: [ErrorHandlerComponent],
  imports: [CommonModule, OverlayModule, A11yModule],
  entryComponents: [ErrorHandlerComponent]
})
export class ErrorHandlerModule {

  public static forRoot(): ModuleWithProviders {
    return {
      ngModule: ErrorHandlerModule,
      providers: [
        {provide: ErrorHandler, useClass: ErrorHandlerService},
        {provide: OverlayContainer, useClass: FullscreenOverlayContainer},
        {provide: ERROR_HANDLER_CONFIG, useValue: DEFAULT_ERROR_HANDLER_CONFIG}
      ]
    };
  }
}

Apoi, integrați această gestionare a configurației în ErrorHandlerService de asemenea:

// Imports
@Injectable({
  providedIn: 'root'
})
export class ErrorHandlerService implements ErrorHandler {
  // ...

  handleError(error: any): void {
    const sanitised = this.sanitiseError(error);
    const {overlayConfig, errorHandlerHooks} = this.injector.get(ERROR_HANDLER_CONFIG);
    const ngZone = this.injector.get(NgZone);

    this.runHooks(errorHandlerHooks, error);
    const overlayRef = this.createOverlayReference(overlayConfig);
    ngZone.run(() => {
      this.attachPortal(overlayRef, sanitised).subscribe(() => {
        overlayRef.dispose();
      });
    });
  }
  // ...
  private runHooks(errorHandlerHooks: Array<(error: any) => void> = [], error): void {
    errorHandlerHooks.forEach((hook) => hook(error));
  }

  private createOverlayReference(overlayConfig: OverlayConfig): OverlayRef {
    const overlaySettings: OverlayConfig = {...DEFAULT_OVERLAY_CONFIG, ...overlayConfig};
    return this.overlay.create(overlaySettings);
  }
  // ...
}

Și suntem aproape gata. Să integrăm un cârlig de gestionare a erorilor terță parte în aplicația noastră:

// Imports
const CustomErrorHandlerConfig: ErrorHandlerConfig = {
  errorHandlerHooks: [
    ThirdPartyErrorLogger.logErrorMessage,
    LoadingIndicatorControl.stopLoadingIndicator,
  ]
};

@NgModule({
  declarations: [
    AppComponent,
    MainComponent
  ],
  imports: [
    BrowserModule,
    HttpClientModule,
    ErrorHandlerModule.forRoot(),
    AppRoutingModule,
  ],
  providers: [
    {provide: ERROR_HANDLER_CONFIG, useValue: CustomErrorHandlerConfig}
  ],
  bootstrap: [AppComponent]
})
export class AppModule {
}

După cum puteți vedea, gestionarea erorilor este o parte extrem de importantă a dezvoltării software-ului, dar poate fi și distractiv.

Vă mulțumesc foarte mult pentru că ați citit această postare pe blog. Dacă preferați să citiți codul, vă rugăm să verificați ng-reusables git repository. De asemenea, puteți încerca implementarea folosind aceasta pachet npm.

Poți să mă urmărești și mai departe Stare de nervozitate sau GitHub.