Deploying Angular 4 Apps with Environment-Specific Info

by | Apr 24, 2017

If you are used to developing websites using .NET but are now using Angular 4 without any .NET, you may be wondering “how can I get my web.config file’s environment-specific settings into my Angular 4 app?” This is a legitimate question that is not limited to .NET developers.  Usually the answer to this question, if you do some searches, is to use the Angular CLI-created environment.ts file for dev and environments.prod.ts file for prod.  And then use those variables to determine what your settings should be.  But what if the requirements for your project state that you must do one build that is tested in dev, QA and then moved to prod without rebuilding?  In that case, it requires more thought.  This article will explore a solution to these questions and more.

Angular 4 Sample Setup

The sample in this post can be found on Github here.  It was created using Angular CLI which generated version 4 code.  The inspiration for this post is from Jurgen Van de Moere’s Angular 1 post found here. This post is my interpretation of how to do it in Angular 4.

The sample application is just a list of three links.  Nothing exciting about that but the fun lies in how those links get loaded into the Angular 4 app.

Create JSON File with Environment-Specific Settings

The first thing to do is to create a JSON file that has your settings, something like this:

{
    "link1": "http://amazon.com",
    "link2": "http://google.com",
    "link3": "http://facebook.com"
}

Assuming the project was created with Angular CLI, there should be an ‘assets’ folder.  It gets copied to the deploy location so is a good candidate for where to put this file.  I named it env-specific.json.  If you didn’t create your project with the CLI, it is easy enough to copy the JSON file using your build tool.  The point is, env-specific.json should be deployed with your application (for development purposes).  It will be up to the deploy team to copy the proper env-specific.json file for the given environment.  The beauty of this is that you don’t have to rebuild the application.

How To Get the JSON File into the Angular 4 App

It’s easy enough to read a JSON file using http.get.  The right place to do this is in a service.  Ideally, the service would only be created once and would be usable everywhere in the app.  To do this, we can take advantage of the core feature module that Angular recommends – read about it here.  Before we create this service, however, there are some important things to be done.

EnvSpecific Class

The EnvSpecific class will be the TypeScript representation of the JSON file.  It will be stored on the service so that it can be used throughout the app.  For the sample app, it looks like this:

import { Injectable } from '@angular/core';

export class EnvSpecific {
    link1: string;
    link2: string;
    link3: string;
}

Make sure env-specific.json is loaded before use

Before creating the service, we need to think through something important:  How do we know when we can use the EnvSettings class since it is being populated by an asynchronous process (http.get())? Thankfully, Angular has thought of this in routing.

In the AppRoutingModule, we can alter the default route (”) by adding resolve as shown:

const routes: Routes = [
  {
    path: '', component: LinksComponent, resolve: { envSpecific: EnvironmentSpecificResolver }
  }
];

The resolve option for a route instructs routing not to create the component until the resolver specified (EnvironmentSpecificResolver) has finished (in effect, making an asynchronous process a synchronous one). In the resolver, we are getting our env-specific.json file and returning it so that the envSpecific property (in the resolve object) is set.

Here is EnvironmentSpecificResolver:

import { Injectable } from '@angular/core';
import { Router, Resolve, RouterStateSnapshot,
         ActivatedRouteSnapshot } from '@angular/router';

import { EnvSpecific } from '../models/env-specific';
import { EnvironmentSpecificService } from './environment-specific.service';

@Injectable()
export class EnvironmentSpecificResolver implements Resolve<EnvSpecific> {
  constructor(private envSpecificSvc: EnvironmentSpecificService, private router: Router) {}

  resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Promise<EnvSpecific> {
    return this.envSpecificSvc.loadEnvironment()
            .then(es => {
                this.envSpecificSvc.setEnvSpecific(es);
                return this.envSpecificSvc.envSpecific;
            }, error => {
                console.log(error);
                return null;
            });
  }
}

The key thing to note here is the resolve method:

  • Returns a Promise<EnvSpecific> so that is what envSpecificSvc.loadEnvironment() must return
  • ‘then’ occurs when successful, call setEnvSpecific(es) so we can use it in other areas of the application
  • If there is an error, log it and return ‘null’

EnvironmentSpecificService Service

All of the prerequisites have been documented so we can now create the EnvironmentSpecificService.  This service will get the env-specific.json file and store it.  It will also allow components to subscribe to it so that when the value is set, they are notified and can act.  Here is the code:

import { Injectable, OnInit } from '@angular/core';
import { Http, Response, Headers, RequestOptions } from '@angular/http';
import { Observable, Subscription, BehaviorSubject } from 'rxjs/Rx';
import 'rxjs/add/operator/map';
import 'rxjs/add/operator/catch';
import 'rxjs/add/operator/toPromise';

import { EnvSpecific } from '../models/env-specific';

@Injectable()
export class EnvironmentSpecificService {

  public envSpecific: EnvSpecific;
  public envSpecificNull: EnvSpecific = null;
  private envSpecificSubject: BehaviorSubject<EnvSpecific> = new BehaviorSubject<EnvSpecific>(null);

  constructor(private http: Http) {
    console.log('EnvironmentSpecificService created');
  }

  public loadEnvironment() {
      // Only want to do this once - if root page is revisited, it calls this again.
      if (this.envSpecific === null || this.envSpecific === undefined) {
        console.log('Loading env-specific.json');

        return this.http.get('./assets/env-specific.json')
            .map((data) => data.json())
            .toPromise<EnvSpecific>();
      }

      return Promise.resolve(this.envSpecificNull);
  }

  public setEnvSpecific(es: EnvSpecific) {
    // This has already been set so bail out.
    if (es === null || es === undefined) {
        return;
    }

    this.envSpecific = es;
    console.log(this.envSpecific);

    if (this.envSpecificSubject) {
        this.envSpecificSubject.next(this.envSpecific);
    }
  }

  /*
    Call this if you want to know when EnvSpecific is set.
  */
  public subscribe(caller: any, callback: (caller: any, es: EnvSpecific) => void) {
      this.envSpecificSubject
          .subscribe((es) => {
              if (es === null) {
                  return;
              }
              callback(caller, es);
          });
  }
}

Notes:

  • loadEnvironment method
    • Call http.get(‘./assets/env-specific.json’) and map to json object
    • Return Promise<EnvSpecific> by calling .toPromise<EnvSpecific>(); – this is for the resolver if you remember
    • If this has already been called, return a null Promise<EnvSpecific> so that it can be ignored in the setEnvSpecific method
  • setEnvSpecific method
    • If es is null, we know that it has been set already so just return
    • Otherwise, set this.envSpecific for later use throughout the app
    • Call next() on this.envSpecificSubject so that anyone who has called the subscribe method can be notified that the EnvSpecific object has now been loaded so they can use it
  • subscribe method
    • For those areas of the app that are outside of the LinksComponent that need the settings on the initial view, they must call this subscribe method so that they can do their thing once the settings are ready
    • For example, the AppComponent wants to display the first link so it subscribes as shown:
export class AppComponent {
  firstLink: string;

  constructor(envSpecificSvc: EnvironmentSpecificService) {
    envSpecificSvc.subscribe(this, this.setLink);
  }

  setLink(caller: any, es: EnvSpecific) {
    const thisCaller = caller as AppComponent;
    thisCaller.firstLink = es.link1;
  }
}

Notes:

  • The caller parameter is required since setLink is a static method

LinksComponent can consume route.data

Since the initial route is LinksComponent, it can directly access the EnvSpecific class through the ActivatedRoute’s data property:

import { Component, OnInit } from '@angular/core';
import { ActivatedRoute } from '@angular/router';

import { EnvSpecific } from '../core/models/env-specific';

@Component({
  selector: 'app-links',
  templateUrl: './links.component.html',
  styleUrls: ['./links.component.css']
})
export class LinksComponent implements OnInit {

  link1: string;
  link2: string;
  link3: string;

  constructor(private route: ActivatedRoute) {
  }

  ngOnInit() {
    this.route.data
      .subscribe((data: { envSpecific: EnvSpecific }) => {
        this.link1 = data.envSpecific.link1;
        this.link2 = data.envSpecific.link2;
        this.link3 = data.envSpecific.link3;
      });
  }
}

Notes about LinksComponent:

  • The constructor injects the ActivatedRoute
  • Access route.data in ngOnInit
    • The route.data object is what is defined in the resolve option above
    • Subscribe to route.data so that when it is ready, we can set the links

Using the EnvironmentSpecificService

Once the first page is shown, all the subsequent components need to do is inject the EnvironmentSpecificService and use the values.  As you can see, another benefit of using a service for your settings is unit testing the settings becomes trivial.

In the sample app, the ReuseLinksComponent does just that by displaying the links in reverse order.  Here is the code:

import { Component, OnInit } from '@angular/core';

import { EnvironmentSpecificService } from '../core/services/environment-specific.service';

@Component({
  selector: 'app-reuse-links',
  templateUrl: './reuse-links.component.html',
  styleUrls: ['./reuse-links.component.css']
})
export class ReuseLinksComponent implements OnInit {

  link1: string;
  link2: string;
  link3: string;

  constructor(private envSpecificSvc: EnvironmentSpecificService) { }

  ngOnInit() {
    this.link1 = this.envSpecificSvc.envSpecific.link1;
    this.link2 = this.envSpecificSvc.envSpecific.link2;
    this.link3 = this.envSpecificSvc.envSpecific.link3;
  }
}

…and the html:

<p>
  Here are the links in another component:<br/>
  Link 3: <a href='{{link3}}'>{{link3}}</a><br/>
  Link 2: <a href='{{link2}}'>{{link2}}</a><br/>
  Link 1: <a href='{{link1}}'>{{link1}}</a>
</p>

Sample App Screenshots

For development, the sample application should initially look something like this:

Angular 4 Sample App Screenshot 1

If you click the Reuse Links link, this will show:

Angular 4 Sample App Screenshot 2

Dropping in a different env-specific.json file

Now we are ready for the huge payoff:  deploying a different set of links to a different environment without rebuilding the app.  The deployment team should be the ones who have the env-specific.json files for all the environments for your application.  That way, consultants or developers don’t need to know potential secret company information such as prod api urls, database connection strings, etc.  When they are ready to deploy, they simply copy the proper env-specific.json file for the deployed environment into the assets folder.

Here is an example of what the dist folder would look like:

Example of what the dist folder would look like

Notes:

  • The ‘env-specific’ folder is what the deployment team would have (showing here for an example)
  • assets/env-specific.json is what would be replaced since the code references it

Let’s try it in the sample app by creating a new env-specific.json file for production:

{
    "link1": "http://smile.amazon.com",
    "link2": "http://foxnews.com",
    "link3": "http://github.com"
}

To do this, I created an IIS virtual directory for my app that points to the dist folder of the Angular 4 sample application.  Then I can copy the prod version of env-specific.json into the dist/assets folder.  This should be the result:

Angular 4 Sample App Screenshot 3

Conclusion

So have I answered my initial questions?

  • How can I get web.config-like environment-specific settings into my Angular 4 application?
    • Read the env-specific.json file with a service before the first route creates its component
  • How can I do it without rebuilding the application?
    • Change the env-specific.json file at deployment time with the proper file

This is the type of issue that is important to know when coding an enterprise Angular 2 or Angular 4 application.