简体   繁体   English

在 APP_INITIALIZER 承诺解决之前构建 Angular (v5) 服务

[英]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.我希望 Angular 在构建其他服务之前等待我的loadConfig()函数解析,但事实并非如此。

app.module.ts 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 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. SomeOtherService的构造函数在从服务器接收数据之前被调用。 This is a problem because then the fields in SomeOtherService do not get set to their proper values.这是一个问题,因为SomeOtherService的字段没有设置为其正确的值。

How do I ensure SomeOtherService 's constructor gets called only AFTER the loadConfig 's request is resolved?如何确保SomeOtherService的构造函数仅在loadConfig的请求得到解决后才被调用?

I had also a simmilar issue what solved the issue for me was to use Observable methods and operators to do everything.我还有一个类似的问题,为我解决的问题是使用 Observable 方法和运算符来做所有事情。 Then in the end just use the toPromise method of the Observable to return a Promise .然后最后只需要使用ObservabletoPromise方法返回一个Promise This is also simpler because you don't need to create a promise yourself.这也更简单,因为您不需要自己创建承诺。

The AppConfig service will then look something like that: 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();
    }
}

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.我使用的是新pipeable运营商在由谷歌建议对角5 rxjs的tap运营商就相当于老do运营商。

I have also created a working sample on stackblitz.com so you can se it working.我还在 stackblitz.com 上创建了一个工作示例,以便您可以设置它工作。 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. await 运算符用于等待 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. Injector 不会等待 observables 或 promises 并且没有代码可以让它发生。

You should use custom Guard or Resolver to ensure that config is loaded before initial navigation completes.您应该使用自定义 Guard 或 Resolver 来确保在初始导航完成之前加载配置。

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.但在我解释之前,让我告诉你,使用subscribe服务通常是一种代码味道。

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.也就是说,如果您查看APP_INITALIZER 源代码,它只是在所有可用的初始化Promise.all上运行Promise.all 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. Promise.all 本身在继续之前等待所有承诺完成,因此,如果您希望 Angular 在引导应用程序之前等待它,您应该从您的函数返回一个承诺。

So @AlesD 's answer is definitely the right way to go.所以@AlesD答案绝对是正确的方法。
(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.我最近在我的一个项目中完成了这样的重构(使用APP_INITALIZER ),如果你愿意,你可以看看这里的 PR。

Now, if I had to rewrite your code I'd do it like that:现在,如果我不得不重写你的代码,我会这样做:

app.module.ts 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; 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.我认为您不应该订阅 http get 调用,而是在解析 loadConfig 承诺之前将其转换为承诺,因为订阅回调可能在请求返回之前被调用,因此可以提前解决承诺。 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.我希望toPromise()处于正确的位置,因为我并没有真正使用 map 函数。

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.我认为这里没有宣布并导致在其他答案示例中工作正常但不适用于作者的差异是注入 SomeOtherService 的地方。 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://github.com/ngrx/platform/issues/931 ,我遇到了这个问题

I think you can check where "SomeOtherService" was called in call stack.我认为您可以检查在调用堆栈中调用“SomeOtherService”的位置。 In my case, besides APP_INITIALIZER, I also added HTTP_INTERCEPTORS where "SomeOtherService" is injected there.就我而言,除了 APP_INITIALIZER 之外,我还添加了 HTTP_INTERCEPTORS,其中注入了“SomeOtherService”。 And that makes the service to be called before APP_INITIALIZER completes.这使得服务在 APP_INITIALIZER 完成之前被调用。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

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