【问题标题】:How to handle/inform users about unrecoverable exceptions in an APP_INITIALIZER?如何处理/通知用户有关 APP_INITIALIZER 中不可恢复的异常?
【发布时间】:2019-05-07 19:04:52
【问题描述】:

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

 providers: [ AppConfig,
        { provide: APP_INITIALIZER, useFactory: (config: AppConfig) => () => config.load(), deps: [AppConfig], multi: true },
        { provide: LocationStrategy, useClass: HashLocationStrategy},
        { provide: ErrorHandler, useClass: GlobalErrorHandler }]

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

@Injectable()
export class AppConfig {

    private config: Object = null;
    constructor(private http: HttpClient) {}

    public getConfig(key: any) {
        return this.config[key];
    }


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

全局异常处理程序:

@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;
    }

}

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

由于AppComponent 没有被加载(这没关系,因为没有配置就无法使用应用程序)并且我的消息/“咆哮”组件是AppComponent 的子组件,我无法显示给用户的消息。

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

【问题讨论】:

    标签: angular typescript initialization


    【解决方案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>
    

    如果引导成功,标签的内容将被替换为角度。

    【讨论】:

    • 这是正确答案!谢谢你终于找到了这个——我试图弄清楚APP_INITIALIZER 中的拒绝最终在哪里结束,结果它抛出了bootstrapModule。再次感谢!
    【解决方案2】:

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

    • 使用initialized 变量扩展配置类:

      @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 中显示给用户。

    【讨论】:

    • 如果 promise 被拒绝会发生什么?是否仍然显示app.component.html..如果初始化过程失败?
    • 就我而言,是的。如果配置的初始化/加载失败,异常会记录在全局错误处理程序中,但 app.component 仍会被加载。
    • 模板中的appConfig.initialized在哪里清除?
    【解决方案3】:

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

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

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

    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;
    

    第一行只是将“失败”类添加到文档&lt;body&gt; 元素中。我们马上就会看到它的效果。

    另外两行 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>
    

    &lt;app-root&gt; 元素中的“正在加载...”文本在 Angular 应用初始化时显示,并在引导过程完成后替换为应用内容。

    确保“哎呀!”如果应用程序无法初始化,则会显示消息,我添加了一个带有一些 CSS 样式的 &lt;style&gt; 块,并在 &lt;app-root&gt; 元素中添加了一些额外内容:

    <!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>
    

    所以现在,默认显示“正在加载...”消息(如果初始化成功,将替换为应用程序内容),但是“哎呀!”如果 &lt;body&gt; 元素具有 failed 类,则会显示消息,这是我们在异常处理程序中添加的。

    当然,你可以做得比简单的&lt;h1&gt; 标签更好——你可以在里面放任何你喜欢的内容。

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

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

    【讨论】:

      【解决方案4】:

      我会更新你的初始化程序捕获而不是抛出 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);
              });
      

      【讨论】:

      • 这个解决方案会导致应用循环重定向:app init->error(http)->redirect ---> app init->error (http)->redirect ---> app init->。 ...
      • 您是否将静态 html 文件添加到 '/relativepath/different.html' 以处理错误,因此您不会得到循环重定向?
      • 是的,我确实添加了,但循环重定向仍然存在。
      【解决方案5】:

      这是我的做法...

      // 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 { ... }
      

      希望对你有所帮助:-)

      【讨论】:

      • 感谢分享,但我需要错误页面 .. 不提醒 :)
      • 好吧,您仍然可以使用纯 javascript 来操作索引页面的 DOM。例如,您可以动态地将 iframe 插入 DOM 并通过它加载您的错误页面,或者在这种情况下一直在其中隐藏一个错误 div? :-)
      【解决方案6】:

      所以如果你想重定向,你可以在你的 src 文件夹中创建一个 error.html 文件。然后在出错后将用户重定向到该文件。

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

       "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);
                      });
              });
          }
      

      【讨论】:

      • 如果我理解正确,则完全不需要重定向。如果应用重定向到localhost:4200/error.html,用户可能会点击重新加载并卡在错误页面。我认为用户应该继续使用localhost:4200/#,但正在向用户显示 ErrorPage.Component。
      【解决方案7】:

      我使用的一个解决方案是创建一个拦截器和一个错误服务。拦截器将捕获所有带有某些我知道是错误的状态代码的 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 行为主体来维护错误的状态。错误组件会将此服务注入其构造函数并订阅它以显示错误详细信息。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2013-04-05
        • 2016-03-30
        • 2017-03-29
        • 1970-01-01
        • 2015-05-26
        • 2011-02-02
        • 2011-03-10
        • 1970-01-01
        相关资源
        最近更新 更多