Рубрики
Без рубрики

Что может пойти не так? Как обрабатывать ошибки в угловом

Примерно год назад я реализовал первые тесты E2E на проекте. Это было довольно большим приложением, используя Java Springboot на спине и угловой на переднем конце. Мы использовали транспортировку в качестве инструмента тестирования, который использует селен. В предельном коде был сервис, который

Автор оригинала: FreeCodeCamp Community Member.

Примерно год назад я реализовал первые тесты E2E на проекте. Это было довольно большим приложением, используя Java Springboot на спине и угловой на переднем конце. Мы использовали транспортировку в качестве инструмента тестирования, который использует селен. В интерфейсном коде возникла сервис, который имел метод обработчика ошибок. Когда этот метод был вызван, появится модальное диалоговое окно, и пользователь мог видеть детали ошибок и трассировки стека.

Проблема заключалась в том, что, в то время как она отслеживала каждую ошибку, которая произошла на заднем дне, передний интерфейс не работает молча. Видеораторы , Собственные зерна И другие неопрятные исключения были зарегистрированы только на консоли. Когда что-то пошло не так во время теста E2E запускает скриншот, который был предпринят, когда тестовый шаг не удался, абсолютно ничего не показал. Получайте удовольствие отладки, что!

К счастью, угловая имеет встроенный способ обработки ошибок, и он чрезвычайно прост в использовании. Мы просто должны создать наш собственный сервис, который реализует угловые ErrorHandler интерфейс:

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

Пока мы могли бы легко предоставить наш сервис в нашем AppModule Может быть, хорошая идея предоставить эту услугу в отдельном модуле. Таким образом, мы могли бы создать нашу собственную библиотеку и использовать его в наших будущих проектах:

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

Мы использовали Угловые CLI Для генерации ErrorHandLermodule Таким образом, у нас уже есть созданный компонент, который может быть нашим содержанием модального диалога. Для того, чтобы мы могли бы поставить его внутри углового наложения CDK, он должен быть входным комплектом. Вот почему мы положили его в ErrorHandLermodule Andcomponents Array.

Мы также добавили некоторые импорт. Оверляйкамодуль и A11ymodule поставляется из модуля CDK. Они необходимы для создания нашего наложения и в ловушку, когда открывается наш диалог ошибок. Как видите, мы предоставляем OverayAleContainer используя FullscreenoverlayContainer Класс, потому что если возникает ошибка, мы хотим ограничить взаимодействия наших пользователей к нашему модалу ошибки. Если у нас нет полноэкранного фона, пользователи могут взаимодействовать с приложением и вызывать дальнейшие ошибки. Давайте добавим наш недавно созданный модуль на наше 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 {
}

Теперь, когда у нас есть наш «ErrorHandLerservice» на месте, мы можем начать реализацию логики. Мы собираемся создать модальный диалог, который отображает ошибку чистым, читаемым образом. Этот диалог будет иметь наложение/фоном, и он будет динамически помещен в DOM с помощью угловой CDK. Давайте установим это:

npm install @angular/cdk --save

Согласно Документация , Наложение Компонент нуждается в некоторых предварительно встроенных файлов CSS. Теперь, если бы мы использовали угловой материал в нашем проекте, это не было бы необходимости, но это не всегда так. Давайте импортируем на накладные CSS в нашем styles.csss файл. Обратите внимание, что если вы уже используете угловой материал в вашем приложении, вам не нужно импортировать эти CSS.

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

Давайте использовать наши Рульеррар Метод создания нашего модального диалога. Важно знать, что ErrorHandler Сервис является частью фазы инициализации приложения угловой. Чтобы избежать довольно неприятных Ошибка циклической зависимости Мы используем инжектор в качестве единственного параметра конструктора. Мы используем систему впрыска в зависимости от угловых в зависимости от того, когда называется фактический метод. Давайте импортируем накладку с CDK и прикрепите наш CurryHandlerComponent в 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 = new ComponentPortal(ErrorHandlerComponent);
       const compRef: ComponentRef = overlayRef.attach(ErrorHandlerPortal);
   }
}

Давайте обратим наше внимание на нашу модал обработчика ошибок. Довольно простое рабочее решение будет отображаться сообщение об ошибке и Stacktrace. Давайте также добавим кнопку «Уволить» на дно.

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

@Component({
  selector: 'btp-error-handler',
  // TODO: template will be implemented later
  template: `${error.message}
` 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(); } }

Как видите, сам компонент довольно прост. Мы собираемся использовать два довольно важных директива в шаблоне, чтобы сделать диалоговое окно доступным. Первый – это cdktrapfocus который будет ловить фокус, когда отображается диалоговое окно. Это означает, что пользователь не может фокусироваться элементы за нашими модальными диалогом. Вторая директива – это cdktrapfocusautocapture Что автоматически сосредоточит первый фокусируемый элемент внутри нашей фокусировки. Кроме того, он автоматически восстановит фокус на ранее фокусированный элемент, когда наш диалог закрыт.

Чтобы иметь возможность отображать свойства ошибок, нам нужно ввести его с помощью конструктора. Для этого нам нужна наша Институт Отказ Мы также создали довольно простую логику для выпуска мероприятия увольнения, используя тему и уволить $ имущество. Давайте свяжем об этом с нашим Рульеррар Метод в нашем сервисе и сделайте рефакторинг.

// 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 = new ComponentPortal(
      ErrorHandlerComponent,
      null,
      this.createInjector(error)
    );
    const compRef: ComponentRef = overlayRef.attach(ErrorHandlerPortal);
    return compRef.instance.dismiss$;
  }

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

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

Давайте сосредоточимся на предоставлении ошибки в качестве нанесенного параметра. Как вы можете видеть, Компонентпорт Класс ожидает, что один должен иметь параметр, который является сама компонента. Второй параметр – это ViewContainerRef который будет иметь эффект логического места компонента компонентного дерева. Третий параметр наш createinejctor метод. Как вы можете видеть, это возвращает новый Portalinjector пример. Давайте посмотрим на его базовую реализацию:

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

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

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

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

Как видите, он ожидает Инжектор В качестве первого параметра и слабый картон для пользовательских токенов. Мы сделали именно что используя наши Error_injector_token который связан с нашей ошибкой. Созданный Portalinjector используется для правильной реализации наших CurryHandlerComponent , это убедитесь, что сама ошибка будет присутствовать в компоненте.

Наконец, наше AscapePortal Метод возвращает недавно экземпляр уволить $ имущество. Мы подписываемся на это, и когда он меняет, мы называем .dispose () на нашем Overayref

Теперь это превосходно для ошибок, которые брошены, когда есть проблема в боковом коде клита. Но мы создаем веб-приложения, и мы используем конечные точки API. Так что, что происходит, когда Andpint отдыхает обратно ошибка?

Мы можем справиться с каждой ошибкой в своем собственном обслуживании, но мы действительно хотим? Если все в порядке, ошибки не будут брошены. Если есть конкретные требования, например, для обработки 418 Код состояния С летающим единорогом вы могли бы реализовать свой обработчик в своем обслуживании. Но когда мы сталкиваемся с довольно распространенными ошибками, как 404 или 503, мы можем захотеть отобразить это в этом диалоговом окне ошибок.

Давайте просто будем собираться, что происходит, когда Httperrorresponse брошен. Это произойдет Async, поэтому, вероятно, мы собираемся сталкиваться с некоторыми проблемами обнаружения изменений. Этот тип ошибок имеет разные свойства, чем простая ошибка, поэтому нам может понадобиться метод Sanityer. Теперь давайте попадаем в это, создав довольно простой интерфейс для SanitiseisherError :

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

Давайте создадим шаблон для нашего CurryHandlerComponent :

// Imports

@Component({
  selector: 'btp-error-handler',
  template: `
    

Error

{{error.message}}

{{detail}}
`, styleUrls: ['./error-handler.component.css'], }) export class ErrorHandlerComponent implements OnInit { // ... }

Мы завернули весь модал в <Раздел> И мы добавили cdktrapfocus Директива к этому. Эта директива помешает пользователю навигацию на доме за нашими наложениями/модальными. [cdktrapfocusautocapture] = “True” Убедитесь, что кнопка увольнения немедленно сосредоточена. Когда модаль закрыт, ранее фокусированный элемент вернет фокус. Мы просто отображаем сообщение об ошибке и детали, используя * NGFOR Отказ Давайте вернемся в нашу 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;
  }
  // ...
}

С довольно простым SaniteError Метод. Мы создаем объект, который основан на нашем ранее определенном интерфейсе. Мы проверяем наличие ошибок и заполните данные соответственно. Более интересная часть использует инжектор для получения ngzone Отказ Когда ошибка происходит асинхронно, она обычно происходит вне обнаружения изменения. Мы обертываем наши AscapePortal с ngzone.run (/* … */) Так что, когда Httperrorresponse поймано, оно оказывается должным образом в нашем модальном.

Хотя нынешнее состояние хорошо работает, у него все еще не хватает настройки. Мы используем наложение из модуля CDK, поэтому выставляя токен впрыска для пользовательских конфигураций был бы хорошим. Еще одним важным недостатком этого модуля является то, что при использовании этого модуля другой модуль не может быть использован для обработки ошибок. Например, интеграция Sentry потребует от того, чтобы вы могли реализовать аналогичный, но облегченный модуль ErrorHandler. Чтобы иметь возможность использовать оба, мы должны реализовать возможность использования крючков внутри нашего обработчика ошибок. Во-первых, давайте создадим наше Институт И наша настройка по умолчанию:

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 = new InjectionToken('btp-eh-conf');

Затем предоставьте его нашим модулем, используя наши существующие Forroot Метод:

@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}
      ]
    };
  }
}

Затем интегрируйте эту конфигурацию обработки в наше ErrorHandLerservice также:

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

И мы почти готовы. Давайте интегрируем сторонний обработчик ошибок в наше приложение:

// 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 {
}

Как видите, ошибки обработки – чрезвычайно важная часть разработки программного обеспечения, но это также может быть весело.

Большое спасибо за чтение этого поста в блоге. Если вы предпочитаете чтение кода, пожалуйста, проверьте мой NG-Reusables Git Rebository Отказ Вы также можете попробовать реализацию, используя это Пакет NPM Отказ

Вы также можете следовать за мной на Twitter или Github Отказ