在APP_INITIALIZER Promise解析之前正在构造Angular(v5)服务

问题描述 投票:14回答:6

我期望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的请求得到解决之后才被调用?

angular dependency-injection angular-services bootstrapping angular-di
6个回答
5
投票

我也有一个类似的问题,为我解决了这个问题,就是使用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();
    }
}

我正在rxjs中使用新的pipeable operators,这是Google为Angular 5推荐的。tap运算符等效于旧的do运算符。

我还在stackblitz.com上创建了一个工作示例,因此您可以确保它正常工作。 Sample link


2
投票

Injector不会等待可观察到的或应许的,并且没有代码可以实现它。

您应该使用自定义Guard或Resolver来确保在初始导航完成之前已加载配置。


2
投票
  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;
        });
    }

等待操作符用于等待Promise。它只能在异步函数中使用。

工作正常。


1
投票

首先,您真的很接近正确的解决方案!

但是在我解释之前,让我告诉您,在服务中使用subscribe通常是代码的味道。

也就是说,如果您查看APP_INITALIZER source code,它只是在所有可用的初始化程序上运行Promise.all。 Promise.all本身在等待所有诺言完成之后再继续,因此,如果希望Angular在启动应用程序之前等待它,则应从函数中返回一个诺言。

所以@AlesDanswer绝对是正确的选择。(我只是想解释更多原因)

我最近在我的一个项目中进行了这样的重构(使用APP_INITALIZER),如果需要,可以查看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
投票

我认为您不应该订阅http get调用,而应在解决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()处于正确的位置,因为我没有真正使用map函数。


0
投票

我面临类似的问题。我认为未在此处宣布的差异导致在其他答案示例中效果很好,但不适用于作者的是SomeOtherService注入的地方。如果将其注入到其他服务中,则可能无法解析初始化程序。正如我在有角度的核心源代码中看到的那样,首先创建模块,然后同时运行提供程序,然后运行初始化程序。我认为初始化器会延迟将服务注入组件而不是其他服务,这将解释其在其他答案中的作用。 enter image description here

© www.soinside.com 2019 - 2024. All rights reserved.