مدیریت کردن خطاها در فریم ورک انگولار

دوشنبه 31 تیر 1398

مدیریت خطاها در فریم ورک انگولار موضوعی بسیار مهم است، در این مطلب قصد داریم کمی بیشتر درباره خطایابی در فریم ورک انگولار صحبت کنیم.

 مدیریت کردن خطاها در فریم ورک انگولار

ما قصد داریم موضوع خطایابی در فریم ورک انگولار را با بیان یک خاطره از یک پروژه به صورت کامل برای شما بیان کنیم.

 فریم ورک انگولار

تجربه ای از خطایابی در فریم ورک انگولار

حدودا یکسال پیش من تست e2e را برای اولین بار در یک پروژه پیاده سازی کردم، این پروژه یک برنامه بسیار بزرگ بود که Spring Boot در زبان برنامه نویسی جاوا برای بخش بک اند و برای فرانت اند نیز از فریم ورک انگولار استفاده کرده بودیم. ما تصمیم گرفتیم که به عنوان یک ابزار تست برنامه از Protractor استفاده کنیم که خود این ابزار نیز از Selenium استفاده می کرد. در کدهای مربوط به فرانت اند یک سرویس وجود داشت که شامل یک متد مربوط به مدیریت کننده خطا بود. زمانی که این متد فراخوانی میشد یک modal dialog بالا می آمد و کاربر می توانست جزئیات خطاها و stack-trace را به راحتی مشاهده کند.

مشکل این جا بود که هر زمانی که خطایی در بخش بک اند اتفاق می افتاد قسمت فرانت اند نیز به آرامی دچار خطا می شد. TypeError ها، ReferenceError  ها و سایر انواع ارورها که تنها در قسمت کنسول چاپ می شدند، زمانی که در حین اجرای تست e2e اسکرین شات ها را تست می کرد مشکلی پیش می آمد تست با شکست رو برو می شد و هیچ چیزی را نشان نمی داد. خطایابی در فریم ورک انگولار در این پروژه واقعا لذت بخش بود.
استفاده از روش های آماده برای خطایابی در فریم ورک انگولار

خوشبختانه یک متد آماده برای خطایابی در انگولار دراین فریم ورک ساخته شده است که استفاده از آن بسیار راحت است، ما فقط کافی است سرویس خود را بسازیم که رابط کاربری Angular's 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

    }

}

در حالی که ما می توانیم به راحتی سرویس خود را در appModul خود فراهم کنیم، این می تواند یک ایده خوب برای فراهم کردن این سرویس در یک ماژول جدا باشد. به این ترتیب ما می توانید کتابخانه خود را بسازیم و از آن در پروژه های آینده به این ترتیب استفاده کنیم:

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

      ]

    };

  }

}


 فریم ورک انگولار


ساخت یک  ErrorHandler Module

ما از CLI فریم ورک انگولار استفاده کردیم تا بتوانیم یک ErrorHandler Module بسازیم، بنابراین ما یک کامپوننت تولید شده در اختیار داریم که می تواند محتوای modal dialog ما باشد، به علاوه ما برای این که بتوانیم آن را داخل یک Angular CDK بگذاریم باید آن را تبدیل به یک entry Component کنیم. به همین دلیل است که ما آن را در داخل آرایه entry Components در ErrorHandler Module's می گذاریم.

ما همچنین باید برخی از ایمپورت ها را نیز اضافه کنیم، OverlayModule و A11yModule از داخل ماژول CDK می آیند، این دو مورد نیاز هستند چرا که ما با استفاده از آن ها overlay خود را می سازیم و در حین باز شدن دیالوگ مربوط به ارور نیز مورد نیاز هستند.

همانطور که می توانید مشاهده کنید ما OverlayContainer را با استفاده از کلاس FullscreenOverlayContainer فراهم کردیم چرا که اگر یک خطا اتفاق بیفتد ما قصد داریم تعاملات کاربر خود را با ارور modal خود محدود کنیم. اگر ما یک پس زمینه تمام صفحه نداشته باشیم کاربران ما ممکن است که با اپلیکیشن تعامل داشته باشند که این موضوع باعث به وجود آمدن ارورهای بیشتر می شود. اجازه دهید که ماژول ساخته شده جدید خود را به AppModul اضافه کنیم:

 فریم ورک انگولار


اضافه کردن ماژول ساخته شده جدید

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

}

 فریم ورک انگولار


توضیحاتی درباره کد بالا

حال ما سرویس مدیریت کننده خطای خود را در فریم ورک انگولار داریم، ما در این مرحله می توانیم پیاده سازی منطق برنامه را شروع کنیم، ما قصد داریم یک modal dialog بسازیم که بتواند خطاها را به صورت کاملا صریح نشان دهد، این دیالوگ می تواند شامل یک overlay/backdrop باشد و به صورت داینامیک در داخل DOM قرار گیرد که این کار با کمک CDK فریم ورک انگولار انجام می شود. اجازه دهید آن را نصب کنیم:

npm install @angular/cdk –save

با توجه به داکیومنتیشن ها کامپوننت Overlay ما نیاز به برخی از فایل های از پیش ساخته شده سی اس اس دارد، حالا اگر ما از متریال فریم ورک انگولار در پروژه خود استفاده کنیم یک کار غیر ضروری را انجام داده ایم. اجازه دهید overlay سی اس اس را در فایل styles.css خود ایمپورت کنیم. توجه داشته باشید که اگر شما در حال حاضر از متریال فریم ورک انگولار در اپلیکیشن خود استفاده می کنید نیازی به ایمپورت کردن این فایل ها ندارید.

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

اجازه دهید از متد مدیریت کننده خطای خود برای ایجاد modal dialog خود استفاده کنیم، یک نکته بسیار مهم که باید بدانید این است که سرویس مدیریت کننده خطا بخشی از فاز راه اندازی اپلیکیشن می باشد، از طرفی برای جلوگیری کردن از به وجود آمدن خطاهای تو در تو و به یکدیگر وابسته ما از injector به عنوان تنها پارامتر کانستراکتور استفاده می کنیم. ما زمانی که متد واقعی فراخوانی می شود از سیستم dependency injection استفاده می کنیم. اجازه دهید overlay را از طریق CDK ایمپورت کنیم و آن را به سرویس مدیریت کننده خود در داخل 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);

   }

}

اجازه دهید توجه خود را به سمت modal مدیریت کننده سرویس خود باز گردانیم، یک راه حل ساده ولی زیبا نمایش دادن پیغام خطا و stacktrace می باشد، بیایید یک باتن dismiss به انتهای برنامه خود اضافه کنیم:

 فریم ورک انگولار


اضافه کردن باتن dismiss

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

  }

}

همانطور که می توانید مشاهده کنید کامپوننت به شکلی زیبا طراحی شده است، ما قصد داریم در این جا از دو دستور العمل مهم در قالب خود استفاده کنیم تا دیالوگ ما قابل دسترسی باشد. دستورالعمل اول cdkTrapFocus می باشد که زمانی که یک دیالوگ در حال اجرا است فوکوس را از بین می برد. این به آن معنا است که کاربر نمی تواند بر روی عناصری که در پشت دیالوگ ما وجود دارد فوکوس کند. دومین دستورالعمل cdkTrapFocusAutoCapture می باشد که عنصر اولی که قابل فوکوس باشد را فوکوس می کند. علاوه بر این، این دستور العمل زمانی که دیالوگ بسته می شود فوکوس را بر روی عنصری که آخرین بار فوکوس بر روی آن بود ذخیره می کند.

علاوه بر این برای نمایش دادن ویژگی های خطاها ما باید آنها را با استفاده از کانستراکتور inject کنیم. برای این کار ما نیاز به injectionToken خود داریم. ما همچنین یک منطق ساده نیز برای برنامه خود ساختیم تا بتوانیم یک رویداد dismiss را منتشر کنیم.

 فریم ورک انگولار


متصل کردن موارد گفته شده با سرویس مدیریت کننده خطای خود

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

  }

}

 فریم ورک انگولار


توضیحاتی درباره کد بالا

اجازه دهید بر روی فراهم کردن یک خطا به عنوان یک پارامتر inject شده تمرکز کنیم، همانطور که میبینید کلاس Component Portal انتظار بیش از یک پارامتر را دارد که در ابتدا تنها یک کامپوننت است. پارامتر دوم یک ViewContainerRef می باشد که می تواند بر روی منطق کامپوننت در درخت کامپوننت تاثیر بگذارد. پارامتر سوم متد create Inejctor ما می باشد. همانطور که میبینید این یک شی Portal Injector به ما باز می گرداند، اجازه دهید نگاهی کوتاه به پیاده سازی آن داشته باشیم:

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

 }

}


 فریم ورک انگولار


توضیح درباره کد بالا

همانطور که می بینید کد بالا انتظار یک Injector به عنوان پارامتر اول و یک WeakMap برای توکن های سفارشی سازی شده را دارد، همانطور که می بینید ما دقیقا این کار را با استفاده از ERROR_INJECTOR_TOKEN انجام داده ایم که با خطای ما مرتبط شده است. Portal Injector ساخته شده برای یک مقدار دهی مناسب از ErrorHandler Component ما استفاده می شود، این می تواند اطمینان حاصل کند که ارور مورد نظر در کامپوننت وجود دارد.

در آخر متد attach Portal ما ویژگی مقداردهی شده dismiss کامپوننت ما را باز می گرداند. ما با آن ارتباط برقرار می کنیم و زمانی که تغییر می کند ما dispose() را در داخل overlayRef خود فراخوانی می کنیم و بعد از آن modal dialog ما رد می شود.

 فریم ورک انگولار


توضیحی کلی درباره هر آنچه تا به حال نوشته شده است

کدهای نوشته شده برای ارورهایی از فریم ورک انگولار که زمانی که یک مسئله در کدهای سمت کاربر وجود دارد به وجود می آیند فوق العاده می باشد، اما ما قصد داریم یک اپلیکیشن تحت وب بنویسیم و از endpoint های API استفاده کنیم. بنابراین زمانی که یک REST endpint یک ارور را باز می گرداند چه اتفاقی می افتد؟

ما می توانیم هر خطایی را در سرویس خود مدیریت کنیم اما آیا واقعا ما قصد داریم این کار را انجام دهیم؟ اگر همه چیز خوب باشد خطایی به وجود نمی آید، اگر نیازمندی های خاصی وجود داشته باشد به عنوان مثال مدیریت وضعیت 418، شما می توانید مدیریت کننده آن را در سرویس خود پیاده سازی کنید. اما زمانی که ما با خطاهای رایج مانند خطای 404 و یا خطای 405 رو به رو می شویم ما ممکن است که بخواهیم آن را در همان دیالوگ خطا نمایش دهیم.

اجازه دهید به سرعت جمع بندی کنیم که زمانی که یک Http Error Response به وجود می آید چه اتفاقی می افتد؟ این خطا به صورت async اتفاق می افتد بنابراین احتمالا ما به برخی از مسائل مربوط به تشخیص تغییرات رو برو خواهیم شد. این نوع از ارورها دارای ویژگی های متفاوتی با خطاهای معمولی هستند، بنابراین ما ممکن است نیاز به یک متد sanitizer داشته باشیم. بنابراین اجازه دهید که کار خود را با ساخت یک اینترفیس برای Sanitised Error شروع کنیم:

export interface SanitizedError {

  message: string;

  details: string[];

}

 فریم ورک انگولار


ساخت یک قالب برای کامپوننت مدیریت کننده خطا در فریم ورک انگولار

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

 // ...

}

ما تمام modal خود را در داخل یک <section> محصور کرده ایم و دستورالعمل cdkTrapFocus را نیز به آن اضافه کرده ایم. این دستور العمل از حرکت کاربر در داخل DOM جلوگیری می کند. [cdkTrapFocusAutoCapture]="true" اطمینان حاصل می کند که باتن مربوط به dismiss بلافاصله فوکوس شود. زمانی که modal بسته می شود عنصری که قبلا فوکوس شده بود دوباره به فوکوس خود باز می گردد. ما به سادگی پیغام خطا را همراه با جزئیات آن را با استفاده از *ngFor نمایش می دهیم. اجازه دهید به سرویس مدیریت کننده خطای خود بازگردیم:

 فریم ورک انگولار


سرویس مدیریت کننده خطا در فریم ورک انگولار

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

  }

  // ...

}

با استفاده از متد ساده sanitise Error ما یک شی که بر پایه اینترفیس قبلی تعریف شده ما می باشد را می سازیم، ما نوع خطاهای فریم ورک انگولار را بررسی می کنیم و مطابق با آن داده ها را جمع آوری می کنیم. بخش جذاب تر استفاده از injector برای گرفتن ngZone می باشد. زمانی که یک خطا به صورت ناهمگام وجود می آید معمولا در خارج از محدوده تشخیص تغییرات رخ داده است، ما attach Portal خود را با ngZone.run(/* ... */) محصور می کنیم، بنابراین زمانی که یک Http Error Response به وجود می آید به صورت کاملا مناسب در modal ما اجرا می شود.

با وجود این که این کدها به خوبی در فریم ورک انگولار کار می کنند ولی هنوز هم نیاز به سفارشی سازی دارند، ما از Overlay در ماژول CDk استفاده می کنیم بنابراین گرفتن یک توکن injection برای سفارشی سازی تنظیمات می تواند مفید باشد. یکی دیگر از نواقص این روش این است که زمانی که این ماژول مورد استفاده قرار می گیرد ماژول دیگری نمی تواند برای مدیریت خطاها به کار رود. به عنوان مثال ادغام Sentry با آن ممکن است نیازمند این باشد که شما آن را به صورت مشابه پیاده سازی کنید. علاوه بر این بتوانیم برای این که از هر دو آنها استفاده کنیم ما باید امکان استفاده از hook ها در داخل مدیریت کننده خطای خود را فراهم کنیم. در ابتدا اجازه دهید InjectionToken خود را بسازیم و تنظیمات خود را بر روی آن انجام دهیم:

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

سپس با استفاده از متد 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}

      ]

    };

  }

}

سپس این مدیریت کننده تنظیمات را به داخل سرویس مدیریت کننده خطای خود در فریم ورک انگولار بیاوریم:

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

  }

  // ...

}

حالا ما تقریبا آماده ایم، بیایید هوک مدیریت کننده خطای third-party در فریم ورک انگولار را به داخل اپلیکیشن خود بیاوریم:

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

}

تبریک می گویم شما موفق شدید که این کار را در فریم ورک انگولار انجام دهید.

ایمان مدائنی

نویسنده 1299 مقاله در برنامه نویسان

کاربرانی که از نویسنده این مقاله تشکر کرده اند

تاکنون هیچ کاربری از این پست تشکر نکرده است

در صورتی که در رابطه با این مقاله سوالی دارید، در تاپیک های انجمن مطرح کنید