Как обрабатывать/информировать пользователей об неисправимых исключениях в APP_INITIALIZER?

Приложение My 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;
    }

}

Если файл конфигурации не может быть загружен, исключение будет выбрано, поймано и восстановлено в глобальном обработчике исключений (неотображаемый HTTPErrorResponse в console.log()), а загрузчик будет висит навсегда)

Поскольку AppComponent не загружается (это нормально, поскольку приложение не может использоваться без конфигурации), и поскольку мой компонент message/"growl" является вспомогательным компонентом AppComponent, я не могу отображать сообщение пользователю.

Есть ли способ показать сообщение на странице index.html на этом этапе? Я не хотел бы перенаправлять пользователя на другую.html-страницу, чем index.html, так как тогда пользователи просто перезагрузили /f5 в файле error.html.

Ответ 1

Я тоже не нашел хорошего решения для очень похожего вопроса, но в качестве обходного пути я так и сделал:

  • Расширьте класс конфигурации с 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);
        }  
    }
    

Конечно, можно также сохранить конкретный текст сообщения об ошибке/исключение в объекте config и показать его пользователям в app.component/error.page.component, если это необходимо.

Ответ 2

У меня есть немного другой подход, чтобы предложить. По моему мнению, мы не хотим, чтобы начальная загрузка приложения Angular продолжалась, если мы не можем правильно его инициализировать, поскольку он не будет работать должным образом. Вместо этого мы просто хотим показать сообщение пользователю и остановиться.

Справочная информация: в моем коде инициализации я должен сделать два асинхронных запроса в определенном порядке:

  1. Я делаю HTTP-запрос к своему "собственному" веб-серверу (откуда подается приложение Angular), чтобы получить имя хоста второго веб-сервера, на котором размещен мой внутренний API.

  2. Затем я делаю запрос к этому второму веб-серверу, чтобы получить дополнительную информацию о конфигурации.

Если какой-либо из этих запросов завершится неудачно, мне нужно остановиться и показать сообщение, а не Angular, продолжая процесс начальной загрузки.

Вот упрощенная версия моего кода перед добавлением новой обработки исключений (обратите внимание, что я предпочитаю использовать Promise и async/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
};

Этот код будет регистрировать ошибку на консоли, но затем продолжит процесс начальной загрузки - не то, что нам нужно.

Чтобы изменить этот код, чтобы (а) отобразить сообщение и (б) остановить процесс начальной загрузки на его дорожках, я добавил следующие 3 строки сразу после вызова console.error:

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

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

Первая строка просто добавляет класс "fail" к элементу документа <body>. Посмотрим, какой эффект это окажет через минуту.

Две другие строки await Promise, которое никогда не будет решено - другими словами, они ждут вечно. Это приводит к остановке процесса начальной загрузки 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>

Этот текст "loading..." в элементе <app-root> отображается во время инициализации приложения Angular и заменяется содержимым приложения после завершения процесса начальной загрузки.

Чтобы убедиться, что "ой!" сообщение отображается, если приложение не может быть инициализировано, я добавил блок <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> - вы можете поместить туда любой контент, который вам нравится.

Общий эффект всего этого заключается в том, что браузер отображает "loading...", а затем либо загружается приложение Angular, либо текст "loading..." заменяется на "упс!".

Обратите внимание, что URL-адрес не изменился, поэтому, если вы нажмете перезагрузить в браузере, он начнется с нуля и попытается загрузить приложение Angular снова - что, вероятно, то, что вы хотите.

Ответ 3

Я бы обновил ваш идентификатор инициализатора, а не бросал 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

Вот как я это делаю...

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

Поэтому, если вы хотите перенаправить, вы можете создать файл error.html внутри вашей папки src. Затем перенаправите пользователя в этот файл после ошибки.

После создания файла 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

Решением, которое я использовал, было создание перехватчика и службы ошибок. Перехватчик будет перехватывать все ошибки 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 для поддержания состояния ошибки. Компонент error внедрит этот сервис в свой конструктор и подпишется на него для отображения подробностей ошибки.

Ответ 7

В 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. Элемент определен в index.html в теге app-root, например

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

Содержимое тега заменяется угловым, если загрузка прошла успешно.