loading...
Cover image for Build your Angular App Once, Deploy Anywhere

Build your Angular App Once, Deploy Anywhere

kylerjohnsondev profile image Kyler Johnson Updated on ・4 min read

Gears Image

Angular version: 8.x 
Node Version: 10.9 or later

The Ideal Scenario

We're building an Angular application and when we merge new code into the master branch of our git repo, we want our build tool (like Jenkins) to grab the latest code and build our deployment package for us. With our deployment package built (A.K.A the dist folder), we want to head over to our deployment tool (like Octopus), select an environment to which we want to deploy our app, click a "deploy" button, and trust it to deploy our package, replacing our envrionment variables in a config file with values specific to the selected environment.

What Do We Need to Achieve This?

We need a configuration file that we can access from our Angular code at runtime - which means it has to exist in the dist folder we intend to deploy. We need it there because we want to configure our deployment tool to replace the values of the environment variables within with values specific to the environment we deploy to.

Why Angular's Environment Files Are Not The Solution

Let's say we are using the environment files for our configuration as described here. If we run ng build and look inside of the dist folder, we do not see any of the environment files there. Because this is a compile-time solution, the configuration settings in the environment files are pulled into the minified JS bundles in the dist folder. We cannot easily configure our build tool to edit our environment variables if we cannot point it toward a file to edit. In short, this does not work with the "build once, deploy anywhere" model. To do this, our app needs to resolve configuration data at runtime instead of compile time.

So What Do We Do?

Luckily, there is a rather quick solution. All we have to do is:

  1. Add a JSON configuration file in the src folder
  2. Update our angular/webpack configuration to include the file in our dist folder
  3. Add a simple configuration service with a call to get our config data from our config file
  4. Use APP_INITIALIZER to invoke the method retrieving our config data during the bootstrap process

Side note: Placing our configuration in a JSON file makes configuring our deployment tool easier because many of them (like Octopus) have native support for replacing values in JSON files.

Adding the config file

There isn't much to this step. We're simply going to add a file named app-config.json and populate it with the following JSON.

{
  "api": "http://localhost:5000/"
}

Ensuring the config file is copied to the dist folder

To achieve this, we need to make an addition to the webpack configuration in the angular.json file. We need to add the path to our config file to the assets array in the webpack build configuration.

Screenshot - angular.json

Building the service

This is a simple service with a private property and two methods - one that sets the property and another that exposes the config data for the rest of your app. We can type the config object with an interface to help ensure we get what we expect from the JSON config file.

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

  private configuration: AppConfig;

  constructor(
    private httpClient: HttpClient
  ) { }

  setConfig(): Promise<AppConfig> {
    return this.httpClient
      .get<AppConfig>('./app-config.json')
      .toPromise()
      .then(config => this.configuration = config);
  }

  readConfig(): AppConfig {
    return this.configuration;
  }
}

Notice that the setConfigData method returns a promise? The initialization of our app will not complete until all promises are resolved so by returning a promise here, we're ensuring that the config data will be available when the rest of our app loads up and needs to use it.

Let's take a look

With that in place, let's set up the APP_INITIALIZER. According to the docs, APP_INITIALIZER is an injection token that allows us to invoke functions during the bootstrapping process of our application. To do that, we add the ConfigService and APP_INITIALIZER token as providers in app.module.ts.

const appInitializerFn = (configService: ConfigService) => {
  return () => {
    return configService.setConfig();
  };
};

@NgModule({
  declarations: [
    AppComponent
  ],
  imports: [
    BrowserModule,
    AppRoutingModule,
    HttpClientModule
  ],
  providers: [
    ConfigService,
    {
      provide: APP_INITIALIZER,
      useFactory: appInitializerFn,
      multi: true,
      deps: [ConfigService]
    }
  ],
  bootstrap: [AppComponent]
})
export class AppModule { }

Note that we need to use a factory function to create an instance of our ConfigService and call the setConfig() method on it.

Now, to ensure that this worked as expected, we can inject our ConfigService into the AppComponent and call our readConfig() method to get the config object.

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

  config: AppConfig;

  constructor(private configService: ConfigService) {}

  ngOnInit(): void {
    this.config = this.configService.readConfig();
  }
}

In our app.component.html file, we will just remove all of the default boilerplate HTML and add the followig to display our config data.

<div>{{ config | json }}</div>

If we run our app locally with ng serve, we will see our JSON configuration object rendered on the webpage.

To see a working example, take a look at the GitLab Repo.

Posted on by:

kylerjohnsondev profile

Kyler Johnson

@kylerjohnsondev

A Principal Software Engineer and Angular fanatic working remotely out of Atlanta, GA

Discussion

pic
Editor guide