¿Cómo configuro diferentes entornos en Angular.js?

220

¿Cómo gestiona las variables / constantes de configuración para diferentes entornos?

Este podría ser un ejemplo:

Se puede acceder a mi API de descanso localhost:7080/myapi/, pero mi amigo que trabaja en el mismo código bajo el control de versión de Git tiene la API implementada en su Tomcat localhost:8099/hisapi/.

Suponiendo que tenemos algo como esto:

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

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

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

¿Cómo se inyecta dinámicamente el valor correcto del punto final de la API, según el entorno?

En PHP, generalmente hago este tipo de cosas con un config.username.xmlarchivo, fusionando el archivo de configuración básica (config.xml) con el archivo de configuración del entorno local reconocido por el nombre del usuario. ¿Pero no sé cómo administrar este tipo de cosas en JavaScript?

rbarilani
fuente

Respuestas:

209

Llego un poco tarde al hilo, pero si estás usando Grunt , he tenido un gran éxito congrunt-ng-constant .

La sección de configuración ngconstanten mi Gruntfile.jsparece

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'
    }
  }
}

Las tareas que usan ngconstantparecen

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'
]);

Por lo tanto, la ejecución grunt servergenerará un config.jsarchivo app/scripts/que se ve como

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

Finalmente, declaro la dependencia de cualquier módulo que lo necesite:

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

Ahora mis constantes se pueden inyectar dependencia donde sea necesario. P.ej,

app.controller('MyController', ['ENV', function( ENV ) {
  if( ENV === 'production' ) {
    ...
  }
}]);
André Dion
fuente
10
En lugar de poner 'ngconstant:development'en 'serve'- si lo pones en la configuración del reloj bajo 'gruntfile'como tasks: ['ngconstant:development']- usted no tendrá que reiniciar grunt servecuando actualice las variables de desarrollo en el gruntfile.
gastado el
10
En lugar de agregar sus constantes en gruntfile.js, puede colocar archivos separados como este:package: grunt.file.readJSON('development.json')
Guilhem Soulas
3
Hay una sintaxis actualizada para Gruntfile.js en la versión 0.5 de grunt-ng-constant: github.com/werk85/grunt-ng-constant/issues/31 . Gran respuesta, gracias!
pherris
10
Para aquellos que usan gulp, hay gulp-ng-constant .
Dheeraj Vepakomma
44
He encontrado que también es necesario incluir el archivo scripts / config.js en angular para encontrar el módulo, de esta manera: <script src = "scripts / config.js"> </script>
Toni Gamez
75

Una solución genial podría ser separar todos los valores específicos del entorno en un módulo angular separado, del que dependen todos los demás módulos:

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

Luego, los módulos que necesitan esas entradas pueden declarar una dependencia de él:

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

Ahora puedes pensar en otras cosas interesantes:

El módulo que contiene la configuración se puede separar en configuration.js, que se incluirá en su página.

Cada uno de ustedes puede editar fácilmente este script, siempre que no marque este archivo separado en git. Pero es más fácil no verificar la configuración si está en un archivo separado. Además, puede ramificarlo localmente.

Ahora, si tiene un sistema de compilación, como ANT o Maven, sus pasos adicionales podrían ser implementar algunos marcadores de posición para los valores API_END_POINT, que se reemplazarán durante el tiempo de compilación, con sus valores específicos.

O tiene su configuration_a.jsy configuration_b.jsy decide en el backend qué incluir.

kfis
fuente
30

Para los usuarios de Gulp , gulp-ng-constant también es útil combinado con gulp-concat , event-stream e 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() { });
});

En mi carpeta de configuración tengo estos archivos:

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

Entonces puedes correr gulp config --env developmenty eso creará algo como esto:

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

También tengo esta especificación:

beforeEach(module('app'));

it('loads the config', inject(function(config) {
  expect(config).toBeTruthy();
}));
Rimian
fuente
¿Hay alguna manera de eliminar la matriz de dependencias con gulp ng constant? No tengo ninguna dependencia a mis constantes como tú tienes en esto, por ejemplo, "ngAnimate". Si no lo incluyo, obtengo una matriz de dependencia vacía como angular.module ("my.module.config", []) pero quiero la salida como angular.module ("my.module.config"). No veo ninguna opción en gulp ng constant pero veo que puedes pasar deps: false en grunt ng constant package. ¿Alguna ayuda?
Arun Gopalpuri
17

Para lograr eso, le sugiero que use el complemento de entorno AngularJS: https://www.npmjs.com/package/angular-environment

Aquí hay un ejemplo:

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();
});

Y luego, puede llamar a las variables desde sus controladores como este:

envService.read('apiUrl');

Espero eso ayude.

juanpablob
fuente
1
¿Cómo cambia él entre desarrollo y producción?
Mawg dice reinstalar a Mónica el
Hola Juan Pablo, o @Mawg si lo descubriste. Antes de hacer una pregunta sobre SO / plantear un problema en Github; ¿Cómo angular-environmentdetecta el medio ambiente? es decir, ¿qué necesita hacer en su máquina / servidor web local para que sepa que es dev / prod respectivamente?
StevieP
Leyendo los documentos de nuevo ... "envServiceProvider.check() ... establecerá automáticamente el entorno apropiado basado en dominios dados". Por lo tanto, creo que detecta el dominio actual y establece el entorno de manera adecuada, ¡es hora de probarlo!
StevieP
13

Puede usar lvh.me:9000para acceder a su aplicación AngularJS ( lvh.mesolo apunta a 127.0.0.1) y luego especificar un punto final diferente si lvh.mees el 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/';
  }
});

Y luego inyecte el servicio de configuración y úselo Configuration.APIdonde necesite acceder a la API:

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

Un poco torpe, pero funciona bien para mí, aunque en una situación ligeramente diferente (los puntos finales de API difieren en producción y desarrollo).

Jure Triglav
fuente
1
así que creo que muchas veces las personas complican demasiado las cosas. El simple uso de window.location.hostfue más que suficiente para mí.
joseym
7

También podríamos hacer algo como esto.

(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];
            }
        }

    });

})();

Y en su controller/service, podemos inyectar la dependencia y llamar al método get con propiedad para acceder.

(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') devolvería la url en función del entorno del host.

Thalaivar
fuente
5

¡Buena pregunta!

Una solución podría ser continuar usando su archivo config.xml y proporcionar información de punto final de API desde el backend a su html generado, como este (ejemplo en php):

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

Quizás no sea una solución bonita, pero funcionaría.

Otra solución podría ser mantener el API_END_POINT valor constante como debería estar en producción, y solo modificar su archivo de hosts para apuntar esa url a su API local.

O tal vez una solución que use localStoragepara anulaciones, como esta:

.factory('User',['$resource','API_END_POINT'],function($resource,API_END_POINT){
   var myApi = localStorage.get('myLocalApiOverride');
   return $resource((myApi || API_END_POINT) + 'user');
});
joakimbeng
fuente
Hola joakimbeng, escribí la solución que uso en php para explicar el punto. Estamos tratando de codificar un cliente javascript puro con un backend java RESTful puro, por lo que la mezcla php / js no es mi caso y también cuando escribo en php siempre trato de mantener php y js no mezclados. pero gracias por la respuesta. Creo que la solución de respuesta @kfis podría funcionar: un archivo configuration.js que no esté bajo el control de versión y que contenga un módulo de configuración. Con este enfoque, puedo inyectar / cargar también un módulo de configuración diferente para fines de prueba, si es necesario. Gracias chicos.
rbarilani
@ hal9087 Estoy totalmente de acuerdo con la parte de mezclar idiomas, se debe evitar a toda costa :) También me gusta la solución configuration.js, ¡lo tendré en cuenta cuando necesite algo similar!
joakimbeng
4

Muy tarde al hilo, pero una técnica que he usado, pre-angular, es aprovechar JSON y la flexibilidad de JS para hacer referencia dinámica a las claves de recopilación, y usar datos inalienables del entorno (nombre del servidor host, idioma actual del navegador) , etc.) como entradas para discriminar selectivamente / preferir nombres de clave con sufijo dentro de una estructura de datos JSON.

Esto proporciona no solo el contexto del entorno de despliegue (por OP) sino cualquier contexto arbitrario (como el lenguaje) para proporcionar i18n o cualquier otra variación requerida simultáneamente e (idealmente) dentro de un único manifiesto de configuración, sin duplicación, y legible obvio.

EN ACERCA DE 10 LÍNEAS DE VAINILLA JS

Ejemplo demasiado simplificado pero clásico: una URL base de punto final de API en un archivo de propiedades con formato JSON que varía según el entorno donde (natch) el servidor host también variará:

    ...
    '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/'
    },
    ...

Una clave para la función de discriminación es simplemente el nombre de host del servidor en la solicitud.

Esto, naturalmente, se puede combinar con una clave adicional basada en la configuración de idioma del usuario:

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

El alcance de la discriminación / preferencia puede limitarse a teclas individuales (como arriba) donde la tecla "base" solo se sobrescribe si hay una tecla coincidente + sufijo para las entradas a la función, o una estructura completa, y esa estructura misma analizado recursivamente para igualar sufijos de discriminación / preferencia:

    '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]'
    },

Por lo tanto, si un usuario visitante del sitio web de producción tiene una configuración de preferencia de idioma ( de ) alemán , la configuración anterior colapsaría para:

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

¿Cómo es una función de reescritura JSON de preferencia / discriminación mágica? No mucho:

// 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);

    };
};

En nuestras implementaciones, que incluyen sitios web angulares y pre-angulares, simplemente arrancamos la configuración muy por delante de otras llamadas de recursos colocando el JSON dentro de un cierre JS autoejecutable, incluida la función prefer (), y alimentamos las propiedades básicas del nombre de host y código de idioma (y acepta cualquier sufijo arbitrario adicional que pueda necesitar):

(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])  ] );

Un sitio pre-Angular ahora tendría una ventana contraída (sin @ llaves con sufijo) window.app_props para consultar.

Un sitio angular, como un paso de arranque / inicio, simplemente copia el objeto de utilería muerto en $ rootScope y (opcionalmente) lo destruye desde el alcance global / ventana

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

para ser inyectado posteriormente en los controladores:

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

o referido desde enlaces en vistas:

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

Advertencias? El carácter @ no es válido JS / JSON variable / nombre de clave, pero hasta ahora aceptado. Si eso es un factor decisivo, sustituya cualquier convención que desee, como "__" (subrayado doble) siempre y cuando se adhiera a ella.

La técnica podría aplicarse del lado del servidor, portada a Java o C #, pero su eficiencia / compacidad puede variar.

Alternativamente, la función / convención podría ser parte de su script de compilación front-end, de modo que el JSON completo sangriento de todo entorno / lenguaje nunca se transmite por cable.

ACTUALIZAR

Hemos desarrollado el uso de esta técnica para permitir múltiples sufijos a una clave, para evitar ser forzado a usar colecciones (aún puede, tan profundamente como desee), y también para honrar el orden de los sufijos preferidos.

Ejemplo (también vea trabajar 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 (uso básico) prefiere las teclas '@dev', descarta todas las demás teclas con sufijo

3 prefiere '@dev' sobre '@fr', prefiere '@ dev & fr' sobre todos los demás

4 (igual que 3 pero prefiere '@fr' sobre '@dev')

5 sin sufijos preferidos, elimina TODAS las propiedades con sufijo

Esto se logra al calificar cada propiedad con sufijo y promover el valor de una propiedad con sufijo a la propiedad sin sufijo al iterar sobre las propiedades y encontrar un sufijo con una puntuación más alta.

Algunas eficiencias en esta versión, incluida la eliminación de la dependencia de JSON para copiar en profundidad, y solo recurrir a objetos que sobreviven a la ronda de puntuación en su profundidad:

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());
}
Storsoc
fuente
2

Si está utilizando Brunch , el complemento Constangular lo ayuda a administrar variables para diferentes entornos.

jvannistelrooy
fuente
-8

¿Has visto esta pregunta y su respuesta?

Puede establecer un valor globalmente válido para su aplicación como este:

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

y luego úsalo en tus servicios. Puede mover este código a un archivo config.js y ejecutarlo al cargar la página u otro momento conveniente.

thalador
fuente
77
¿Podría alguien explicar por qué esta es una respuesta tan mala? Se ha rechazado masivamente, pero ni un solo comentario ...
aendrew
55
Esto es viejo como el infierno, pero si tuviera que adivinar por qué los votos negativos, es porque no aborda el problema de las configuraciones específicas del entorno, es solo una sugerencia para usar .value () para establecer un valor global en cualquier aplicación antigua. No se menciona cómo se usaría esto dependiendo de env o cualquier cosa a lo largo de los parámetros de las preguntas originales.
coblr