如何在Angular.js中配置不同的环境?


220

如何管理不同环境的配置变量/常量?

这可能是一个示例:

我的其余API可以访问localhost:7080/myapi/,但在Git版本控制下使用相同代码工作的朋友在Tomcat上已部署了该API localhost:8099/hisapi/

假设我们有这样的东西:

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

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

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

如何根据环境动态注入API端点的正确值?

在PHP中,我通常使用config.username.xml文件来执行此类操作,将基本​​配置文件(config.xml)与用户名称识别的本地环境配置文件合并。但是我不知道如何在JavaScript中管理这种事情?

Answers:


209

我现在还不算很晚,但是如果您使用的是Grunt,那么我会取得很大的成功grunt-ng-constant

ngconstant在我的配置部分Gruntfile.js看起来像

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

使用的任务ngconstant看起来像

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

所以运行grunt server会产生一个config.js文件app/scripts/,看起来像

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

最后,我声明对任何需要它的模块的依赖:

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

现在,我的常量可以在需要的地方进行依赖注入。例如,

app.controller('MyController', ['ENV', function( ENV ) {
  if( ENV === 'production' ) {
    ...
  }
}]);

10
而不是把'ngconstant:development''serve'-如果你把它放在手表的config下'gruntfile'tasks: ['ngconstant:development']-你不会需要重新启动grunt serve时,您更新的gruntfile发展的变量。
花了

10
无需在gruntfile.js中添加常量,您可以像这样放置单独的文件:package: grunt.file.readJSON('development.json')
Guilhem

3
在0.5版的grunt-ng-constant中存在针对Gruntfile.js的更新语法:github.com/werk85/grunt-ng-constant/issues/31。好答案,谢谢!
pherris 2014年

10
对于使用gulp的用户,可以使用gulp -ng-constant
Dheeraj Vepakomma 2014年

4
我发现还需要将scripts / config.js文件包含到angular中以找到模块,例如:<script src =“ scripts / config.js”> </ script>
Toni Gamez 2014年

75

一个很酷的解决方案可能是将所有特定于环境的值分成一个单独的角度模块,所有其他模块都依赖于以下角度模块:

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

然后,需要这些条目的模块可以声明对它的依赖关系:

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

现在您可以考虑其他更酷的东西:

包含配置的模块可以分成configuration.js,它将包含在您的页面中。

只要您不将此单独的文件检入git中,每个人都可以轻松地编辑此脚本。但是,如果不在单独的文件中,则不检入配置会更容易。另外,您可以在本地分支。

现在,如果您有一个像ANT或Maven这样的构建系统,那么您的进一步步骤可能是为值API_END_POINT实现一些占位符,这些占位符将在构建时被替换为您的特定值。

或者,您有您的configuration_a.jsand,configuration_b.js然后在后端确定要包含的内容。


30

对于Gulp用户而言,gulp-ng-constantgulp-concat结合使用也很有用,gulp event-streamyargs

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

在我的配置文件夹中,我有以下文件:

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

然后,您可以运行gulp config --env development,这将创建如下内容:

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

我也有这个规格:

beforeEach(module('app'));

it('loads the config', inject(function(config) {
  expect(config).toBeTruthy();
}));

有没有办法用gulp ng常量删除依赖项数组?我对我的常量没有任何依赖性,例如“ ngAnimate”。如果不包含它,则会得到一个空的依赖项数组,例如angular.module(“ my.module.config”,[]),但我希望输出为angular.module(“ my.module.config”)。我在gulp ng constant中看不到任何选项,但我看到可以在grunt ng constant包中传递deps:false。有什么帮助吗?
阿伦·戈帕普里

17

为此,我建议您使用AngularJS环境插件:https : //www.npmjs.com/package/angular-environment

这是一个例子:

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

然后,您可以从控制器中调用变量,如下所示:

envService.read('apiUrl');

希望能帮助到你。


1
他如何在开发和生产之间切换?
Mawg说恢复Monica 2015年

嗨,Juan Pablo,或者@Mawg(如果您知道的话)。在我问关于SO的问题/在Github上提出问题之前;如何angular-environment检测环境?即,您需要在本地计算机/ Web服务器上执行什么操作,以使其分别知道它是dev / prod?
StevieP

再次阅读文档...“ envServiceProvider.check()...将根据给定的域自动设置适当的环境”。因此,我认为它可以检测到当前域并适当地设置环境-是时候对其进行测试了!
StevieP

13

您可以lvh.me:9000用来访问AngularJS应用程序(lvh.me仅指向127.0.0.1),然后指定另一个端点(如果lvh.me是主机):

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

然后注入配置服务并Configuration.API在需要访问API的任何地方使用:

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

有点笨拙,但对我来说效果很好,尽管情况略有不同(API端点在生产和开发中有所不同)。


1
所以我认为人们常常把事情复杂化。简单的使用window.location.host对我来说绰绰有余。
2014年

7

我们也可以这样做。

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

    });

})();

在您的中controller/service,我们可以注入依赖项并调用具有要访问属性的get方法。

(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') 将根据主机环境返回url。


5

好问题!

一种解决方案是继续使用config.xml文件,并从后端向生成的html提供api端点信息,如下所示(例如php中的示例):

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

也许不是一个很好的解决方案,但它可以工作。

另一个解决方案可能是保持 API_END_POINT生产中应使用恒定值,而仅修改您的hosts-file,以将该URL指向本地api。

或者也许是使用localStorage替代的解决方案,例如:

.factory('User',['$resource','API_END_POINT'],function($resource,API_END_POINT){
   var myApi = localStorage.get('myLocalApiOverride');
   return $resource((myApi || API_END_POINT) + 'user');
});

嗨,joakimbeng,我写了我在php中使用的解决方案来解释这一点。我们正在尝试用一个纯RESTful Java后端编写一个纯JavaScript客户端,所以php / js混合并不是我的情况,而且当我用php编写时,我总是尝试保持php和js不混合。但感谢您的回答。我认为@kfis答案解决方案可以工作:不在版本控制下的configuration.js文件包含配置模块。通过这种方法,我可以根据需要注入/加载不同的配置模块以进行测试。多谢你们。
rbarilani

@ hal9087我完全同意混合语言部分,应该不惜一切代价避免:)我也喜欢configuration.js解决方案,当我需要类似的东西时,我会牢记在心!
joakimbeng

4

线程很晚,但是我在Angular之前使用的一种技术是利用JSON和JS的灵活性来动态引用集合键,并使用环境的不可分割的事实(主机服务器名称,当前浏览器语言)等)作为输入,以选择性地区分/优先选择JSON数据结构中的后缀键名。

这不仅提供了部署环境上下文(每个OP),而且还提供了任意上下文(例如语言)以同时(在理想情况下)在单个配置清单中提供i18n或任何其他所需的变体,而无需重复且易于理解。

关于10行VANILLA JS

过于简化但经典的示例:JSON格式的属性文件中的API终结点基础URL随环境而异,主机服务器在(环境)上也会有所不同:

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

区分功能的关键只是请求中的服务器主机名。

自然,可以根据用户的语言设置将其与其他键组合使用:

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

辨别/偏好的范围可以限于单个键(如上),其中只有在为该函数的输入有匹配的键+后缀或整个结构以及该结构本身的情况下,才会覆盖“基本”键递归解析以匹配歧视/偏好后缀:

    'help': {
        'BLURB': 'This pre-production environment is not supported. Contact Development Team with questions.',
        'PHONE': '808-867-5309',
        'EMAIL': 'coder.jen@lostnumber.com'
    },
    'help@www.productionwebsite.com': {
        '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': 'customer.service@productionwebsite.com'
    },

因此,如果访问生产网站的用户具有德语(de)语言首选项设置,则上述配置将折叠为:

    'help': {
        'BLURB': 'Bitte kontaktieren Sie unseren Kundendienst!!1!',
        'PHONE': '1-800-CUS-TOMR',
        'EMAIL': 'customer.service@productionwebsite.com'
    },

这种神奇的偏好/区分JSON重写功能是什么样的?不多:

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

    };
};

在包括Angular和pre-Angular网站的实现中,我们通过将JSON放入自执行的JS闭包中(包括preferred()函数),并馈入主机名和语言代码(并接受您可能需要的任何其他任意后缀):

(function(prefs){ var props = {
    'svcs': {
        'VER': '2.3',
        'API@localhost': 'http://localhost:9090/',
        'API@www.uat.productionwebsite.com': 'https://www.uat.productionwebsite.com:9090/res/',
        'API@www.productionwebsite.com': '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])  ] );

Angular之前的站点现在将具有折叠的(没有@后缀键)window.app_props可供参考。

作为引导/初始化步骤,Angular站点只需将死掉的prop对象复制到$ rootScope中,并(可选)将其从全局/窗口范围中销毁

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

随后注入到控制器中:

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

或从视图的绑定中引用:

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

注意事项?@字符不是有效的JS / JSON变量/键命名,但到目前为止已被接受。如果这违反了协议,只要您坚持使用它,就可以替换任何喜欢的约定,例如“ __”(双下划线)。

该技术可以在服务器端应用,移植到Java或C#,但效率/紧凑性可能会有所不同。

另外,功能/约定也可能是前端编译脚本的一部分,因此,完整的,全环境/全语言的JSON永远不会通过网络传输。

更新

我们已经改进了该技术的用法,以允许一个键使用多个后缀,避免被迫使用集合(您仍然可以根据需要深度使用),并遵守首选后缀的顺序。

示例(另请参见工作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(基本用法)倾向于使用'@dev'键,并丢弃所有其他后缀键

3比“ @fr”更喜欢“ @dev”,比所有其他偏爱“ @ dev&fr”

4(与3相同,但比起“ @dev”更喜欢“ @fr”)

5没有首选后缀,删除所有后缀属性

它通过对每个带后缀的属性进行评分,并在对属性进行迭代并找到更高分的后缀时,将带后缀的属性的值提升为非带后缀的属性来实现。

此版本中的一些效率,包括消除对JSON的依赖以进行深度复制,以及仅递归到在深度得分中幸存下来的对象:

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


-8

你看到这个问题了及其答案了吗?

您可以像这样为您的应用设置全局有效值:

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

然后在您的服务中使用它。您可以将该代码移动到config.js文件,并在页面加载或其他方便的时候执行它。


7
有人可以解释为什么这是一个不好的答案吗?它遭到了广泛的否决,但未发表任何评论……
致谢

5
这已经很古老了,但是如果我不得不猜测为什么要投票,那是因为它没有解决特定于环境的配置问题,这只是建议在任何旧应用中使用.value()设置全局值。没有提及如何根据环境或原始问题参数中的任何内容使用此方法。
coblr 2014年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.