Angular 导入的模块不等待 APP_INITIALIZER

Posted

技术标签:

【中文标题】Angular 导入的模块不等待 APP_INITIALIZER【英文标题】:Angular imported modules do not wait for APP_INITIALIZER 【发布时间】:2021-06-25 19:45:26 【问题描述】:

我正在尝试在 Angular 11 应用中使用 auth0/auth0-angular 库。

我正在关注loading config dynamically 上的部分。

它提供了这个示例应用模块代码:

// app.module.ts
// ---------------------------
import  AuthModule, AuthClientConfig  from '@auth0/auth0-angular';

// Provide an initializer function that returns a Promise
function configInitializer(
  handler: HttpBackend,
  config: AuthClientConfig
) 
  return () =>
    new HttpClient(handler)
      .get('/config')
      .toPromise()
      .then((loadedConfig: any) => config.set(loadedConfig));   // Set the config that was loaded asynchronously here


// Provide APP_INITIALIZER with this function. Note that there is no config passed to AuthModule.forRoot
imports: [
  // other imports..

  HttpClientModule,
  AuthModule.forRoot(),   //<- don't pass any config here
],
providers: [
  
    provide: APP_INITIALIZER,
    useFactory: configInitializer,    // <- pass your initializer function here
    deps: [HttpBackend, AuthClientConfig],
    multi: true,
  ,
],

简而言之,它使用APP_INITIALIZER 提供程序通过Promise 动态加载配置,这应该在实例化Auth0 库的AuthModule 之前完成,以便它具有从API 加载的适当Auth0 配置值并且 AuthClientConfig.set(...) 已提前使用这些值调用。

Angular APP_INITIALIZER documentation 说:

如果这些函数中的任何一个返回 Promise,则在解决 Promise 之前初始化不会完成。

所以,他们的例子从表面上看是有道理的。

但是,当我尝试在自己的应用中实际实施此解决方案时,我收到以下错误:

Error: Configuration must be specified either through AuthModule.forRoot or through AuthClientConfig.set

这表明 AuthModule 在加载和设置配置之前已被实例化。

在我看来,Angular 在开始实例化导入的模块之前实际上并没有等待Promise 解析。

我认为这个 StackBlitz demo 在没有任何 Auth0 依赖项的简化示例中演示了该问题。

在这个例子中,我希望 TestModulePromise 解析之后才会被实例化,所以我应该看到以下控制台输出:

Inside factory method
Inside promise
Inside timeout
TestModule constructor

但我实际看到的是这样的:

TestModule constructor
Inside factory method
Inside promise
Inside timeout

有人可以帮我理解APP_INITIALIZER 的确切性质,即它何时被调用,Angular 何时等待Promise 解决,Angular 何时开始实例化其他模块,为什么我的 Auth0 设置不能是否正确加载等?

【问题讨论】:

【参考方案1】:

TL;DR - 我最终解决了这个问题,方法是在引导应用程序之前在 main.ts 中加载配置,然后通过自定义注入令牌使配置可用,然后我的应用配置服务不可用不需要等待它通过 HTTP 加载,因为它已经可用。

详情

我的AppConfig 界面的 sn-p:

export interface AppConfig 
  auth: 
    auth0_audience: string,
    auth0_domain: string,
    auth0_client_id: string,
  ;

我的常量文件中的自定义InjectionToken

 const APP_CONFIG: InjectionToken<AppConfig>
  = new InjectionToken<AppConfig>('Application Configuration');

main.ts:

fetch('/config.json')
  .then(response => response.json())
  .then((config: AppConfig) => 
    if (environment.production) 
      enableProdMode();
    

    platformBrowserDynamic([
       provide: APP_CONFIG, useValue: config ,
    ])
      .bootstrapModule(AppModule)
      .catch(err => console.error(err));
  );

然后在我的主 AppModule 中导入 Auth0 AuthModule.forRoot() 而不进行配置并调用我自己的 AppConfigService 来配置 AuthModule

我仍然需要 APP_INITIALIZER 依赖于 AppConfigService 并返回一个 Promise 这以某种方式使 Angular 等到 AppConfigService 构造函数被调用,但否则它不会t 做任何事情(并且仍然不会延迟 AuthModule 被初始化),所以我立即解决它。

AppModule:

@NgModule(
  declarations: [
    ...
  ],
  imports: [
    AuthModule.forRoot(),
    ...
  ],
  providers: [
    AppConfigService,
    
      provide: APP_INITIALIZER,
      useFactory: () => () => 
        return new Promise(resolve => 
          resolve();
        );
      ,
      deps: [ AppConfigService ],
      multi: true,
    ,
    
      provide: HTTP_INTERCEPTORS,
      useClass: AuthHttpInterceptor,
      multi: true,
    ,
  ],
  bootstrap: [ AppComponent ],
)
export class AppModule  

最后,AppConfigService

@Injectable()
export class AppConfigService 

  constructor(
    @Inject(APP_CONFIG) private readonly appConfig: AppConfig,
    private authClientConfig: AuthClientConfig,
  ) 
    this.authClientConfig.set(
      clientId: this.appConfig.auth.auth0_client_id,
      domain: this.appConfig.auth.auth0_domain,
      audience: this.appConfig.auth.auth0_audience,
      httpInterceptor: 
        allowedList: [
          ...
        ],
      ,
    );
  

这一切似乎都运行良好,尽管我仍然不了解 APP_INITIALIZER 的确切性质,而且我不太乐意在构造函数中调用 Auth0 客户端配置的 set 方法而不是异步“加载”文档建议的方法。

【讨论】:

嘿伙计,谢谢。我花了一整天,试图让 APP_INITIALIZER 触发(没有成功)。使用 main.ts 方法,我能够注入 AppConfig 类。我不需要 APP_INITIALIZER 步骤,因为配置直接注入到 Auth0AuthService 中,然后使用它来创建 WebAuth 客户端。【参考方案2】:

我认为您可能需要将该 api 调用包装在 Promise 中。

function configInitializer(handler: HttpBackend, config: AuthClientConfig) 
  return () => fetchAndSetConfig(handler, config);


function fetchAndSetConfig() 
  return new Promise((resolve, reject) => 
     new HttpClient(handler).get('/config').toPromise()
       .then((loadedConfig: any) => 
          config.set(loadedConfig);
          resolve(true);
      ); 
  )

【讨论】:

感谢您的建议。然后你只有一个承诺等待另一个承诺。 Angular 应该等待外部承诺,只有在内部承诺完成并解决它时才会解决。我在我的主项目和 StackBlitz 示例中尝试了你的方法,但它仍然不起作用。【参考方案3】:

我在动态 auth0 配置中遇到了同样的问题。我已经通过Philip Lysenko 尝试过this solution。主要思想是对主路由使用延迟加载。它对我有用。

另外,为您的根路由器配置设置initialNavigation: 'enabledNonBlocking'

“网络”选项卡上的外观:picture

【讨论】:

感谢@Dmytro 的建议。最后我采用了另一种方法,因为我对延迟加载方法不满意 - 这似乎是一个巨大的结构变化,对于这种情况来说有点过头了。

以上是关于Angular 导入的模块不等待 APP_INITIALIZER的主要内容,如果未能解决你的问题,请参考以下文章

如何将 angular.js 模块导入 Angular 应用程序?

Angular 模块导入的动态配置

为啥没有用于@angular/core 模块导入的相对路径

通过模块导入多个 Angular 组件

无法导入“@angular/material”模块

Angular2从模块导入组件/服务