How do I configure different environments in Angular.js?
Asked Answered
H

10

222

How do you manage configuration variables/constants for different environments?

This could be an example:

My rest API is reachable on localhost:7080/myapi/, but my friend that works on the same code under Git version control has the API deployed on his Tomcat on localhost:8099/hisapi/.

Supposing that we have something like this :

angular
    .module('app', ['ngResource'])

    .constant('API_END_POINT','<local_end_point>')

    .factory('User', function($resource, API_END_POINT) {
        return $resource(API_END_POINT + 'user');
    });

How do I dynamically inject the correct value of the API endpoint, depending on the environment?

In PHP I usually do this kind of stuff with a config.username.xml file, merging the basic configuration file (config.xml) with the local environment configuration file recognised by the name of the user. But I don't know how to manage this kind of thing in JavaScript?

Help answered 2/5, 2013 at 13:41 Comment(0)
G
208

I'm a little late to the thread, but if you're using Grunt I've had great success with grunt-ng-constant.

The config section for ngconstant in my Gruntfile.js looks like

ngconstant: {
  options: {
    name: 'config',
    wrap: '"use strict";\n\n{%= __ngModule %}',
    space: '  '
  },
  development: {
    options: {
      dest: '<%= yeoman.app %>/scripts/config.js'
    },
    constants: {
      ENV: 'development'
    }
  },
  production: {
    options: {
      dest: '<%= yeoman.dist %>/scripts/config.js'
    },
    constants: {
      ENV: 'production'
    }
  }
}

The tasks that use ngconstant look like

grunt.registerTask('server', function (target) {
  if (target === 'dist') {
    return grunt.task.run([
      'build',
      'open',
      'connect:dist:keepalive'
    ]);
  }

  grunt.task.run([
    'clean:server',
    'ngconstant:development',
    'concurrent:server',
    'connect:livereload',
    'open',
    'watch'
  ]);
});

grunt.registerTask('build', [
  'clean:dist',
  'ngconstant:production',
  'useminPrepare',
  'concurrent:dist',
  'concat',
  'copy',
  'cdnify',
  'ngmin',
  'cssmin',
  'uglify',
  'rev',
  'usemin'
]);

So running grunt server will generate a config.js file in app/scripts/ that looks like

"use strict";
angular.module("config", []).constant("ENV", "development");

Finally, I declare the dependency on whatever modules need it:

// the 'config' dependency is generated via grunt
var app = angular.module('myApp', [ 'config' ]);

Now my constants can be dependency injected where needed. E.g.,

app.controller('MyController', ['ENV', function( ENV ) {
  if( ENV === 'production' ) {
    ...
  }
}]);
Gunnel answered 20/8, 2013 at 19:17 Comment(10)
Rather than putting 'ngconstant:development' in 'serve' - if you put it in watch's config under 'gruntfile' as tasks: ['ngconstant:development'] - you won't need to restart grunt serve when you update the development variables in the gruntfile.Ruthy
Instead of adding your constants in the gruntfile.js, you can put in separate files like this: package: grunt.file.readJSON('development.json')Gine
There is an updated syntax for Gruntfile.js in the 0.5 version of grunt-ng-constant: github.com/werk85/grunt-ng-constant/issues/31. Great answer, thanks!Impregnable
For those who use gulp, there's gulp-ng-constant.Cabal
@Impregnable I've edited the answer and updated with the changes to the new syntaxDough
I have found It's also needed to include the scripts/config.js file to angular to find module, like this: <script src="scripts/config.js"></script>Heilman
After a while, i changed my choice. I'm applying and i applied this approach (not that plugin) because i want read configuration values ​​from dedicated files, as @GuilhemSoulas has already written.Help
@Ruthy just curious i noticed this as well that I have to restart grunt if i changed the configuration slightly (like if I added a new property under ENV that won't show up in my app). For your solution though, under tasks how will you know which task to run, like 'ngconstant:development' vs 'ngconstant:production', it has to be dynamic based on the grunt task thats run, and I think you mentioned just placing 'tasks: ['ngconstant:development']' which wont work for prod build? Is there a solution for issue?Oxycephaly
I have an additional environment called staging where I wish to host my app. Is there a way to differentiate this env from production while running grunt build to run the correct ngconstant task?Sofar
Why i cant understand this?. Every team member working on same app will have to change the same file... There is no REAL way to get environment variables (those obtained from OS) rather than modifying a "probably" version-controlled file???Battlefield
P
75

One cool solution might be separating all environment-specific values into some separate angular module, that all other modules depend on:

angular.module('configuration', [])
       .constant('API_END_POINT','123456')
       .constant('HOST','localhost');

Then your modules that need those entries can declare a dependency on it:

angular.module('services',['configuration'])
       .factory('User',['$resource','API_END_POINT'],function($resource,API_END_POINT){
           return $resource(API_END_POINT + 'user');
       });

Now you could think about further cool stuff:

The module, that contains the configuration can be separated into configuration.js, that will be included at your page.

This script can be easily edited by each of you, as long as you don’t check this separate file into git. But it's easier to not check in the configuration if it is in a separate file. Also, you could branch it locally.

Now, if you have a build-system, like ANT or Maven, your further steps could be implementing some placeholders for the values API_END_POINT, that will be replaced during build-time, with your specific values.

Or you have your configuration_a.js and configuration_b.js and decide at the backend which to include.

Pansypant answered 2/5, 2013 at 14:16 Comment(0)
G
31

For Gulp users, gulp-ng-constant is also useful combined with gulp-concat, event-stream and yargs.

var concat = require('gulp-concat'),
    es = require('event-stream'),
    gulp = require('gulp'),
    ngConstant = require('gulp-ng-constant'),
    argv = require('yargs').argv;

var enviroment = argv.env || 'development';

gulp.task('config', function () {
  var config = gulp.src('config/' + enviroment + '.json')
    .pipe(ngConstant({name: 'app.config'}));
  var scripts = gulp.src('js/*');
  return es.merge(config, scripts)
    .pipe(concat('app.js'))
    .pipe(gulp.dest('app/dist'))
    .on('error', function() { });
});

In my config folder I have these files:

ls -l config
total 8
-rw-r--r--+ 1 .. ci.json
-rw-r--r--+ 1 .. development.json
-rw-r--r--+ 1 .. production.json

Then you can run gulp config --env development and that will create something like this:

angular.module("app.config", [])
.constant("foo", "bar")
.constant("ngConstant", true);

I also have this spec:

beforeEach(module('app'));

it('loads the config', inject(function(config) {
  expect(config).toBeTruthy();
}));
Grania answered 5/1, 2015 at 23:28 Comment(1)
Is there a way to remove the dependency array with gulp ng constant? I don't have any dependency to my constants like you have in this e.g. "ngAnimate". If I don't include it, I get an empty dependency array as angular.module("my.module.config", []) but I want the output as angular.module("my.module.config"). I don't see any option in gulp ng constant but I see that you can pass deps: false in grunt ng constant package. Any help?Jocelyn
L
17

To achieve that, I suggest you to use AngularJS Environment Plugin: https://www.npmjs.com/package/angular-environment

Here's an example:

angular.module('yourApp', ['environment']).
config(function(envServiceProvider) {
    // set the domains and variables for each environment 
    envServiceProvider.config({
        domains: {
            development: ['localhost', 'dev.local'],
            production: ['acme.com', 'acme.net', 'acme.org']
            // anotherStage: ['domain1', 'domain2'], 
            // anotherStage: ['domain1', 'domain2'] 
        },
        vars: {
            development: {
                apiUrl: '//localhost/api',
                staticUrl: '//localhost/static'
                // antoherCustomVar: 'lorem', 
                // antoherCustomVar: 'ipsum' 
            },
            production: {
                apiUrl: '//api.acme.com/v2',
                staticUrl: '//static.acme.com'
                // antoherCustomVar: 'lorem', 
                // antoherCustomVar: 'ipsum' 
            }
            // anotherStage: { 
            //  customVar: 'lorem', 
            //  customVar: 'ipsum' 
            // } 
        }
    });

    // run the environment check, so the comprobation is made 
    // before controllers and services are built 
    envServiceProvider.check();
});

And then, you can call the variables from your controllers such as this:

envService.read('apiUrl');

Hope it helps.

Lovell answered 26/7, 2015 at 5:13 Comment(5)
how does he switch between development and production?Pediatrician
Hi Juan Pablo, or @Pediatrician if you figured it out. Before I ask a question on SO / raise an issue on Github; how does angular-environment detect the environment? i.e. what do you need to do on your local machine / web server so that it knows that it is dev / prod respectively?Arrogate
Reading the docs again... "envServiceProvider.check() ... will automatically set the appropriate environment based on given domains". So I reckon it detects the current domain and sets the environment appropriately – time to test it out!Arrogate
@Arrogate Did you find out how it detects environments automatically?Tetzel
@Tetzel - it was five years ago - I can't remember that far back. Sorry!Arrogate
P
16

You could use lvh.me:9000 to access your AngularJS app, (lvh.me just points to 127.0.0.1) and then specify a different endpoint if lvh.me is the host:

app.service("Configuration", function() {
  if (window.location.host.match(/lvh\.me/)) {
    return this.API = 'http://localhost\\:7080/myapi/';
  } else {
    return this.API = 'http://localhost\\:8099/hisapi/';
  }
});

And then inject the Configuration service and use Configuration.API wherever you need to access the API:

$resource(Configuration.API + '/endpoint/:id', {
  id: '@id'
});

A tad clunky, but works fine for me, albeit in a slightly different situation (API endpoints differ in production and development).

Preference answered 16/7, 2013 at 9:48 Comment(2)
so I think often times people overcomplicated things. The simple use of window.location.host was more than sufficient for me.Pumice
@Pumice what do you say if my API URL differ to app host?Overglaze
I
7

We could also do something like this.

(function(){
    'use strict';

    angular.module('app').service('env', function env() {

        var _environments = {
            local: {
                host: 'localhost:3000',
                config: {
                    apiroot: 'http://localhost:3000'
                }
            },
            dev: {
                host: 'dev.com',
                config: {
                    apiroot: 'http://localhost:3000'
                }
            },
            test: {
                host: 'test.com',
                config: {
                    apiroot: 'http://localhost:3000'
                }
            },
            stage: {
                host: 'stage.com',
                config: {
                apiroot: 'staging'
                }
            },
            prod: {
                host: 'production.com',
                config: {
                    apiroot: 'production'
                }
            }
        },
        _environment;

        return {
            getEnvironment: function(){
                var host = window.location.host;
                if(_environment){
                    return _environment;
                }

                for(var environment in _environments){
                    if(typeof _environments[environment].host && _environments[environment].host == host){
                        _environment = environment;
                        return _environment;
                    }
                }

                return null;
            },
            get: function(property){
                return _environments[this.getEnvironment()].config[property];
            }
        }

    });

})();

And in your controller/service, we can inject the dependency and call the get method with property to be accessed.

(function() {
    'use strict';

    angular.module('app').service('apiService', apiService);

    apiService.$inject = ['configurations', '$q', '$http', 'env'];

    function apiService(config, $q, $http, env) {

        var service = {};
        /* **********APIs **************** */
        service.get = function() {
            return $http.get(env.get('apiroot') + '/api/yourservice');
        };

        return service;
    }

})();

$http.get(env.get('apiroot') would return the url based on the host environment.

Iritis answered 11/12, 2015 at 10:24 Comment(0)
A
5

Good question!

One solution could be to continue using your config.xml file, and provide api endpoint information from the backend to your generated html, like this (example in php):

<script type="text/javascript">
angular.module('YourApp').constant('API_END_POINT', '<?php echo $apiEndPointFromBackend; ?>');
</script>

Maybe not a pretty solution, but it would work.

Another solution could be to keep the API_END_POINT constant value as it should be in production, and only modify your hosts-file to point that url to your local api instead.

Or maybe a solution using localStorage for overrides, like this:

.factory('User',['$resource','API_END_POINT'],function($resource,API_END_POINT){
   var myApi = localStorage.get('myLocalApiOverride');
   return $resource((myApi || API_END_POINT) + 'user');
});
Anselm answered 2/5, 2013 at 14:4 Comment(2)
Hi joakimbeng , i wrote the solution that i use in php to explain the point. We are trying to code a pure javascript client with a pure RESTful java backend, so the php/js mix it's not my case and also when i write in php i always try to keep php and js not mixed. but thanks for the answer. I think the @Pansypant answer solution could works: a configuration.js file not under version control that contains a configuration module. With this approach i can inject/loads also a different configuration module for testing purpose, if it's necessary. Thanks guys.Help
@hal9087 I totally agree about the mixing languages part, it should be avoided at all cost :) I like the configuration.js solution as well, I'll keep it in mind when I need something similar!Anselm
A
4

Very late to the thread, but a technique I've used, pre-Angular, is to take advantage of JSON and the flexibility of JS to dynamically reference collection keys, and use inalienable facts of the environment (host server name, current browser language, etc.) as inputs to selectively discriminate/prefer suffixed key names within a JSON data structure.

This provides not merely deploy-environment context (per OP) but any arbitrary context (such as language) to provide i18n or any other variance required simultaneously, and (ideally) within a single configuration manifest, without duplication, and readably obvious.

IN ABOUT 10 LINES VANILLA JS

Overly-simplified but classic example: An API endpoint base URL in a JSON-formatted properties file that varies per environment where (natch) the host server will also vary:

    ...
    'svcs': {
        'VER': '2.3',
        'API@localhost': 'http://localhost:9090/',
        '[email protected]': 'https://www.uat.productionwebsite.com:9090/res/',
        '[email protected]': 'https://www.productionwebsite.com:9090/api/res/'
    },
    ...

A key to the discrimination function is simply the server hostname in the request.

This, naturally, can be combined with an additional key based on the user's language settings:

    ...
    'app': {
        'NAME': 'Ferry Reservations',
        'NAME@fr': 'Réservations de ferry',
        'NAME@de': 'Fähren Reservierungen'
    },
    ...

The scope of the discrimination/preference can be confined to individual keys (as above) where the "base" key is only overwritten if there's a matching key+suffix for the inputs to the function -- or an entire structure, and that structure itself recursively parsed for matching discrimination/preference suffixes:

    'help': {
        'BLURB': 'This pre-production environment is not supported. Contact Development Team with questions.',
        'PHONE': '808-867-5309',
        'EMAIL': '[email protected]'
    },
    '[email protected]': {
        'BLURB': 'Please contact Customer Service Center',
        'BLURB@fr': 'S\'il vous plaît communiquer avec notre Centre de service à la clientèle',
        'BLURB@de': 'Bitte kontaktieren Sie unseren Kundendienst!!1!',
        'PHONE': '1-800-CUS-TOMR',
        'EMAIL': '[email protected]'
    },

SO, if a visiting user to the production website has German (de) language preference setting, the above configuration would collapse to:

    'help': {
        'BLURB': 'Bitte kontaktieren Sie unseren Kundendienst!!1!',
        'PHONE': '1-800-CUS-TOMR',
        'EMAIL': '[email protected]'
    },

What does such a magical preference/discrimination JSON-rewriting function look like? Not much:

// prefer(object,suffix|[suffixes]) by/par/durch storsoc
// prefer({ a: 'apple', a@env: 'banana', b: 'carrot' },'env') -> { a: 'banana', b: 'carrot' }
function prefer(o,sufs) {
    for (var key in o) {
        if (!o.hasOwnProperty(key)) continue; // skip non-instance props
        if(key.split('@')[1]) { // suffixed!
            // replace root prop with the suffixed prop if among prefs
            if(o[key] && sufs.indexOf(key.split('@')[1]) > -1) o[key.split('@')[0]] = JSON.parse(JSON.stringify(o[key]));

            // and nuke the suffixed prop to tidy up
            delete o[key];

            // continue with root key ...
            key = key.split('@')[0];
        }

        // ... in case it's a collection itself, recurse it!
        if(o[key] && typeof o[key] === 'object') prefer(o[key],sufs);

    };
};

In our implementations, which include Angular and pre-Angular websites, we simply bootstrap the configuration well ahead of other resource calls by placing the JSON within a self-executing JS closure, including the prefer() function, and fed basic properties of hostname and language-code (and accepts any additional arbitrary suffixes you might need):

(function(prefs){ var props = {
    'svcs': {
        'VER': '2.3',
        'API@localhost': 'http://localhost:9090/',
        '[email protected]': 'https://www.uat.productionwebsite.com:9090/res/',
        '[email protected]': 'https://www.productionwebsite.com:9090/api/res/'
    },
    ...
    /* yadda yadda moar JSON und bisque */

    function prefer(o,sufs) {
        // body of prefer function, broken for e.g.
    };

    // convert string and comma-separated-string to array .. and process it
    prefs = [].concat( ( prefs.split ? prefs.split(',') : prefs ) || []);
    prefer(props,prefs);
    window.app_props = JSON.parse(JSON.stringify(props));
})([location.hostname, ((window.navigator.userLanguage || window.navigator.language).split('-')[0])  ] );

A pre-Angular site would now have a collapsed (no @ suffixed keys) window.app_props to refer to.

An Angular site, as a bootstrap/init step, simply copies the dead-dropped props object into $rootScope, and (optionally) destroys it from global/window scope

app.constant('props',angular.copy(window.app_props || {})).run( function ($rootScope,props) { $rootScope.props = props; delete window.app_props;} );

to be subsequently injected into controllers:

app.controller('CtrlApp',function($log,props){ ... } );

or referred to from bindings in views:

<span>{{ props.help.blurb }} {{ props.help.email }}</span>

Caveats? The @ character is not valid JS/JSON variable/key naming, but so far accepted. If that's a deal-breaker, substitute for any convention you like, such as "__" (double underscore) as long as you stick to it.

The technique could be applied server-side, ported to Java or C# but your efficiency/compactness may vary.

Alternately, the function/convention could be part of your front-end compile script, so that the full gory all-environment/all-language JSON is never transmitted over the wire.

UPDATE

We've evolved usage of this technique to allow multiple suffixes to a key, to avoid being forced to use collections (you still can, as deeply as you want), and as well to honor the order of the preferred suffixes.

Example (also see working jsFiddle):

var o = { 'a':'apple', 'a@dev':'apple-dev', 'a@fr':'pomme',
          'b':'banana', 'b@fr':'banane', 'b@dev&fr':'banane-dev',
          'c':{ 'o':'c-dot-oh', 'o@fr':'c-point-oh' }, 'c@dev': { 'o':'c-dot-oh-dev', 'o@fr':'c-point-oh-dev' } };

/*1*/ prefer(o,'dev');        // { a:'apple-dev', b:'banana',     c:{o:'c-dot-oh-dev'}   }
/*2*/ prefer(o,'fr');         // { a:'pomme',     b:'banane',     c:{o:'c-point-oh'}     }
/*3*/ prefer(o,'dev,fr');     // { a:'apple-dev', b:'banane-dev', c:{o:'c-point-oh-dev'} }
/*4*/ prefer(o,['fr','dev']); // { a:'pomme',     b:'banane-dev', c:{o:'c-point-oh-dev'} }
/*5*/ prefer(o);              // { a:'apple',     b:'banana',     c:{o:'c-dot-oh'}       }

1/2 (basic usage) prefers '@dev' keys, discards all other suffixed keys

3 prefers '@dev' over '@fr', prefers '@dev&fr' over all others

4 (same as 3 but prefers '@fr' over '@dev')

5 no preferred suffixes, drops ALL suffixed properties

It accomplishes this by scoring each suffixed property and promoting the value of a suffixed property to the non-suffixed property when iterating over the properties and finding a higher-scored suffix.

Some efficiencies in this version, including removing dependence on JSON to deep-copy, and only recursing into objects that survive the scoring round at their depth:

function prefer(obj,suf) {
    function pr(o,s) {
        for (var p in o) {
            if (!o.hasOwnProperty(p) || !p.split('@')[1] || p.split('@@')[1] ) continue; // ignore: proto-prop OR not-suffixed OR temp prop score
            var b = p.split('@')[0]; // base prop name
            if(!!!o['@@'+b]) o['@@'+b] = 0; // +score placeholder
            var ps = p.split('@')[1].split('&'); // array of property suffixes
            var sc = 0; var v = 0; // reset (running)score and value
            while(ps.length) {
                // suffix value: index(of found suffix in prefs)^10
                v = Math.floor(Math.pow(10,s.indexOf(ps.pop())));
                if(!v) { sc = 0; break; } // found suf NOT in prefs, zero score (delete later)
                sc += v;
            }
            if(sc > o['@@'+b]) { o['@@'+b] = sc; o[b] = o[p]; } // hi-score! promote to base prop
            delete o[p];
        }
        for (var p in o) if(p.split('@@')[1]) delete o[p]; // remove scores
        for (var p in o) if(typeof o[p] === 'object') pr(o[p],s); // recurse surviving objs
    }
    if( typeof obj !== 'object' ) return; // validate
    suf = ( (suf || suf === 0 ) && ( suf.length || suf === parseFloat(suf) ) ? suf.toString().split(',') : []); // array|string|number|comma-separated-string -> array-of-strings
    pr(obj,suf.reverse());
}
Awestricken answered 22/6, 2016 at 6:11 Comment(0)
W
2

If you're using Brunch, the plugin Constangular helps you to manage variables for different environments.

Widera answered 13/5, 2014 at 8:9 Comment(0)
C
-8

Have you seen this question and its answer?

You can set a globally valid value for you app like this:

app.value('key', 'value');

and then use it in your services. You could move this code to a config.js file and execute it on page load or another convenient moment.

Cristophercristy answered 2/5, 2013 at 14:18 Comment(2)
Could somebody please explain why this is such a bad answer? It's been massively downvoted, but not a single comment...Congius
This is old as hell, but if I had to guess why the downvotes, it's because it doesn't address the problem of environment specific configurations, it's just a suggestion to use .value() to set a global value in any old app. There is no mention on how one would use this depending on env or anything along the original questions parameters.Tinishatinker

© 2022 - 2024 — McMap. All rights reserved.