The Problem

When developing Ionic 2 apps, very often developers want to store different application configuration values in different environments. For example, the backend API URLs in development and production are different. Or, two different Google Map Api keys are used in QA and production environments.

Our Approach at F5 Works

  • Storing KEY=VALUE pairs in a single .env file, which comply Twelve-Factor App methodology
  • Inject .env content into ENV global variable during Ionic build process (we are using Webpack but similar approach can apply to other bundlers such as RollupJS)
  • Create an injectable class AppConfig which can be accessed in the app, and can be easily mocked for ease of testing

The Recipe

Step 1: Create .env at project root (the KEY=VALUE store)

API_URL=http://localhost:3000/api/v1
GOOGLE_MAP_API_KEY=XxxYyyZzz

NOTE: you should git-ignore the .env file

Step 2: Inject .env content into ENV global variable

  • create config/webpack.config.js with content below
var dotenvConfig = require('dotenv').config();
var _ = require('lodash');
var path = require('path');
var webpack = require('webpack');
var ionicWebpackFactory = require(process.env.IONIC_WEBPACK_FACTORY);

function getPlugins() {
  var plugins = [
    new webpack.DefinePlugin({
      'process.env': _(process.env)
                      .pick(_.keys(dotenvConfig))
                      .mapValues((v) => (JSON.stringify(v)))
                      .value()
    })
  ];
  // for dev builds, use our custom environment
  return [
    ...plugins,
    ionicWebpackFactory.getIonicEnvironmentPlugin()
  ];
}

module.exports = {
  entry: process.env.IONIC_APP_ENTRY_POINT,
  output: {
    path: '{{BUILD}}',
    publicPath: 'build/',
    filename: process.env.IONIC_OUTPUT_JS_FILE_NAME,
    devtoolModuleFilenameTemplate: ionicWebpackFactory.getSourceMapperFunction(),
  },
  devtool: process.env.IONIC_SOURCE_MAP_TYPE,

  resolve: {
    extensions: ['.ts', '.js', '.json'],
    modules: [path.resolve('node_modules')]
  },

  module: {
    loaders: [
      {
        test: /\.json$/,
        loader: 'json-loader'
      },
      {
        test: /\.ts$/,
        loader: process.env.IONIC_WEBPACK_LOADER
      }
    ]
  },

  plugins: getPlugins(),

  // Some libraries import Node modules but don't use them in the browser.
  // Tell Webpack to provide empty mocks for them so importing them works.
  node: {
    fs: 'empty',
    net: 'empty',
    tls: 'empty'
  }
};

NOTE: you can find original webpack config from node_modules/@ionic/app-scripts

  • add below to package.json so Ionic build process know it:
  "config": {
    "ionic_webpack": "./config/webpack.config.js"
  }

  • install npm module dotenv and lodash:
npm install dotenv --save-dev
npm install lodash --save-dev

Step 3: Create and inject AppConfig class

  • create src/config/app.config.ts:
import { Injectable } from '@angular/core';

declare var process: any;

@Injectable()
export class AppConfig {
  public apiBaseUrl: string;
  public googleMapApiKey: string;

  constructor() {
    this.apiBaseUrl = this._readString('API_URL', 'http://localhost:3000/api/v1');
    this.googleMapApiKey = this._readString('GOOGLE_MAP_API_KEY', 'xxxyyy111');

    console.debug('AppConfig', this);
  }
  
  private _readString(key: string, defaultValue?: string): string {
    const v = process.env[key];
    return v === undefined ? defaultValue : String(v);
  }
}
  • inject AppConfig in src/config/app.module.ts:
import { NgModule }     from '@angular/core';
import { AppConfig }    from '../config/app.config';

@NgModule({
  providers: [
    AppConfig,

    // other injectables
  ]
})
export class AppModule {}

Step 4: Reading values in AppConfig from other components

  • now AppConfig values are available in all components, e.g. src/app/app.component.ts:
import { Component, ViewChild } from '@angular/core';
import { AppConfig } from '../config/app.config';

@Component({
  templateUrl: 'app.html'
})
export class MyApp {
  constructor(public appConfig: AppConfig) {
    console.debug('AppConfig', this.appConfig);
  }
}

  • firing up Ionic app and view result in action from Chrome inspector

.env for different environment

Now, you can populate different set values of into .env from the machine that build or run Ionic app. For instance, our CI agent in Jenkins has different set of .env values for building apps of different environments.

Alternative

Instead of having single .env, alternatively we could have json files for each environment.

config/developement.json
config/production.json

We used this approach in our Ionic1 projects. However when project grows, it becomes a bit messy as more personal config json files tend to be created. So we choose to experiment the single env approach which Twelve-Factor App suggests.