如何在APP_INITIALIZER中处理/通知用户不可恢复的异常?

时间:2018-02-01 14:08:04

标签: angular typescript initialization

我的Angular5应用程序在应用程序初始化期间从后端加载配置文件(APP_INITIALIZER)。由于应用程序无法在没有它的情况下运行,我的目标是向用户显示无法加载配置的消息。

@Injectable()
export class GlobalErrorHandler implements ErrorHandler {
    constructor( private messageService: MessageService) { }
    handleError(error) {
        // the AppConfig exception cannot be shown with the growl message since the growl component is within the AppComponent
        this.messageService.add({severity: 'error', summary: 'Exception', detail: `Global Exception Handler: ${error.message}`});

        throw error;
    }

}

AppComponent类应该在应用加载之前从后端服务加载配置文件:

AppComponent

全局异常处理程序:

index.html

如果无法加载配置文件,则抛出异常,捕获并发现在全局异常处理程序中重新抛出(在console.log()中未被捕获的HTTPErrorResponse)并且加载微调器永远挂起)

由于json没有加载(这没关系,因为没有配置就不能使用app)而且我的消息/“growl”组件是data的子组件,我无法向用户显示消息。

有没有办法在此阶段在cArray页面中显示消息?我不想将用户重定向到与index.html不同的.html页面,因为用户只需在error.html上重新加载/ f5。

7 个答案:

答案 0 :(得分:3)

我也没有为一个非常类似的问题找到一个好的解决方案,但作为一种解决方法,我就是这样做的:

  • 使用initialized变量扩展config类:

    @Injectable()
    export class AppConfig {       
        public settings: AppSettings = null;
        public initialized = false;
    
        public load() {
            return new Promise((resolve, reject) => {
                this.http.get(environment.serviceUrl + 'config')
                    .catch((error: any) => {                      
                        this.initialized = false;
                        resolve(error);
                        return Observable.throw(error || 'Server error');
                    })
                    .subscribe((responseData: any) => {
                        this.settings = responseData;
                        this.initialized = true;
                        resolve(true);
                    });
            });
        }
    }
    
  • app.component.html内,我会显示应用或错误消息,具体取决于此变量:

    <div *ngIf="!appConfig.initialized">
        <b>Failed to load config!</b>
        <!-- or <app-config-error-page> -->
    </div>
    <div *ngIf="appConfig.initialized" #layoutContainer >
        <!-- the app content -->
    </div>
    
  • 全局异常处理程序:

    @Injectable()
    export class GlobalErrorHandler implements ErrorHandler {
        constructor( private notificationService: NotificationService) { }
        handleError(error) {
            this.notificationService.error('Exception', `${error.message}`);
            return Observable.throw(error);
        }  
    }
    

当然,还可以在配置对象中存储具体的错误消息/异常文本,并在需要时将其显示给app.component / error.page.component中的用户。

答案 1 :(得分:1)

我提供的方法略有不同。我认为,如果我们无法正确初始化Angular应用程序,我们不希望它们继续运行,因为它无法正常工作。相反,我们只想向用户显示消息,然后停止。

背景:在我的初始化代码中,我必须以特定顺序发出两个异步请求:

  1. 我向“自己的” Web服务器(从其提供Angular应用程序的服务器)发出HTTP请求,以获取托管我的后端API的 second Web服务器的主机名

  2. 然后我向第二台Web服务器发出请求以获取更多配置信息。

如果这些请求中的任何一个失败,那么我需要停止并显示一条消息,而不是Angular继续执行引导程序。

这是我的代码之前的简化版本,其中添加了新的异常处理(请注意,我更喜欢使用Promiseasync / await Observable,但这并不重要):

const appInitializer = (
  localConfigurationService: LocalConfigurationService,
  remoteConfigurationService: RemoteConfigurationService
): () => Promise<void> => {

  return async () => {
    try {
      const apiHostName = await localConfigurationService.getApiHostName();
      await remoteConfigurationService.load(apiHostName);
    } catch (e) {
      console.error("Failed to initialize the Angular app!", e);
    }
};

export const appInitializerProvider = {
  provide: APP_INITIALIZER,
  useFactory: appInitializer,
  deps: [LocalConfigurationService, RemoteConfigurationService],
  multi: true
};

该代码会将错误记录到控制台,但随后继续进行引导过程-不是我们想要的。

要修改此代码以(a)显示一条消息,并(b)停止其引导过程,我在console.error调用之后立即添加了以下3行:

      window.document.body.classList.add('failed');

      const forever = new Promise(() => { }); // will never resolve
      await forever;

第一行只是将“失败”类添加到文档<body>元素中。稍后我们将看到什么效果。

另外两行awaitPromise从未解决-换句话说,它们永远等待。这具有使Angular引导程序停止的作用,因此Angular应用程序永远不会出现。

最后的更改是在我的index.html文件中(浏览器加载的HTML文件,该文件“包含”了Angular应用程序)。这是我的更改之前 的样子:

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>My App</title>
  <base href="/">
</head>
<body>
  <app-root>Loading...</app-root>
</body>
</html>

初始化Angular应用程序时,会显示<app-root>元素中的“正在加载...”文本,并且在引导过程完成后会被应用程序内容替换。

确保“哎呀!”如果无法初始化应用程序,则会显示一条消息,我添加了一个<style>块,其中包含一些CSS样式以及<app-root>元素中的一些额外内容:

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>My App</title>
  <base href="/">

  <style type="text/css">
    #failure-message {
      display: none;
    }

    body.failed #loading-message {
      display: none;
    }

    body.failed #failure-message {
      display: block;
    }
  </style>

</head>
<body>
<app-root>
  <h1 id="loading-message">Loading...</h1>
  <h1 id="failure-message">Oops! Something went wrong.</h1>
</app-root>
</body>
</html>

因此,现在,默认情况下会显示“正在加载...”消息(如果初始化成功,则将其替换为应用程序内容),但会显示“糟糕!”。如果<body>元素具有failed类(这是我们在异常处理程序中添加的类),则会显示一条消息。

当然,您可以比简单的<h1>标签做得更好-您可以在其中放置任何喜欢的内容。

所有这些的最终结果是浏览器显示“正在加载...”,然后将Angular应用加载或将“正在加载...”文本替换为“哎呀!”。

请注意,URL不会更改,因此,如果您在浏览器中单击重新加载,它将从头开始,并尝试重新加载Angular应用程序-可能正是您想要的。

答案 2 :(得分:1)

在main.ts中,我以这种方式更改了引导程序:

platformBrowserDynamic().bootstrapModule(AppModule)
.catch(err => {
    // error should be logged into console by defaultErrorLogger, so no extra logging is necessary here
    // console.log(err);
    // show error to user
    const errorMsgElement = document.querySelector('#errorMsgElement');
    let message = 'Application initialization failed';
    if (err) {
        if (err.message) {
            message = message + ': ' + err.message;
        } else {
            message = message + ': ' + err;
        }
    }
    errorMsgElement.textContent = message;
});

捕获任何发生的异常,并将消息设置为html元素。该元素在app.root标记的index.html中定义,例如

<app-root><div id="errorMsgElement" style="padding: 20% 0; text-align: center;"></div> 
</app-root>

如果引导成功,标记的内容将替换为angular。

答案 3 :(得分:0)

我会更新你的初始化程序捕获而不是抛出toGlobalErrorHandler

   this.http.get(environment.serviceUrl + 'config/config')
        .catch((error: any) => {
            window.location.href = '/relativepath/different.html';
            return Observable.throw(error || 'Server error')
        })
        .subscribe((responseData) => {
            this.config = responseData;
            this.config['service_endpoint'] = environment.serviceUrl;
            resolve(true);
        });

答案 4 :(得分:0)

我是这样做的......

// app.module.ts

export function loadAppsettings(http: HttpClient) {
  return async () => {
    try {
      const as = await http
          .get<IAppsettings>(`//${window.location.host}/appsettings.json`)
          .toPromise();
      // `appsettings` is a global constant of type IAppsettings
      Object.assign(appsettings, as); 
      return as;
    }
    catch (e) {
      alert('Exception message...');
      throw e;
    }
  };
}

@NgModule({
  // ...
  providers: [
    // ...
    { 
      provide: APP_INITIALIZER, 
      useFactory: loadAppsettings, 
      multi: true, 
      deps: [HttpClient]
    },
  ]
})
export class AppModule { ... }

希望它有所帮助: - )

答案 5 :(得分:0)

因此,如果要重定向,可以在src文件夹中创建error.html文件。然后在出错后将用户重定向到该文件。

创建error.html后,在angular.json文件中将其添加到资源,这样它就不会加载角度应用程序并重定向它。

 "assets": [
       {
        "glob": "**/*",
        "input": "src/assets",
        "output": "/assets"
       },
       {
         "glob": "error.html",
         "input": "src",
         "output": "/"
       }
    ],

然后将您的加载功能更改为

public load() {
        return new Promise((resolve, reject) => {
            this.http.get(environment.serviceUrl + 'config/config')
                .catch((error: any) => {                  
                    window.location.href = '/error.html';
                    return Observable.throw(error || 'Server error');
                })
                .subscribe((responseData) => {
                    this.config = responseData;
                    this.config['service_endpoint'] = environment.serviceUrl;                  
                    resolve(true);
                });
        });
    }

答案 6 :(得分:0)

我使用的解决方案是创建一个拦截器和一个错误服务。拦截器将捕获所有HTTP错误以及某些我认为是错误的状态代码。

拦截器示例

import { Injectable } from '@angular/core';
import { HttpEvent, HttpInterceptor, HttpHandler, HttpRequest, HttpErrorResponse } from '@angular/common/http';
import { Router } from '@angular/router';

import { ErrorService } from '@app/@pages/error/error.service';

import { catchError } from 'rxjs/operators';
import { Observable, throwError } from 'rxjs';

@Injectable()
export class HttpErrorInterceptor implements HttpInterceptor {

    constructor(
        private router: Router,
        private errorSvc: ErrorService
    ) { }

    intercept(req: HttpRequest<any>, next: HttpHandler) {
        return next
            .handle(req)
            .pipe(
                catchError((error: any, resp: Observable<HttpEvent<any>>) => {
                    if (error instanceof HttpErrorResponse && (error.status === 0 || error.status > 400)) {
                        this.errorSvc.setHttpError(error);
                        this.router.navigate(['/error']);
                    }
                    return throwError(error);
                })
            );
    }
}

错误服务示例

import { Injectable } from '@angular/core';

import { LocalStorageService } from 'angular-2-local-storage';

import { BehaviorSubject, Observable } from 'rxjs';
import { HttpErrorResponse } from '@angular/common/http';

@Injectable({
    providedIn: 'root'
})
export class ErrorService {

    private error$: BehaviorSubject<HttpErrorResponse> = new BehaviorSubject(this.localStorageSvc.get<HttpErrorResponse>('error'));

    constructor(private localStorageSvc: LocalStorageService) { }

    setHttpError(error: HttpErrorResponse): void {
        this.localStorageSvc.set('error', error);
        this.error$.next(error);
    }

    getHttpError(): Observable<HttpErrorResponse> {
        return this.error$.asObservable();
    }
}

所以这是如何工作的,当您尝试通过APP_INITIALIZER加载配置数据时,拦截器将捕获错误。发现错误后,应将应用程序路由到错误页面。错误服务将使用本地存储和rxjs行为主题维护错误状态。错误组件会将此服务注入其构造函数,并订阅该服务以显示错误详细信息。