Como configurar diferentes ambientes no Angular.js?

220

Como você gerencia variáveis ​​/ constantes de configuração para diferentes ambientes?

Este poderia ser um exemplo:

Minha API de descanso está acessível localhost:7080/myapi/, mas meu amigo que trabalha no mesmo código sob o controle de versão do Git tem a API implantada em seu Tomcat localhost:8099/hisapi/.

Supondo que temos algo parecido com isto:

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

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

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

Como injeto dinamicamente o valor correto do terminal da API, dependendo do ambiente?

No PHP, geralmente faço esse tipo de coisa com um config.username.xmlarquivo, mesclando o arquivo de configuração básica (config.xml) com o arquivo de configuração do ambiente local reconhecido pelo nome do usuário. Mas não sei como gerenciar esse tipo de coisa em JavaScript?

rbarilani
fonte

Respostas:

209

Estou um pouco atrasado para o tópico, mas se você estiver usando o Grunt , tive grande sucesso grunt-ng-constant.

A seção de configuração para ngconstantna minha Gruntfile.jsaparência como

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

As tarefas que usam se ngconstantparecem

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

Portanto, a execução grunt servergerará um config.jsarquivo app/scripts/que se parece com

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

Por fim, declaro a dependência de quaisquer módulos necessários:

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

Agora minhas constantes podem ser injetadas em dependência quando necessário. Por exemplo,

app.controller('MyController', ['ENV', function( ENV ) {
  if( ENV === 'production' ) {
    ...
  }
}]);
André Dion
fonte
10
Em vez de colocar 'ngconstant:development'em 'serve'- se você colocá-lo na configuração do relógio sob a 'gruntfile'como tasks: ['ngconstant:development']- você não precisará reiniciar grunt servequando você atualizar as variáveis de desenvolvimento na gruntfile.
precisa saber é
10
Em vez de adicionar suas constantes no gruntfile.js, você pode colocar em arquivos separados assim: #package: grunt.file.readJSON('development.json')
Guilhem Soulas
3
Há uma sintaxe atualizada para Gruntfile.js na versão 0.5 do grunt-ng-constant: github.com/werk85/grunt-ng-constant/issues/31 . Ótima resposta, obrigado!
precisa saber é o seguinte
10
Para quem usa gulp, há gulp-ng-constant .
Dheeraj Vepakomma
4
Descobri Também é necessário incluir o arquivo scripts / config.js no angular para encontrar o módulo, assim: <script src = "scripts / config.js"> </script>
Toni Gamez
75

Uma solução interessante pode ser a separação de todos os valores específicos do ambiente em algum módulo angular separado, do qual todos os outros módulos dependem:

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

Em seguida, seus módulos que precisam dessas entradas podem declarar uma dependência:

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

Agora você pode pensar em outras coisas legais:

O módulo, que contém a configuração, pode ser separado em configuration.js, que será incluído na sua página.

Esse script pode ser facilmente editado por cada um de vocês, contanto que você não verifique este arquivo separado no git. Mas é mais fácil não verificar a configuração se ela estiver em um arquivo separado. Além disso, você pode ramificá-lo localmente.

Agora, se você tiver um sistema de compilação, como ANT ou Maven, suas etapas adicionais poderão implementar alguns espaços reservados para os valores API_END_POINT, que serão substituídos durante o tempo de compilação pelos seus valores específicos.

Ou você tem o seu configuration_a.jse configuration_b.jse decidir no backend que incluir.

kfis
fonte
30

Para usuários do Gulp , gulp-ng-constant também é útil combinado com 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() { });
});

Na minha pasta de configuração, tenho estes arquivos:

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

Então você pode executar gulp config --env developmente isso criará algo como isto:

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

Eu também tenho esta especificação:

beforeEach(module('app'));

it('loads the config', inject(function(config) {
  expect(config).toBeTruthy();
}));
Rimian
fonte
Existe uma maneira de remover a matriz de dependência com gulp ng constante? Eu não tenho nenhuma dependência de minhas constantes como você tem neste exemplo, "ngAnimate". Se não incluí-lo, recebo uma matriz de dependência vazia como angular.module ("my.module.config", []), mas quero a saída como angular.module ("my.module.config"). Não vejo nenhuma opção no gulp ng constant, mas vejo que você pode passar deps: false no grunt ng constant package. Qualquer ajuda?
Arun Gopalpuri
17

Para conseguir isso, sugiro que você use o Plug-in de ambiente AngularJS: https://www.npmjs.com/package/angular-environment

Aqui está um exemplo:

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

E então, você pode chamar as variáveis ​​de seus controladores como este:

envService.read('apiUrl');

Espero que ajude.

juanpablob
fonte
1
como ele alterna entre desenvolvimento e produção?
Mawg diz que restabelece Monica
Olá Juan Pablo, ou @Mawg, se você descobriu. Antes de fazer uma pergunta sobre SO / levantar uma questão no Github; como angular-environmentdetecta o ambiente? ou seja, o que você precisa fazer no seu computador / servidor da web local para que ele saiba que é dev / prod, respectivamente?
21716 StevieP
A leitura dos documentos novamente ... " envServiceProvider.check()... definirá automaticamente o ambiente apropriado com base nos domínios especificados". Então eu acho que ele detecta o domínio atual e define o ambiente adequadamente - é hora de testá-lo!
21716 StevieP
13

Você pode usar lvh.me:9000para acessar o aplicativo AngularJS ( lvh.meapenas aponta para 127.0.0.1) e, em seguida, especificar um terminal diferente se lvh.mefor o 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/';
  }
});

E, em seguida, injete o serviço de configuração e use Configuration.APIsempre que precisar acessar a API:

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

Um pouco desajeitado, mas funciona bem para mim, embora em uma situação um pouco diferente (os pontos de extremidade da API diferem em produção e desenvolvimento).

Jure Triglav
fonte
1
então acho que muitas vezes as pessoas complicam demais as coisas. O simples uso de window.location.hostera mais do que suficiente para mim.
joseym
7

Nós também poderíamos fazer algo assim.

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

    });

})();

E no seu controller/service, podemos injetar a dependência e chamar o método get com a propriedade a ser acessada.

(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') retornaria o URL com base no ambiente host.

Thalaivar
fonte
5

Boa pergunta!

Uma solução poderia ser continuar usando o arquivo config.xml e fornecer informações sobre o ponto de extremidade da API do backend para o html gerado, como este (exemplo em php):

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

Talvez não seja uma solução bonita, mas funcionaria.

Outra solução poderia ser manter o API_END_POINTvalor constante como deveria estar em produção e modificar apenas o arquivo host para apontar esse URL para a API local.

Ou talvez uma solução usando localStoragesubstituições, 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
fonte
Oi joakimbeng, eu escrevi a solução que eu uso no php para explicar o ponto. Estamos tentando codificar um cliente javascript puro com um back-end Java RESTful puro, portanto o mix php / js não é o meu caso e também quando escrevo em php, sempre tento manter php e js não misturados. mas obrigado pela resposta. Acho que a solução de resposta do @kfis pode funcionar: um arquivo configuration.js não está sob controle de versão que contém um módulo de configuração. Com essa abordagem, posso injetar / carregar também um módulo de configuração diferente para fins de teste, se necessário. Obrigado rapazes.
Rbarilani #
@ hal9087 Concordo totalmente com a parte das linguagens de mixagem, deve ser evitada a todo custo :) Gosto também da solução configuration.js. Vou lembrá-la quando precisar de algo semelhante!
Joakimbeng #
4

Muito tarde para o encadeamento, mas uma técnica que usei, pré-Angular, é aproveitar o JSON e a flexibilidade do JS para fazer referência dinâmica às chaves de coleção e usar fatos inalienáveis ​​do ambiente (nome do servidor host, idioma atual do navegador , etc.) como entradas para discriminar / preferir seletivamente nomes-chave com sufixo em uma estrutura de dados JSON.

Isso fornece não apenas o contexto do ambiente de implantação (por OP), mas qualquer contexto arbitrário (como a linguagem) para fornecer o i18n ou qualquer outra variação necessária simultaneamente e (idealmente) em um único manifesto de configuração, sem duplicação e claramente óbvio.

EM MAIS DE 10 LINHAS VANILLA JS

Exemplo excessivamente simplificado, mas clássico: um URL base do terminal da API em um arquivo de propriedades formatado em JSON que varia de acordo com o ambiente em que (natch) o servidor host também varia:

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

Uma chave para a função de discriminação é simplesmente o nome do host do servidor na solicitação.

Naturalmente, isso pode ser combinado com uma chave adicional com base nas configurações de idioma do usuário:

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

O escopo da discriminação / preferência pode ser limitado a chaves individuais (como acima), onde a chave "base" é substituída apenas se houver uma chave correspondente + sufixo para as entradas da função - ou uma estrutura inteira, e essa estrutura em si. analisado recursivamente para combinar sufixos de discriminação / preferência:

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

Assim, se um usuário visitar o site da produção tem Alemão ( de ) configuração de preferência de idioma, a configuração acima entraria em colapso para:

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

Como é uma função mágica de reescrita de JSON de preferência / discriminação? Não muito:

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

    };
};

Em nossas implementações, que incluem sites Angular e pré-Angular, simplesmente inicializamos a configuração bem antes de outras chamadas de recursos, colocando o JSON dentro de um fechamento JS auto-executável, incluindo a função prefer (), e alimentamos propriedades básicas de hostname e código de idioma (e aceita quaisquer sufixos arbitrários adicionais que você possa precisar):

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

Um site pré-angular teria agora de Reduzido (teclas sufixo há @) window.app_props se referir.

Um site Angular, como uma etapa de bootstrap / init, simplesmente copia o objeto props descartado para $ rootScope e (opcionalmente) o destrói do escopo global / window

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

para ser posteriormente injetado nos controladores:

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

ou referido de ligações em visualizações:

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

Ressalvas? O caractere @ não é válido para nomeação de variável / chave JS / JSON, mas até agora aceito. Se isso for um rompimento de um contrato, substitua qualquer convenção que você goste, como "__" (sublinhado duplo), desde que você cumpra.

A técnica pode ser aplicada no servidor, portada para Java ou C #, mas sua eficiência / compactação pode variar.

Como alternativa, a função / convenção pode fazer parte do seu script de compilação de front-end, para que o JSON sangrento de todo o ambiente / todos os idiomas nunca seja transmitido por fio.

ATUALIZAR

Desenvolvemos o uso dessa técnica para permitir vários sufixos em uma chave, para evitar ser forçado a usar coleções (você ainda pode, tão profundamente quanto quiser) e também para honrar a ordem dos sufixos preferidos.

Exemplo (veja também jsFiddle de trabalho ):

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) prefere as teclas '@dev', descarta todas as outras chaves com sufixo

3 prefere '@dev' sobre '@fr', prefere '@ dev & fr' sobre todos os outros

4 (igual a 3, mas prefere '@fr' em vez de '@dev')

5 sem sufixos preferidos, descarta TODAS as propriedades com sufixo

Isso é feito marcando cada propriedade com sufixo e promovendo o valor de uma propriedade com sufixo para a propriedade sem sufixo ao iterar sobre as propriedades e encontrar um sufixo com pontuação mais alta.

Algumas eficiências nesta versão, incluindo a remoção da dependência do JSON para cópia em profundidade e a recorrência apenas em objetos que sobrevivem à rodada de pontuação em profundidade:

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
fonte
2

Se você estiver usando o Brunch , o plug-in Constangular ajuda a gerenciar variáveis ​​para diferentes ambientes.

jvannistelrooy
fonte
-8

Você já viu essa pergunta e sua resposta?

Você pode definir um valor válido globalmente para seu aplicativo como este:

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

e depois use-o em seus serviços. Você pode mover esse código para um arquivo config.js e executá-lo no carregamento da página ou em outro momento conveniente.

thalador
fonte
7
Alguém poderia explicar por que essa resposta é tão ruim? Foi massivamente diminuído, mas nem um único comentário ...
aendrew
5
Isso é antigo como o inferno, mas se eu tivesse que adivinhar por que os votos negativos, é porque ele não aborda o problema de configurações específicas do ambiente, é apenas uma sugestão para usar .value () para definir um valor global em qualquer aplicativo antigo. Não há menção sobre como alguém usaria isso, dependendo do ambiente ou de qualquer coisa ao longo dos parâmetros das perguntas originais.
coblr