O que poderia dar errado? Como lidar com erros no Angular

Balázs Tápai em Level Up Your Code Seguir 1 de jul · 9 min ler

Aproximadamente um ano atrás, implementei os primeiros testes e2e em um projeto. Foi uma aplicação bastante grande usando o JAVA SpringBoot no back-end e o Angular no front-end. Usamos o Transferidor como uma ferramenta de teste, que usa o Selênio. No código front-end havia um serviço, que tinha um método manipulador de erros. Quando esse método era chamado, uma caixa de diálogo modal aparecia e o usuário podia ver os detalhes dos erros e o rastreamento da pilha.

O problema é que, embora tenha rastreado cada erro ocorrido no back-end, o front-end falhou silenciosamente. TypeErrors , ReferenceErrors e outras exceções não identificadas foram registradas apenas no console. Quando algo deu errado durante o teste e2e, a captura de tela, que foi tirada quando a etapa de teste falhou, não mostrou absolutamente nada. Divirta-se depurando isso!

Felizmente Angular tem uma maneira interna de lidar com erros e é extremamente fácil de usar. Nós apenas temos que criar nosso próprio serviço, que implementa a interface ErrorHandler do Angular:

 import {ErrorHandler, Injectable} de ' @ angular / core '; @Injetável ({ 
providedIn: 'root'
})
classe de exportação ErrorHandlerService implementa ErrorHandler {
construtor () {}
handleError (error: any) {
// Implemente sua própria maneira de lidar com erros
}
}

Embora possamos fornecer nosso serviço facilmente em nosso AppModule , pode ser uma boa ideia fornecer esse serviço em um módulo separado. Dessa forma, poderíamos criar nossa própria biblioteca e usá-la em nossos projetos futuros também:

 // 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},
]
};
}
}

Usamos o CLI Angular para gerar o ErrorHandlerModule , então já temos um componente gerado, que pode ser o conteúdo do nosso diálogo modal. Para que possamos colocá-lo em uma sobreposição de CDK angular, ele precisa ser um entryComponent . É por isso que nós colocá-lo na ErrorHandlerModule variedade entryComponents 's.

Nós também adicionamos algumas importações. OverlayModule e o A11yModule vêm do módulo CDK. Eles são necessários para criar nossa sobreposição e interceptar o foco quando nossa caixa de diálogo de erro é aberta. Como você pode ver, nós fornecemos OverlayContainer usando a classe FullscreenOverlayContainer porque se ocorrer um erro, queremos restringir as interações dos nossos usuários ao nosso modal de erro. Se não tivermos um pano de fundo de tela cheia, os usuários poderão interagir com o aplicativo e causar mais erros. Vamos adicionar nosso módulo recém-criado ao nosso 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 {
}

Agora que temos nosso ErrorHandlerService no lugar, podemos começar a implementar a lógica. Vamos criar um diálogo modal, que exibe o erro de maneira clara e legível. Este diálogo terá uma sobreposição / pano de fundo e será colocado dinamicamente no DOM com a ajuda do CDK Angular. Vamos instalá-lo:

 npm install @angular/cdk --save 

De acordo com a documentação , o componente Overlay precisa de alguns arquivos css pré-construídos. Agora, se usássemos o Material Angular em nosso projeto, não seria necessário, mas nem sempre é o caso. Vamos importar o CSS de sobreposição em nosso arquivo styles.css . Note que, se você já usa o Material Angular no seu aplicativo, não precisa importar este CSS.

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

Vamos usar nosso método handleError para criar nossa caixa de diálogo modal. É importante saber que o serviço ErrorHandler faz parte da fase de inicialização do aplicativo Angular. Para evitar um erro de dependência cíclica bastante desagradável, usamos o injetor como seu único parâmetro de construtor. Usamos o sistema de injeção de dependência do Angular quando o método real é chamado. Vamos importar a sobreposição do CDK e anexar nosso ErrorHandlerComponent no 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);
}
}

Vamos voltar nossa atenção para nosso modal manipulador de erros. Uma solução de trabalho bastante simples seria exibir a mensagem de erro e o rastreamento de pilha. Vamos também adicionar um botão "rejeitar" na parte inferior.

 // 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();
}
}

Como você pode ver, o componente em si é bem simples. Vamos usar duas diretivas bastante importantes no modelo para tornar o diálogo acessível. O primeiro é o cdkTrapFocus que intercepta o foco quando o diálogo é renderizado. Isso significa que o usuário não pode focar elementos por trás de nossa caixa de diálogo modal. A segunda diretriz é o cdkTrapFocusAutoCapture que focalizará automaticamente o primeiro elemento focalizável dentro da nossa armadilha de foco. Além disso, ele restaurará automaticamente o foco para o elemento anteriormente focalizado, quando nossa caixa de diálogo estiver fechada.

Para poder exibir as propriedades do erro, precisamos injetá-lo usando o construtor. Para isso, precisamos do nosso próprio injectionToken . Também criamos uma lógica bastante simples para emitir um evento de dispensa usando um assunto e dismiss$ propriedade dismiss$ . Vamos conectar isso com nosso método handleError em nosso serviço e fazer algumas refatorações.

 // 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);
}
}

Vamos nos concentrar em fornecer o erro como um parâmetro injetado primeiro. Como você pode ver, a classe ComponentPortal espera um parâmetro must-have, que é o próprio componente. O segundo parâmetro é um ViewContainerRef que teria um efeito do local lógico do componente da árvore de componentes. O terceiro parâmetro é o nosso método createInejctor . Como você pode ver, ele retorna uma nova instância do PortalInjector . Vamos dar uma olhada rápida em sua implementação subjacente:

 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);
}
}

Como você pode ver, ele espera um Injector como primeiro parâmetro e um WeakMap para tokens personalizados. Fizemos exatamente isso usando nosso ERROR_INJECTOR_TOKEN que está associado ao nosso erro. O PortalInjector criado é usado para a instanciação adequada do nosso ErrorHandlerComponent , ele irá certificar-se de que o erro em si estará presente no componente.

Por fim, nosso método attachPortal retorna o componente recentemente instanciado que dismiss$ propriedade. Nós nos inscrevemos nele, e quando ele muda, nós chamamos o .dispose() em nosso overlayRef . E nossa caixa de diálogo modal de erro é descartada. Note que nós também chamamos de completo em nosso assunto dentro do componente, portanto, não precisamos cancelar a assinatura dele.