AppModule加载之前的角加载外部配置

时间:2019-01-31 21:34:31

标签: angular typescript

请考虑以下情形(Angular v7):

  1. 从外部端点(JSON)加载配置参数(API服务器URL和Auth服务器URL),加载AppModule之前
  2. 将配置传递给AppModule(OAuth2模块)
  3. 使用AOT编译应用程序

第2点在这里很关键,看起来像这样:

@NgModule({
  imports: [
    ...
    OAuthModule.forRoot({
      resourceServer: {
        allowedUrls: [API_SERVER_URL], // <== we need to set the value that we loaded from the external endpoint (JSON) here
        sendAccessToken: true
      }
    }),
    ...
  ],
  declarations: [AppComponent],
  bootstrap: [AppComponent]
})
export class AppModule { }

我已经尝试的东西:

  • 使用APP_INITIALIZER的解决方案。这不起作用,因为OAuthModule.forRoot()APP_INITIALIZER可以下载外部配置JSON之前被触发。
  • main.ts中的配置with an async function加载到Angular环境变量中,然后引导AppModule。由于import { AppModule } from './app/app.module';中的main.ts语句也不起作用,这导致AppModule在加载外部配置之前加载并触发OAuthModule.forRoot()this comment确认了此行为) 。
  • Load the AppModule dynamicallymain.ts中,因此顶部没有import语句。 This是该注释中给出的StackBlitz示例。 它有效,但是1)打破了延迟加载WARNING in Lazy routes discovery is not enabled.,而2)不适用于AOT编译。 确实非常接近我的需求。

想知道是否有人在 AppModule加载之前知道加载外部配置的另一种方法。

选项3的StackBlitz(动态加载AppModule):https://stackblitz.com/edit/angular-n8hdty

3 个答案:

答案 0 :(得分:4)

这里还有另一个选择。 @yurzui答案有效,但需要使用useFactory,这会使代码更难以理解。

useFactory是必需的,因为一旦将@NgModule导入AppModule时将执行Angular main.ts装饰器,因此尚未加载配置。

因此,我决定通过在angular.js的scripts部分中添加脚本来加载配置。方法如下:

src / config / load.js:

// This file is added to the scripts section of 'angular.json' so it can run before Angular bootstrap process.
// It responsability is to load app configuration from JSON files.
(() => {
  const fetchSync = url => {
    // The code below will log the following warning: "[Deprecation] Synchronous XMLHttpRequest on the main thread is deprecated because of its detrimental effects to the end user's experience. For more help, check https://xhr.spec.whatwg.org/.",
    // but since we want the configuration to be set before Angular bootstrap process, we ignore this warning.
    const xhr = new XMLHttpRequest();
    xhr.open('GET', url, false);
    xhr.send(null);
    return JSON.parse(xhr.responseText);
  };

  // We attach the fetched configuration to the 'window' global variable to access it later from Angular.
  window.configuration = {
    ...fetchSync('config/config.base.json'),
    ...fetchSync('config/config.local.json'),
  };
})();

angular.json:

  // ...
  "architect": {
    "build": {
      "builder": "@angular-devkit/build-angular:browser",
      "options": {
        // ...
        "assets": [
          // ...
          "src/config/config.base.json",
          "src/config/config.local.json"
        ],
        "scripts": ["src/config/load.js"],
  // ...

src / config / configuration.ts:

import get from 'lodash/get';

export class Configuration {
  // We get the configuration from the 'window.configuration' property which as been set earlier by 'config/load.js'.
  private static value = (window as any).configuration;

  /**
   * Get configuration value.
   * @param path The path of the configuration value. Use '.' for nested values.
   * @param defaultValue The returned value if the given path doesn't exist.
   * @example
   * const baseUrl = Configuration.get<string>('apis.github.baseUrl');
   */
  static get<T>(path: string, defaultValue?: T): T {
    return get(Configuration.value, path, defaultValue);
  }
}

然后您可以使用:

OAuthModule.forRoot({
  resourceServer: {
    allowedUrls: Configuration.get('allowedUrls')
    sendAccessToken: true
  }
}),

如果lodash出现问题,请参见this

答案 1 :(得分:3)

Angular文档中有一章非常好,称为NgModule FAQ,其中包含以下部分:

  

如果两个模块提供相同的服务怎么办?

     

...

     

如果NgModule A为令牌“ X”提供服务并导入   NgModule B,它也为令牌“ X”提供服务,然后是NgModule   A的服务定义“获胜”。

换句话说,您可以在AppModule中为您的库覆盖OAuthModuleConfig:

main.ts

(async () => {
  const response = await fetch('https://api.myjson.com/bins/lf0ns');
  const config = await response.json();

  environment['allowedUrls'] = config.apiBaseURL;

  platformBrowserDynamic().bootstrapModule(AppModule)
    .catch(err => console.error(err));
})();

app.module.ts

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';

import { AppComponent } from './app.component';
import { OAuthModule, OAuthModuleConfig } from 'angular-oauth2-oidc';
import { HttpClientModule } from '@angular/common/http';
import { environment } from '../environments/environment';

@NgModule({
  declarations: [
    AppComponent
  ],
  imports: [
    BrowserModule,
    HttpClientModule,
    OAuthModule.forRoot(),
  ],
  providers: [
    {
      provide: OAuthModuleConfig,
      useFactory: () => ({
        resourceServer: {
          allowedUrls: [environment['allowedUrls']],
          sendAccessToken: true
        }
      })
    }
  ],
  bootstrap: [AppComponent]
})
export class AppModule {}

请注意,我们也应该使用useFactory而不是useValue,这样我们就不必依赖何时导入AppModule

答案 2 :(得分:0)

除了@yurzui的答案,如果您在AOT中尝试此操作(例如ng build --prod),您将获得

ERROR in Error during template compile of 'AppModule'
  Function expressions are not supported in decorators in 'AuthModule'
    'AuthModule' contains the error at src\app\core\auth.module.ts(29,23)
      Consider changing the function expression into an exported function.

因此我们为工厂创建了导出函数:

app.module.ts

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';

import { AppComponent } from './app.component';
import { OAuthModule, OAuthModuleConfig } from 'angular-oauth2-oidc';
import { HttpClientModule } from '@angular/common/http';
import { environment } from '../environments/environment';

export function oAuthConfigFactory() : OAuthModuleConfig {
  return {
    resourceServer: {
      allowedUrls: [environment.servers.apiServer],
      sendAccessToken: true
    }
  }
}

@NgModule({
  declarations: [
    AppComponent
  ],
  imports: [
    BrowserModule,
    HttpClientModule,
    OAuthModule.forRoot(),
  ],
  providers: [
    {
      provide: OAuthModuleConfig,
      useFactory: oAuthConfigFactory
    }
  ],
  bootstrap: [AppComponent]
})
export class AppModule {}