Angular 异常处理

2019-11-05 15:52:34 浏览数 (1)

对于 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。

0 人点赞