前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Angular 异常处理

Angular 异常处理

作者头像
阿宝哥
发布2019-11-05 15:52:34
1.2K0
发布2019-11-05 15:52:34
举报
文章被收录于专栏:全栈修仙之路全栈修仙之路

对于 Angular 应用程序,默认的异常处理是在控制台中输出异常,这对于本地开发和测试阶段,是很方便。但这对于线上环境来说,输出到控制台没有多大的意义。一般情况下,我们希望能自动收集线上环境抛出的异常,并上报到指定的异常收集服务器上,以便于对异常信息进行汇总和分析。

针对上述的需求,我们可以利用 Angular 为我们提供的钩子,来实现自定义异常处理器:

代码语言:javascript
复制
class MyErrorHandler implements ErrorHandler {
  handleError(error) {
    // do something with the exception
  }
}

@NgModule({
  providers: [{provide: ErrorHandler, useClass: MyErrorHandler}]
})
class MyModule {}

通过上面的示例,我们知道要自定义异常处理器需要两个步骤:

  1. 创建异常处理类并实现 ErrorHandler:
代码语言:javascript
复制
export declare class ErrorHandler {
    handleError(error: any): void;
}
  1. ErrorHandler 作为 Token,使用 useClass 的方式配置 provider。

自定义异常处理器

下面我们来根据上述的流程,自定义一个简单的异常处理器,实现自动提交异常信息的功能。这里我们先来定义一个 ErrorService:

代码语言:javascript
复制
import { Injectable } from "@angular/core";
import { HttpClient } from "@angular/common/http";
import { mapTo } from "rxjs/operators";

@Injectable({
  providedIn: "root"
})
export class ErrorService {
  errorServerUrl: "http://xxx.com/";
  constructor(private http: HttpClient) {}

  postError(error: any) {
    this.http
      .post(this.errorServerUrl, error)
      .pipe(mapTo(true))
      .subscribe(res => {
        if (res) console.log("Error has been submited");
      });
  }
}

接下来定义一个异常处理类:

代码语言:javascript
复制
import { ErrorHandler } from "@angular/core";
import { ErrorService } from "./error.service";

class MyErrorHandler implements ErrorHandler {
  constructor(private errorService: ErrorService) {}
  handleError(error) {
    if (error) this.errorService.postError(error);
  }
}

最后我们还需要配置一下 Provider:

代码语言:javascript
复制
@NgModule({
  declarations: [AppComponent, HttpClientModule],
  imports: [BrowserModule],
  providers: [
    {
      provide: ErrorHandler,
      useClass: MyErrorHandler
    }
  ],
  bootstrap: [AppComponent]
})
export class AppModule {}

经过上面的几个步骤,一个简单的异常器就完成了。有的同学可能想进一步了解 Angular 内部的异常处理流程,下面我们来简单介绍一下。

Angular 异常处理机制

配置默认异常处理器

通过浏览 Angular 源码,我们发现在 BrowserModule 模块中会注册默认的 ErrorHandler 处理器:

代码语言:javascript
复制
// packages/platform-browser/src/browser.ts
export const BROWSER_MODULE_PROVIDERS: StaticProvider[] = [
  BROWSER_SANITIZATION_PROVIDERS,
  {provide: ErrorHandler, useFactory: errorHandler, deps: []},
  // ...
]

export function errorHandler(): ErrorHandler {
  return new ErrorHandler();
}

BrowserModule 模块的定义:

代码语言:javascript
复制
// packages/platform-browser/src/browser.ts
@NgModule({
    providers: BROWSER_MODULE_PROVIDERS, 
    exports: [CommonModule, ApplicationModule]
})
export class BrowserModule {
  constructor(@Optional() @SkipSelf() @Inject(BrowserModule) parentModule: BrowserModule|null) {
    if (parentModule) {
      throw new Error(
          `BrowserModule has already been loaded. If you need access to common directives such as NgIf and NgFor from a lazy loaded module, import CommonModule instead.`);
    }
  }
}
启动应用程序

对于使用 Angular CLI 创建的 Angular 应用程序,在 src 目录下会自动生成一个 main.ts 文件:

代码语言:javascript
复制
import { enableProdMode } from '@angular/core';
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';

import { AppModule } from './app/app.module';
import { environment } from './environments/environment';

if (environment.production) {
  enableProdMode();
}

platformBrowserDynamic().bootstrapModule(AppModule)
  .catch(err => console.log(err));

在上面代码中,我们通过调用 platformBrowserDynamic() 返回对象上的 bootstrapModule() 方法来启动我们应用程序。其中 platformBrowserDynamic 定义如下:

代码语言:javascript
复制
export declare const platformBrowserDynamic: (extraProviders?: StaticProvider[] | undefined) => PlatformRef;

这时我就知道调用 platformBrowserDynamic() 方法后会返回 PlatformRef 对象。因此现在我们的主要目标就是分析 PlatformRef 对象,PlatformRef 类 bootstrapModule() 方法的定义如下:

代码语言:javascript
复制
@Injectable()
export class PlatformRef {
  private _modules: NgModuleRef<any>[] = [];

  /** @internal */
  constructor(private _injector: Injector) {}
  
  bootstrapModule<M>(
      moduleType: Type<M>, compilerOptions: (CompilerOptions&BootstrapOptions)|
      Array<CompilerOptions&BootstrapOptions> = []): Promise<NgModuleRef<M>> {
    const options = optionsReducer({}, compilerOptions);
    return compileNgModuleFactory(this.injector, options, moduleType)
        .then(moduleFactory => this.bootstrapModuleFactory(moduleFactory, options));
  }
}

通过观察以上代码,我们发现在完成模块编译后,在 bootstrapModule() 方法内部会继续调用 bootstrapModuleFactory() 方法(源码片段):

代码语言:javascript
复制
// packages/core/src/application_ref.ts
bootstrapModuleFactory<M>(moduleFactory: NgModuleFactory<M>, options?: BootstrapOptions):
      Promise<NgModuleRef<M>> {
    const ngZoneOption = options ? options.ngZone : undefined;
    const ngZone = getNgZone(ngZoneOption);
    const providers: StaticProvider[] = [{provide: NgZone, useValue: ngZone}];

    return ngZone.run(() => {
      const ngZoneInjector = Injector.create(
        {  providers: providers, 
           parent: this.injector, 
           name: moduleFactory.moduleType.name
      });
      const moduleRef = <InternalNgModuleRef<M>>moduleFactory.create(ngZoneInjector);
      const exceptionHandler: ErrorHandler = moduleRef.injector.get(ErrorHandler, null);
      if (!exceptionHandler) {
        throw new Error('No ErrorHandler. Is platform module (BrowserModule) included?');
      }
      moduleRef.onDestroy(() => remove(this._modules, moduleRef));
      ngZone !.runOutsideAngular(
          () => ngZone !.onError.subscribe(
            {next: (error: any) => { 
               exceptionHandler.handleError(error); }}));
      });
    });
}

在 ngZone 对象的 run() 方法内部,我们先调用 Injector 的 create() 方法创建 ngZoneInjector 注入器,然后把它作为参数传给 moduleFactory 对象的 create() 方法,创建根模块对象。接着通过调用根级注入器的 get() 方法,获取 ErrorHandler 对象。

在获取 ErrorHandler 对象之后,通过调用 ngZone !.runOutsideAngular() 方法,启用异常处理器:

代码语言:javascript
复制
ngZone !.runOutsideAngular(
   () => ngZone !.onError.subscribe(
     {next: (error: any) => { 
        exceptionHandler.handleError(error); }}));
});

因为 NgZone 类 onError 属性是一个 EventEmitter 对象:

代码语言:javascript
复制
/**
 * Notifies that an error has been delivered.
 */
readonly onError: EventEmitter<any> = new EventEmitter(false);

所以我们可以订阅该对象,然后执行我们异常处理逻辑:

代码语言:javascript
复制
ngZone !.onError.subscribe(
  { next: (error: any) => { 
        exceptionHandler.handleError(error);
    }
  }
)

其实上面还涉及到 NgZone 的相关知识,感兴趣的同学可以阅读 Angular 2中的Zone 这篇文章。此外在 bootstrapModuleFactory() 方法内部,在完成应用初始化操作之后,内部还会进一步调用 _moduleDoBootstrap() 启动我们的根组件:

代码语言:javascript
复制
return _callAndReportToErrorHandler(exceptionHandler, ngZone !, () => {
     const initStatus: ApplicationInitStatus = 
        moduleRef.injector.get(ApplicationInitStatus);
        initStatus.runInitializers();
        return initStatus.donePromise.then(() => {
          this._moduleDoBootstrap(moduleRef);
          return moduleRef;
        });
});

关于自定义初始化逻辑的说明,感兴趣的同学可以参考我之前的文章 Angular Multi Providers 和 APP_INITIALIZER。接下来我们继续看一下 _moduleDoBootstrap() 方法:

代码语言:javascript
复制
private _moduleDoBootstrap(moduleRef: InternalNgModuleRef<any>): void {
    const appRef = moduleRef.injector.get(ApplicationRef) as ApplicationRef;
    if (moduleRef._bootstrapComponents.length > 0) {
      moduleRef._bootstrapComponents.forEach(f => appRef.bootstrap(f));
    } else if (moduleRef.instance.ngDoBootstrap) {
      moduleRef.instance.ngDoBootstrap(appRef);
    } else {
      throw new Error(
          `The module ${stringify(moduleRef.instance.constructor)} was bootstrapped, but it does not declare "@NgModule.bootstrap" components nor a "ngDoBootstrap" method. ` +
          `Please define one of these.`);
    }
    this._modules.push(moduleRef);
}

上面代码提到了 ApplicationRef 类,该类内部也注入了 ErrorHandler 对象。不过这里我们不会详细展开,主要看一下跟 ErrorHandler 对象相关的处理逻辑:

代码语言:javascript
复制
// packages/core/src/application_ref.ts
@Injectable()
export class ApplicationRef {
    constructor(
      private _zone: NgZone, 
      private _console: Console, 
      private _injector: Injector,
      private _exceptionHandler: ErrorHandler,
      private _componentFactoryResolver: ComponentFactoryResolver,
      private _initStatus: ApplicationInitStatus) {
        this._zone.onMicrotaskEmpty.subscribe(
          {next: () => { this._zone.run(() => { this.tick(); }); }});
    }
}

在 ApplicationRef 构造函数内部,会订阅 NgZone 对象的 onMicrotaskEmpty 属性,即当微任务执行完成后,会调用内部 tick 方法执行变化检测,在变化检测周期如果发生异常时,就会调用我们自定义的异常处理器的 handleError 方法执行相应的异常处理逻辑:

代码语言:javascript
复制
tick(): void {
    if (this._runningTick) {
      throw new Error('ApplicationRef.tick is called recursively');
    }
    const scope = ApplicationRef._tickScope();
    try {
      this._runningTick = true;
      this._views.forEach((view) => view.detectChanges());
      if (this._enforceNoNewChanges) {
        this._views.forEach((view) => view.checkNoChanges());
      }
    } catch (e) {
      // Attention: Don't rethrow as it could cancel subscriptions to Observables!
      this._zone.runOutsideAngular(
          () => this._exceptionHandler.handleError(e)
      );
    } finally {
      this._runningTick = false;
      wtfLeave(scope);
    }
}

总结

本文通过一个简单的示例,简单介绍了在 Angular 项目中如何自定义异常处理器,此外也简单介绍了 Angular 内部的异常处理机制。其实目前市面上也有一些不错的异常监控平台,比如 FunDebug,该平台提供的功能还是蛮强大的,也支持 Angular 或 Ionic 项目,感兴趣的同学可以了解一下 FunDebug Angular 4

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2018/09/07,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 自定义异常处理器
  • Angular 异常处理机制
    • 配置默认异常处理器
      • 启动应用程序
      • 总结
      领券
      问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档