对于 Angular 应用程序,默认的异常处理是在控制台中输出异常,这对于本地开发和测试阶段,是很方便。但这对于线上环境来说,输出到控制台没有多大的意义。一般情况下,我们希望能自动收集线上环境抛出的异常,并上报到指定的异常收集服务器上,以便于对异常信息进行汇总和分析。
针对上述的需求,我们可以利用 Angular 为我们提供的钩子,来实现自定义异常处理器:
代码语言:javascript复制class MyErrorHandler implements ErrorHandler {
handleError(error) {
// do something with the exception
}
}
@NgModule({
providers: [{provide: ErrorHandler, useClass: MyErrorHandler}]
})
class MyModule {}
通过上面的示例,我们知道要自定义异常处理器需要两个步骤:
- 创建异常处理类并实现 ErrorHandler:
export declare class ErrorHandler {
handleError(error: any): void;
}
- 以
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 定义如下:
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。