web-dev-qa-db-ja.com

Angular(v5)サービスはAPP_INITIALIZERプロミスが解決する前に構築されています

Angularが他のサービスを構築する前にloadConfig()関数が解決するまで待つことを期待していますが、そうではありません。

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

some-other-service.ts

@Injectable()
export class SomeOtherService {

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

SomeOtherServiceのコンストラクターは、データがサーバーから受信される前に呼び出されます。 SomeOtherServiceのフィールドが適切な値に設定されないため、これは問題です。

SomeOtherServiceのリクエストが解決された後でのみloadConfigのコンストラクターが確実に呼び出されるようにするにはどうすればよいですか?

14
CodyBugstein

私も同様の問題を抱えていましたが、この問題を解決したのは、Observableメソッドと演算子を使用してすべてを実行することでした。最後に、toPromiseObservableメソッドを使用してPromiseを返します。自分でプロミスを作成する必要がないため、これも簡単です。

その場合、AppConfigサービスは次のようになります。

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

Googleが推奨するrxjsの新しい pipeable演算子 を使用していますAngular 5. 5. tap演算子は、古いdo演算子と同等です。

また、stackblitz.comで実際に動作するサンプルを作成しました。 サンプルリンク

5
AlesD

インジェクターはオブザーバブルまたはプロミスを待たず、それを引き起こすコードはありません。

カスタムガードまたはリゾルバーを使用して、最初のナビゲーションが完了する前に構成が読み込まれるようにする必要があります。

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

Awaitオペレーターは、Promiseを待機するために使用されます。非同期関数内でのみ使用できます。

正常に動作しています。

2
Hardik Patel

リクエストを返す前にサブスクライブするコールバックが呼び出される可能性があるため、http getコールをサブスクライブするのではなく、promiseに変換してからloadConfigプロミスを解決してください。試してください:

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

私はタイムアウトでそれを試しましたが、それはうまくいきました。そして、私は実際にマップ関数を使用していないため、toPromise()が正しい位置にあることを願っています。

1
Fussel

まず第一に、あなたは本当に正しい解決策に本当に近づいていました!

しかし、説明する前に、サービスにsubscribeを使用することは、多くの場合コードのにおいであることをお伝えします。

そうは言っても、 APP_INITALIZERソースコード を見ると、Promise.all利用可能なすべての初期化子。 Promise.all自体は続行する前にすべてのpromiseが完了するのを待機しているため、Angularでアプリをブートストラップする前にそれを待機する場合は、関数からpromiseを返す必要があります。

だから @ AlesDanswer は間違いなく正しい方法です
(そして、私はその理由をもう少し説明しようとしています)

私はそのようなリファクタリングをしました(APP_INITALIZER)ごく最近、私のプロジェクトの1つに参加しました。必要に応じて、PR here をご覧ください。

今、私があなたのコードを書き直さなければならなかったならば、私はそのようにします:

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

同様の問題に直面しています。ここで発表されなかった違いと他の回答の例でうまく機能する原因になると思いますが、著者にとってはそうではなく、SomeOtherServiceが挿入されます。他のサービスに注入された場合、初期化子がまだ解決されていない可能性があります。イニシャライザは、他のサービスではなくコンポーネントへのサービスの注入を遅らせ、それが他の回答で機能する理由を説明すると思います。私の場合、 https://github.com/ngrx/platform/issues/931 が原因でこの問題が発生しました

0
Maciej Sikorski