angular5 如何抛异常_Angular 异常处理

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

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

2

3

4

5

6

7

8

9

10class MyErrorHandler implements ErrorHandler {

handleError(error) {

// do something with the exception

}

}

@NgModule({

providers: [{provide: ErrorHandler, useClass: MyErrorHandler}]

})

class MyModule {}

通过上面的示例,我们知道要自定义异常处理器需要两个步骤:创建异常处理类并实现 ErrorHandler:1

2

3export declare class ErrorHandler {

handleError(error: any): void;

}以 ErrorHandler 作为 Token,使用 useClass 的方式配置 provider。

自定义异常处理器

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

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20import { 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");

});

}

}

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

2

3

4

5

6

7

8

9import { 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:1

2

3

4

5

6

7

8

9

10

11

12@NgModule({

declarations: [AppComponent, HttpClientModule],

imports: [BrowserModule],

providers: [

{

provide: ErrorHandler,

useClass: MyErrorHandler

}

],

bootstrap: [AppComponent]

})

export class AppModule {}

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

Angular 异常处理机制

配置默认异常处理器

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

2

3

4

5

6

7

8

9

10// 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 模块的定义:1

2

3

4

5

6

7

8

9

10

11

12

13// 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 文件:1

2

3

4

5

6

7

8

9

10

11

12import { 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 定义如下:1export declare const platformBrowserDynamic: (extraProviders?: StaticProvider[] | undefined) => PlatformRef;

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

2

3

4

5

6

7

8

9

10

11

12

13

14

15@Injectable()

export class PlatformRef {

private _modules: NgModuleRef[] = [];

/** @internal */

constructor(private _injector: Injector) {}

bootstrapModule(

moduleType: Type, compilerOptions: (CompilerOptions&BootstrapOptions)|

Array = []): Promise> {

const options = optionsReducer({}, compilerOptions);

return compileNgModuleFactory(this.injector, options, moduleType)

.then(moduleFactory => this.bootstrapModuleFactory(moduleFactory, options));

}

}

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

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26// packages/core/src/application_ref.ts

bootstrapModuleFactory(moduleFactory: NgModuleFactory, options?: BootstrapOptions):

Promise> {

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 = >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() 方法,启用异常处理器:1

2

3

4

5ngZone !.runOutsideAngular(

() => ngZone !.onError.subscribe(

{next: (error: any) => {

exceptionHandler.handleError(error); }}));

});

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

2

3

4/**

* Notifies that an error has been delivered.

*/

readonly onError: EventEmitter = new EventEmitter(false);

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

2

3

4

5

6ngZone !.onError.subscribe(

{ next: (error: any) => {

exceptionHandler.handleError(error);

}

}

)

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

2

3

4

5

6

7

8

9return _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() 方法:1

2

3

4

5

6

7

8

9

10

11

12

13private _moduleDoBootstrap(moduleRef: InternalNgModuleRef): 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 对象相关的处理逻辑:1

2

3

4

5

6

7

8

9

10

11

12

13

14// 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 方法执行相应的异常处理逻辑:1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21tick(): 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。

你可能感兴趣的:(angular5,如何抛异常)