web-dev-qa-db-fra.com

Comment gérer/informer les utilisateurs sur les exceptions irrécupérables dans un APP_INITIALIZER?

Mon application Angular5 charge un fichier de configuration à partir du backend lors de l'initialisation de l'application (APP_INITIALIZER). Puisque l'application ne peut pas être exécutée sans elle, mon objectif est de montrer à l'utilisateur que la configuration n'a pas pu être chargée.

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

La classe AppConfig devrait charger un fichier de configuration à partir d'un service backend avant le chargement de l'application:

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

Gestionnaire d'exception globale: 

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

}

Si le fichier de configuration ne peut pas être chargé, une exception est levée, interceptée et rediffusée dans le gestionnaire d'exceptions global (HTTPErrorResponse non capturé dans console.log ()) et le spinner en cours de chargement est suspendu pour toujours.

Étant donné que la AppComponent n'est pas chargée (ce qui est correct car l'application ne peut pas être utilisée sans configuration) et que mon message/composant "Growl" est un sous-composant de la AppComponent, je ne peux pas afficher de message à l'utilisateur.

Est-il possible d'afficher un message sur la page index.html au cours de cette étape? Je ne voudrais pas rediriger l'utilisateur vers une page .html différente de celle de l'index.html, car les utilisateurs rechargeront simplement/f5 sur le fichier error.html.

16
netik

Je n'ai pas trouvé de solution satisfaisante à un problème très similaire également, mais voici une solution:

  • Étendez la classe de configuration avec une variable 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);
                    });
            });
        }
    }
    
  • Et dans le app.component.html, je montre l'application ou un message d'erreur, en fonction de cette variable:

    <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>
    
  • Le gestionnaire d'exception global:

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

Bien sûr, vous pouvez également stocker le message d'erreur concret/texte d'exception dans l'objet config et le montrer aux utilisateurs dans app.component/error.page.component si nécessaire.

3
user66875

Donc, si vous voulez rediriger, vous pouvez créer un fichier error.html dans votre dossier src. Puis redirigez l'utilisateur vers ce fichier après une erreur.

Après avoir créé le fichier error.html, ajoutez-le aux ressources de votre fichier angular.json afin qu'il ne charge pas d'application angulaire et ne le redirige pas.

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

Puis changez votre fonction de charge en 

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);
                });
        });
    }
0
Okan Aslankan

Voici comment je le fais ...

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

J'espère que ça aide un peu :-)

0
Heehaaw

J'ai une approche légèrement différente à offrir. À mon avis, nous ne voulons pas que l'amorçage de l'application Angular se poursuive si nous ne pouvons pas l'initialiser correctement, car cela ne fonctionnera pas correctement. Au lieu de cela, nous voulons simplement afficher un message à l'utilisateur et arrêter.

Background : dans mon code d'initialisation, je dois faire deux requêtes asynchrones dans un ordre particulier:

  1. Je soumets une requête HTTP à mon "propre" serveur Web (d'où est servie l'application Angular), afin d'obtenir le nom d'hôte du serveur Web second hébergeant mon API back-end.

  2. Je fais ensuite une demande à ce deuxième serveur Web pour récupérer des informations de configuration supplémentaires.

Si l'une de ces demandes échoue, je dois alors m'arrêter et afficher un message plutôt que de poursuivre le processus d'amorçage avec Angular.

Voici une version simplifiée de mon code before en ajoutant le nouveau traitement des exceptions (notez que je préfère utiliser Promise et asyncawait au lieu de Observable, mais ce n'est pas vraiment pertinent):

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

Ce code enregistre l'erreur sur la console, mais continue ensuite avec le processus d'amorçage - pas ce que nous voulons.

Pour modifier ce code afin (a) d'afficher un message et (b) d'arrêter le processus d'amorçage dans ses traces, j'ai ajouté les 3 lignes suivantes immédiatement après cet appel console.error:

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

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

La première ligne ajoute simplement la classe 'en échec' à l'élément <body> du document. Nous verrons quel effet cela a dans un instant. 

Les deux autres lignes await une Promise qui n'est jamais résolue - en d'autres termes, elles attendent pour toujours. Cela a pour effet de bloquer le processus de démarrage Angular, de sorte que l'application Angular n'apparaît jamais. 

La modification finale se trouve dans mon fichier index.html (le fichier HTML chargé par le navigateur et "contenant" l'application Angular). Voici à quoi cela ressemblait _/avant mes modifications:

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

Ce texte "chargement ..." dans l'élément <app-root> est affiché lors de l'initialisation de l'application Angular et est remplacé par le contenu de l'application une fois le processus d'amorçage terminé.

Pour s'assurer qu'un "Oops!" message est affiché si l'application ne peut pas être initialisée, j'ai ajouté un bloc <style> avec des styles CSS et du contenu supplémentaire dans l'élément <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>

Alors maintenant, le message "chargement ..." est affiché par défaut (à remplacer par le contenu de l'application si l'initialisation est réussie), mais "oops!" message est affiché à la place si l'élément <body> a la classe failed - ce que nous ajoutons dans notre gestionnaire d'exceptions.

Bien sûr, vous pouvez faire mieux qu’un simple tag <h1> - vous pouvez y insérer le contenu que vous voulez.

L’effet net de tout cela est que le navigateur affiche "loading ..." puis l’application Angular se charge ou le texte "loading ..." est remplacé par "oops!".

Notez que l'URL n'est pas modifiée. Par conséquent, si vous appuyez sur reload dans le navigateur, il recommence à zéro et essaie de charger à nouveau l'application Angular - ce qui est probablement ce que vous voulez.

0
Gary McGill

Je mettrais à jour votre capture d'initialiseur plutôt que de lancer 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);
        });
0
DNA