web-dev-qa-db-fra.com

Angular (v5) est en cours de construction avant la résolution de la promesse APP_INITIALIZER

Je m'attends à ce que Angular attende que ma fonction loadConfig() soit résolue avant de construire d'autres services, mais ce n'est pas le cas.

app.module.ts

export function initializeConfig(config: AppConfig){
    return () => config.loadConfig();
}

@NgModule({
     declarations: [...]
     providers: [
          AppConfig,
         { provide: APP_INITIALIZER, useFactory: initializeConfig, deps: [AppConfig], multi: true }
     ] })
export class AppModule {

}

app.config.ts

@Injectable()
export class AppConfig {

    config: any;

    constructor(
        private injector: Injector
    ){
    }

    public loadConfig() {
        const http = this.injector.get(HttpClient);

        return new Promise((resolve, reject) => {
            http.get('http://mycoolapp.com/env')
                .map((res) => res )
                .catch((err) => {
                    console.log("ERROR getting config data", err );
                    resolve(true);
                    return Observable.throw(err || 'Server error while getting environment');
                })
                .subscribe( (configData) => {
                    console.log("configData: ", configData);
                    this.config = configData;
                    resolve(true);
                });
        });
    }
}

certains-autres-services.ts

@Injectable()
export class SomeOtherService {

    constructor(
        private appConfig: AppConfig
    ) {
         console.log("This is getting called before appConfig's loadConfig method is resolved!");
    }
 }

Le constructeur de SomeOtherService est appelé avant la réception des données du serveur. Il s'agit d'un problème car les champs de SomeOtherService ne sont pas définis sur leurs valeurs appropriées.

Comment puis-je m'assurer que le constructeur de SomeOtherService est appelé uniquement APRÈS que la requête de loadConfig soit résolue?

14
CodyBugstein

J'ai également eu un problème similaire qui m'a permis de résoudre le problème en utilisant des méthodes et des opérateurs observables pour tout faire. Ensuite, à la fin, utilisez simplement la méthode toPromise de Observable pour renvoyer un Promise. C'est aussi plus simple car vous n'avez pas besoin de créer une promesse vous-même.

Le service AppConfig ressemblera alors à quelque chose comme ça:

import { Injectable, Injector } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs/Observable';
import { tap } from 'rxjs/operators/tap';

@Injectable()
export class AppConfig {

    config: any = null;

    constructor(
        private injector: Injector
    ){
    }

    public loadConfig() {
        const http = this.injector.get(HttpClient);

        return http.get('https://jsonplaceholder.typicode.com/posts/1').pipe(
          tap((returnedConfig) => this.config = returnedConfig)
        ).toPromise();
        //return from([1]).toPromise();
    }
}

J'utilise le nouveau opérateurs canalisables dans rxjs qui est recommandé par Google pour Angular 5. L'opérateur tap est équivalent à l'ancien do.

J'ai également créé un échantillon de travail sur stackblitz.com afin que vous puissiez le voir fonctionner. Exemple de lien

5
AlesD

L'injecteur n'attend pas d'observables ou de promesses et il n'y a pas de code qui pourrait y arriver.

Vous devez utiliser Guard ou Resolver personnalisé pour vous assurer que la configuration est chargée avant la fin de la navigation initiale.

2
kemsky
  async loadConfig() {
        const http = this.injector.get(HttpClient);

        const configData = await http.get('http://mycoolapp.com/env')
                    .map((res: Response) => {
                        return res.json();
                    }).catch((err: any) => {
                        return Observable.throw(err);
                    }).toPromise();
                this.config = configData;
        });
    }

L'opérateur d'attente est utilisé pour attendre une promesse. Il ne peut être utilisé qu'à l'intérieur d'une fonction asynchrone.

Cela fonctionne bien.

2
Hardik Patel

Je pense que vous ne devriez pas vous abonner à l'appel http get mais le transformer en promesse avant de résoudre la promesse loadConfig, car le rappel de souscription peut être appelé avant le retour de la demande et résout donc la promesse trop tôt. Essayer:

@Injectable()
export class AppConfig {

    config: any;

    constructor(
        private injector: Injector
    ){
    }

    public loadConfig() {
        const http = this.injector.get(HttpClient);

        return new Promise((resolve, reject) => {
            http.get('http://mycoolapp.com/env')
                .map((res) => res )
                .toPromise()
                .catch((err) => {
                    console.log("ERROR getting config data", err );
                    resolve(true);
                    return Observable.throw(err || 'Server error while getting environment');
                })
                .then( (configData) => {
                    console.log("configData: ", configData);
                    this.config = configData;
                    resolve(true);
                });
        });
    }
}

Je ne l'ai essayé qu'avec un timeout, mais cela a fonctionné. Et j'espère que toPromise() est à la bonne position, car je n'utilise pas vraiment la fonction map.

1
Fussel

Tout d'abord, vous étiez vraiment proche de la bonne solution!

Mais avant de vous expliquer, permettez-moi de vous dire que l'utilisation de subscribe dans un service est souvent une odeur de code.

Cela dit, si vous jetez un œil au code source APP_INITALIZER , il exécute simplement un Promise.all sur tous les initialiseurs disponibles. Promise.all attend lui-même que toutes les promesses se terminent avant de continuer et donc, vous devez renvoyer une promesse de votre fonction si vous voulez Angular attendre cela avant d'amorcer l'application.

Donc @ AlesD 's answer est certainement la bonne voie à suivre.
(et j'essaie juste d'expliquer un peu plus pourquoi)

J'ai fait un tel refactor (pour utiliser APP_INITALIZER) très récemment dans un de mes projets, vous pouvez jeter un œil au PR ici si vous voulez.

Maintenant, si je devais réécrire votre code, je le ferais comme ça:

app.module.ts

export function initializeConfig(config: AppConfig) {
  return () => config.loadConfig().toPromise();
}

@NgModule({
  declarations: [
    //  ...
  ],
  providers: [
    HttpClientModule,
    AppConfig,
    {
      provide: APP_INITIALIZER,
      useFactory: initializeConfig,
      deps: [AppConfig, HttpClientModule],
      multi: true,
    },
  ],
})
export class AppModule {}

app.config.ts;

@Injectable()
export class AppConfig {
  config: any;

  constructor(private http: HttpClient) {}

  // note: instead of any you should put your config type
  public loadConfig(): Observable<any> {
    return this.http.get('http://mycoolapp.com/env').pipe(
      map(res => res),
      tap(configData => (this.config = configData)),
      catchError(err => {
        console.log('ERROR getting config data', err);
        return _throw(err || 'Server error while getting environment');
      })
    );
  }
}
1
maxime1992

Je suis confronté à un problème similaire. Je pense que la différence qui n'a pas été annoncée ici et qui fait que dans d'autres réponses fonctionne bien, mais pas pour l'auteur, c'est là que SomeOtherService est injecté. S'il est injecté dans un autre service, il est possible que l'initialiseur ne soit pas encore résolu. Je pense que les initialiseurs retarderont l'injection de services dans les composants, pas dans d'autres services et cela expliquera pourquoi cela fonctionne dans d'autres réponses. Dans mon cas, j'ai eu ce problème en raison de https://github.com/ngrx/platform/issues/931

0
Maciej Sikorski