简体   繁体   中英

Angular (v5) service is getting constructed before APP_INITIALIZER promise resolves

I'm expecting Angular to wait until my loadConfig() function resolves before constructing other services, but it is not.

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

The constructor of SomeOtherService is getting called before the data is received from the server. This is a problem because then the fields in SomeOtherService do not get set to their proper values.

How do I ensure SomeOtherService 's constructor gets called only AFTER the loadConfig 's request is resolved?

I had also a simmilar issue what solved the issue for me was to use Observable methods and operators to do everything. Then in the end just use the toPromise method of the Observable to return a Promise . This is also simpler because you don't need to create a promise yourself.

The AppConfig service will then look something like that:

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

I'm using the new pipeable operators in rxjs which is recommended by Google for Angular 5. The tap operator is equivalent to the old do operator.

I have also created a working sample on stackblitz.com so you can se it working. Sample link

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

The await operator is used to wait for a Promise. It can only be used inside an async function.

It is working fine.

Injector does not wait for observables or promises and there is no code that could make it happen.

You should use custom Guard or Resolver to ensure that config is loaded before initial navigation completes.

First of all, you were really close to the right solution!

But before I explain, let me tell you that using subscribe into a service is often a code smell.

That said, if you take a look to the APP_INITALIZER source code it's just running a Promise.all on all the available initializer. Promise.all is itself waiting for all the promises to finish before continuing and thus, you should return a promise from your function if you want Angular to wait for that before bootstrapping the app.

So @AlesD 's answer is definitely the right way to go.
(and I'm just trying to explain a bit more why)

I've done such a refactor (to use APP_INITALIZER ) very recently into one of my projects, you can take a look to the PR here if you want.

Now, if I had to rewrite your code I'd do it like that:

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

I think you should not subscribe to the http get call but turn it into a promise before resolving the loadConfig promise, because the callback to subscribe may be called before the request returned and therefore resolves the promise to early. Try:

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

I only tried it with a timeout, but that worked. And I hope that toPromise() is at the correct position, due I'm not really using the map function.

I'm facing a similar issue. I think the difference which wasn't announced here and causes that in other answers example works fine but not for the author is where SomeOtherService is injected. If it is injected into some other service it is possible that the initializer will not be resolved yet. I think the initializers will delay injecting services into components, not into other services and that will explain why it works in other answers. In my case, I had this issue due to https://github.com/ngrx/platform/issues/931

I think you can check where "SomeOtherService" was called in call stack. In my case, besides APP_INITIALIZER, I also added HTTP_INTERCEPTORS where "SomeOtherService" is injected there. And that makes the service to be called before APP_INITIALIZER completes.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM