Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Completely external constants in Angular 5 using some configuration file

I have an app-const.ts with a server URL:

export class AppConst {
  public static serverPath = 'http://10.0.0.126:3031';
}

This is an URL path to Spring Boot REST server. At this case I hold this constant in one place and use it in all modules. However after a build I don't have a way to change this constant without re-building the whole project again if the server URL is changed.

Is there any way to hold this constant in some external configuration file on a hosting (next to index.html) so that I can change it without rebuilding the project (like application.properties file in Spring Boot, who knows)?

Or how I can easily manage the situation with changing server URL?

Addition. To clear situation: I place my Angular web-client on a hosting. Then this client starts communicate with a Spring Boot REST server that can be placed somewhere (in a cloud for example). This Spring Boot server has a server URL (serverPath) that might be changed sometimes. Now if server URL change I need to change this serverPath constant and rebuild the whole Angular project only due to this constant.

like image 745
Kirill Ch Avatar asked Apr 18 '18 15:04

Kirill Ch


4 Answers

I have got a following solution. It uses external JSON configuration file.

So first create a JSON in assets/data folder.

config.json:

{ "serverPath": "http://10.0.0.126:3031" }

Then read and parse it.

config.service.ts:

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';

import { Observable } from 'rxjs/Observable';

@Injectable()
export class ConfigService {

  private configUrl = "assets/data/config.json";

  constructor(private http: HttpClient) {
  }

  public getJSON(): Observable<any> {
    return this.http.get(this.configUrl)
  }

  public getSavedServerPath(){
    return localStorage.getItem('serverPath');
  }
}

In app.module.ts you need to import HttpClientModule so that this works.

Then you can save serverPath in LocalStorage in login component for example.

login.component.ts:

  constructor(public loginService:LoginService, public configService:ConfigService, private router: Router) {
  }

  ngOnInit() {

    this.configService.getJSON().subscribe(data => {
      localStorage.setItem("serverPath", data["serverPath"]);
    });

    ...
  }

After that you can access serverPath in all your other services.

server.service.ts:

import {Injectable } from '@angular/core';
import {Headers, Http, Response} from '@angular/http';
import 'rxjs/Rx';
import {Observable} from 'rxjs/Observable';
import {ConfigService} from '../services/config.service';

@Injectable()
export class ServerService {

  private serverPath:string;

  constructor(public configService: ConfigService, private http:Http) {
    this.serverPath = this.configService.getSavedServerPath();
  }
  ...
}

After a build you will see assets/data/config.json file in your dist folder. Copy all your dist folder to your hosting and all works.

like image 76
Kirill Ch Avatar answered Nov 08 '22 23:11

Kirill Ch


I have several applications that do exactly this. I've built a utility library for my applications that includes this.

First, I have a "Configuration" class. The json configuration file gets loaded from the server and mapped to an instance of this class:

export class Configuration {
  [key: string]: any;
}

Then, there's the ConfigurationService, that is responsible for loading the configuration file:

import {APP_INITIALIZER, Injectable} from '@angular/core';
import {HttpClient} from '@angular/common/http';
import {Observable} from 'rxjs/Observable';
import {AsyncSubject} from 'rxjs/AsyncSubject';
import 'rxjs/observable/throw';
import {Configuration} from './configuration';

// synchronous version of the initializer - the app initialization will wait for the configuration to load
export function configurationServiceInitializerFactory(configurationService: ConfigurationService): Function {
  // a lambda is required here, otherwise `this` won't work inside ConfigurationService::load
  return () =>  configurationService.load(Synchronicity.Sync);
}

// async version of the initializer - the app initialization will proceed without waiting for the configuration to load
export function asyncConfigurationServiceInitializerFactory(configurationService: ConfigurationService): Function {
  // a lambda is required here, otherwise `this` won't work inside ConfigurationService::load
  return () =>  {
    configurationService.load(Synchronicity.Async);
    return null;
  };
}

export const enum Synchronicity {
  Sync,
  Async,
  Unknown
}

@Injectable()
export class ConfigurationService {

  private synchronicity: Synchronicity = Synchronicity.Unknown;

  // the observable from the (load) http call to get the configuration
  private httpObservable: Observable<Configuration>;

  // the error (if any) that occurred during the load
  private loadError;

  private loadAttempted = false;
  private hasError = false;
  private loaded = false;

  // Observable that makes the config available to consumers when using async initialization
  private loadSubject = new AsyncSubject<Configuration>();

  // the configuration
  private configuration: Configuration;

  constructor(private http: HttpClient) {
  }

  public hasLoadError(): boolean {
    return this.hasError;
  }

  public isLoadead(): boolean {
    return this.loaded;
  }

  // use this when you have initialized with the (synchronous) configurationServiceInitializerFactory
  public getConfig(): Configuration {
    if(!this.loadAttempted) {
      throw new Error('ConfigurationService.getConfig() - service has not been iniialized yet');
    }

    if(this.synchronicity === Synchronicity.Async) {
      throw new Error('ConfigurationService.getConfig() - service has been iniialized async - use getConfigurationObserable()');
    }

    if(this.hasError) {
      throw this.loadError;
    }

    if(!this.loaded) {
      throw new Error('ConfigurationService.getConfig() - service has not finished loading the config');
    }

    return this.configuration;
  }

  // use this when you have initialized with the asyncCnfigurationServiceInitializerFactory
  public getConfigObservable(): Observable<Configuration> {

    // if neither init function was used, init async
    if (!this.loadAttempted) {
      this.load(Synchronicity.Async);
    }
    return this.loadSubject;
  }

  // the return value (Promise) of this method is provided via the APP_INITIALIZER Injection Token,
  // so the application's initialization will not complete until the Promise resolves.
  public load(synchronicity: Synchronicity): Promise<Configuration> {
    if (!this.loadAttempted) {
      this.loadAttempted = true;
      this.synchronicity = synchronicity;
      this.httpObservable = this.http.get<Configuration>('config/ui-config.json'); // path is relative to that for app's index.html
      this.httpObservable.subscribe(
        config => {
          this.configuration = config;
          this.loadError = undefined;
          this.hasError = false;
          this.loadSubject.next(this.configuration);
          this.loadSubject.complete();
          this.loaded = true;
        },
        error => {
          this.loadError = error;
          this.hasError = true;
          this.loadSubject.error(error);
          this.loadSubject.complete();
        }
      );
      return this.httpObservable.toPromise();
    }
  }
}

As you can see, this service gets the configuration from a relative path, config/ui-config.json. The path is relative to the index.html file that was loaded to bootstrap the application. You need to arrange for the server to return the configuration file from that location.

The service will be hooked into Angular's initialization sequence (code to follow). It can be done either synchronously or asynchronously with respect to the app's initialization.

If you use the 'synchronous' method, then app initialization will pause while the json file is loaded. The upside to this is that once the app finishes initialization, the configuration is known to be available. The downside is the potentially long pause during initialization, where your user is looking at a blank page.

If you use the 'asynchronous' method, then the app initialization will only kick off the request for the config file, but will not pause to wait for that request to complete. Upside: quick (normal) initialization. Downside: you get an Observable of the Configuration instead of a Configuration, so you need to flatMap (mergeMap) over that Observable everywhere that you need the configuration.

Here's how it gets hooked into the app initialization, in app.module:

@NgModule({
  declarations: [
    AppComponent
  ],
  imports: [
    BrowserModule,
    // etc.
  ],
  providers: [
    ConfigurationService,
    { provide: APP_INITIALIZER, useFactory: asyncConfigurationServiceInitializerFactory, deps: [ConfigurationService], multi: true },
  ],
  bootstrap: [AppComponent]
})
export class AppModule {
}

That's an example of the async config. For sync, just use configurationServiceInitializerFactory instead of asyncConfigurationServiceInitializerFactory

Again, if you use the synchronous version, you can just inject the ConfigurationService into your services, and call it's getConfig() method.

If you use the async version, you still inject the ConfigurationService into your services, but then you need to do something like this:

getSomething(): Observable<Something> {
    return this.configurationService.getConfigObservable().mergeMap(config =>
      this.http.get<Something>(`${config.serviceRoot}/something`)
    );
}

Edit: Oh, I almost forgot, I did a blog post on this a while back, and that has a little more detail. It's at https://chariotsolutions.com/blog/post/12-factor-ish-configuration-of-angular-applications/

And there's a complete example on GitHub at https://github.com/rfreedman/angular-configuration-service

like image 20
GreyBeardedGeek Avatar answered Nov 09 '22 01:11

GreyBeardedGeek


Another solution would be to add it as a javascript variable in your index.html file. I used this method and it works.

Add it to the "head" part of your index.html with the "script" tags, example:

<head>
  <script>
    window.LMS_REST_API_URL = "http://192.168.0.111:3000/";
  </script>
...

(my global variable is named "LMS_REST_API_URL")

After this you can access this variable like this:

private lms_cli_URL = window["LMS_REST_API_URL"];

I used it directly from the service that needs the URL, but it will probably also work in a seperate app-const.ts class file like you are using.

like image 1
Jeff Orange Avatar answered Nov 09 '22 01:11

Jeff Orange


I used the assets folder to reference external configuration. The idea is that the deployment process updates the configuration for that environment into the assets folder which is then read on app startup. To do this, first place your configuration in src/app/assets/config.json. E.g.

{
    "serverRoot":"https://my.server.root.com/"
}

The deployment process can then update the serverRoot property within this file to the correct value for that environment, or replace the contents of config.json entirely.

Then, create a model class in src/app/model/environment.ts which will allow type safe access to the configuration:

export class Environment {
    static serverRoot:string;

    static load(config:json) {
        this.serverRoot = config.serverRoot;
    }
}

Then, create a service to load the configuration in src/app/services/environment-load.service.ts:

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Environment } from '../model/Environment';


@Injectable({
  providedIn: 'root'
})
export class EnvironmentLoadService {

  constructor(private http: HttpClient) { }

  init() {
    return this.http.get('assets/config.json').toPromise().then(data => {
      Environment.load(data);
    });
  }

  static initializeEnvironmentConfig = (appConfig: EnvironmentLoadService) => {
    return () => {
      return appConfig.init();
    };
  };
}

Finally, in your app module (src/app/app.module.ts), set the EnvironmentLoadService as a Provider which is created during the app initialization stage of the app's lifecycle. This guarantees all promises are resolved before the app initialization stage is complete and that your config is fully loaded before the first component is constructed:

import { BrowserModule } from '@angular/platform-browser';
import { NgModule, APP_INITIALIZER } from '@angular/core';
import { AppComponent } from './app.component';
import { HttpClientModule } from '@angular/common/http';
import { EnvironmentLoadService } from './services/environment-load.service';

@NgModule({
  declarations: [
    AppComponent
  ],
  imports: [
    BrowserModule,
    HttpClientModule
  ],
  providers: [EnvironmentLoadService, {
    provide: APP_INITIALIZER,
    useFactory: EnvironmentLoadService.initializeEnvironmentConfig,
    multi: true,
    deps: [EnvironmentLoadService]
  }],
  bootstrap: [AppComponent]
})
export class AppModule { }
like image 1
Chris Knight Avatar answered Nov 08 '22 23:11

Chris Knight