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

你。 提交于 2020-05-25 09:34:30

问题


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?


回答1:


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




回答2:


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.




回答3:


  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.




回答4:


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



回答5:


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.




回答6:


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



来源:https://stackoverflow.com/questions/49163532/angular-v5-service-is-getting-constructed-before-app-initializer-promise-resol

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!