AngularJS:用asynchronous数据初始化服务

我有一个AngularJS服务,我想用一些asynchronous数据进行初始化。 像这样的东西:

myModule.service('MyService', function($http) { var myData = null; $http.get('data.json').success(function (data) { myData = data; }); return { setData: function (data) { myData = data; }, doStuff: function () { return myData.getSomeData(); } }; }); 

显然,这是行不通的,因为如果在myData返回之前试图调用doStuff() ,我将得到一个空指针exception。 据我所知,在这里和这里提出的其他一些问题,我有几个select,但没有一个看起来很干净(也许我错过了一些东西):

安装服务与“运行”

当设置我的应用程序这样做:

 myApp.run(function ($http, MyService) { $http.get('data.json').success(function (data) { MyService.setData(data); }); }); 

然后我的服务将如下所示:

 myModule.service('MyService', function() { var myData = null; return { setData: function (data) { myData = data; }, doStuff: function () { return myData.getSomeData(); } }; }); 

这在某些时候是有效的,但是如果asynchronous数据碰巧花费的时间比初始化所需要的时间长,那么当我调用doStuff()时,会得到一个空指针exception。

使用承诺对象

这可能会奏效。 唯一的缺点就是在我称之为MyService的地方,我将不得不知道doStuff()返回一个promise,所有的代码都要和我们交互。 我宁愿等到myData重新载入我的应用程序之前。

手动引导

 angular.element(document).ready(function() { $.getJSON("data.json", function (data) { // can't initialize the data here because the service doesn't exist yet angular.bootstrap(document); // too late to initialize here because something may have already // tried to call doStuff() and would have got a null pointer exception }); }); 

全局Javascript Var我可以直接发送我的JSON到一个全局的Javascriptvariables:

HTML:

 <script type="text/javascript" src="data.js"></script> 

data.js:

 var dataForMyService = { // myData here }; 

然后在初始化MyService时可用:

 myModule.service('MyService', function() { var myData = dataForMyService; return { doStuff: function () { return myData.getSomeData(); } }; }); 

这也可以,但是我有一个全球性的JavaScriptvariables,味道不好。

这些是我唯一的select吗? 这些select之一是否比其他select更好? 我知道这是一个相当长的问题,但我想表明,我试图探索我所有的select。 任何指导将非常感激。

你有没有看过$routeProvider.when('/path',{ resolve:{...} ?它可以使承诺的方法有点干净:

在服务中承诺:

 app.service('MyService', function($http) { var myData = null; var promise = $http.get('data.json').success(function (data) { myData = data; }); return { promise:promise, setData: function (data) { myData = data; }, doStuff: function () { return myData;//.getSomeData(); } }; }); 

添加resolve到你的路由configuration:

 app.config(function($routeProvider){ $routeProvider .when('/',{controller:'MainCtrl', template:'<div>From MyService:<pre>{{data | json}}</pre></div>', resolve:{ 'MyServiceData':function(MyService){ // MyServiceData will also be injectable in your controller, if you don't want this you could create a new promise with the $q service return MyService.promise; } }}) }): 

在解决所有依赖关系之前,您的控制器将不会被实例化:

 app.controller('MainCtrl', function($scope,MyService) { console.log('Promise is now resolved: '+MyService.doStuff().data) $scope.data = MyService.doStuff(); }); 

我在plnkr上做了一个例子: http ://plnkr.co/edit/GKg21XH0RwCMEQGUdZKH?p=preview

基于Martin Atkins的解决scheme,下面是一个完整,简洁的纯angular度解决scheme:

 (function() { var initInjector = angular.injector(['ng']); var $http = initInjector.get('$http'); $http.get('/config.json').then( function (response) { angular.module('config', []).constant('CONFIG', response.data); angular.element(document).ready(function() { angular.bootstrap(document, ['myApp']); }); } ); })(); 

这个解决scheme使用一个自动执行的匿名函数来获取$ http服务,请求configuration,并在CONFIGvariables可用时将其注入到一个常量中。

一旦完成,我们等待文档准备就绪,然后引导Angular应用程序。

与Martin的解决scheme相比,这是一个轻微的增强,延迟取得configuration,直到文档准备好。 据我所知,没有理由推迟$ http呼吁。

unit testing

注意:我发现这个解决scheme在代码包含在app.js文件中进行unit testing的时候效果不好。 原因是上面的代码在加载JS文件时立即运行。 这意味着testing框架(在我的情况下,茉莉花)没有机会提供$http的模拟实现。

我不完全满意的解决scheme是将此代码移到我们的index.html文件中,所以Grunt / Karma / Jasmineunit testing基础结构看不到它。

我使用了类似于@XMLilley描述的方法,但希望能够像使用$http一样使用AngularJS服务来加载configuration,并在不使用低级API或jQuery的情况下进行进一步的初始化。

在路由上使用resolve也不是一种select,因为当我的应用程序启动时,即使在module.config()块中,我也需要这些值作为常量。

我创build了一个小的AngularJS应用程序加载configuration,将它们设置为实际应用程序的常量,并引导它。

 // define the module of your app angular.module('MyApp', []); // define the module of the bootstrap app var bootstrapModule = angular.module('bootstrapModule', []); // the bootstrapper service loads the config and bootstraps the specified app bootstrapModule.factory('bootstrapper', function ($http, $log, $q) { return { bootstrap: function (appName) { var deferred = $q.defer(); $http.get('/some/url') .success(function (config) { // set all returned values as constants on the app... var myApp = angular.module(appName); angular.forEach(config, function(value, key){ myApp.constant(key, value); }); // ...and bootstrap the actual app. angular.bootstrap(document, [appName]); deferred.resolve(); }) .error(function () { $log.warn('Could not initialize application, configuration could not be loaded.'); deferred.reject(); }); return deferred.promise; } }; }); // create a div which is used as the root of the bootstrap app var appContainer = document.createElement('div'); // in run() function you can now use the bootstrapper service and shutdown the bootstrapping app after initialization of your actual app bootstrapModule.run(function (bootstrapper) { bootstrapper.bootstrap('MyApp').then(function () { // removing the container will destroy the bootstrap app appContainer.remove(); }); }); // make sure the DOM is fully loaded before bootstrapping. angular.element(document).ready(function() { angular.bootstrap(appContainer, ['bootstrapModule']); }); 

看看它的行动(使用$timeout而不是$http ): $http : //plnkr.co/edit/FYznxP3xe8dxzwxs37hi?p=preview

UPDATE

我会推荐使用Martin Atkins和JBCP所描述的方法。

更新2

因为我在多个项目中需要它,所以我刚刚发布了一个处理这个问题的bower模块: https : //github.com/philippd/angular-deferred-bootstrap

从后端加载数据并在AngularJS模块上设置名为APP_CONFIG的常量的示例:

 deferredBootstrapper.bootstrap({ element: document.body, module: 'MyApp', resolve: { APP_CONFIG: function ($http) { return $http.get('/api/demo-config'); } } }); 

“手动引导”情况下,可以通过在引导之前手动创build注入器来访问Angular服务。 此初始注入器将独立(不附加到任何元素),只包含加载的模块的子集。 如果你所需要的只是核心的Angular服务,那么加载ng就足够了,像这样:

 angular.element(document).ready( function() { var initInjector = angular.injector(['ng']); var $http = initInjector.get('$http'); $http.get('/config.json').then( function (response) { var config = response.data; // Add additional services/constants/variables to your app, // and then finally bootstrap it: angular.bootstrap(document, ['myApp']); } ); } ); 

例如,您可以使用module.constant机制将数据提供给您的应用程序:

 myApp.constant('myAppConfig', data); 

这个myAppConfig现在可以像任何其他服务一样注入,特别是在configuration阶段可用:

 myApp.config( function (myAppConfig, someService) { someService.config(myAppConfig.someServiceConfig); } ); 

或者,对于较小的应用程序,只需将全局configuration直接注入到服务中,代价是在整个应用程序中传播关于configuration格式的知识。

当然,由于这里的asynchronous操作会阻止应用程序的引导,从而阻止模板的编译/链接,所以使用ng-cloak指令可以防止在工作过程中显示未parsing的模板。 您还可以在DOM中提供某种加载指示,方法是提供一些仅在AngularJS初始化之前才显示的HTML:

 <div ng-if="initialLoad"> <!-- initialLoad never gets set, so this div vanishes as soon as Angular is done compiling --> <p>Loading the app.....</p> </div> <div ng-cloak> <!-- ng-cloak attribute is removed once the app is done bootstrapping --> <p>Done loading the app!</p> </div> 

我在Plunker上创build了一个完整的工作示例 ,从静态JSON文件加载configuration。

我有同样的问题:我喜欢resolve对象,但只适用于ng-view的内容。 如果你有控制器(对于顶级导航,比方说,)存在于ng-view之外,并且在路由甚至开始发生之前需要使用数据进行初始化? 我们如何避免在服务器端进行操作?

使用手动引导和angular度常量 。 耐人寻味的XHR可以让你获得你的数据,并且在callback中引导angular度,处理你的asynchronous问题。 在下面的例子中,你甚至不需要创build一个全局variables。 返回的数据仅作为注入的angular度范围存在,除非注入,否则不存在于控制器,服务等内部。 (就像您将resolve对象的输出注入到路由视图的控制器中一样)。如果您希望此后将该数据作为服务进行交互,则可以创build服务,注入数据,并且不会有人聪明。

例:

 //First, we have to create the angular module, because all the other JS files are going to load while we're getting data and bootstrapping, and they need to be able to attach to it. var MyApp = angular.module('MyApp', ['dependency1', 'dependency2']); // Use angular's version of document.ready() just to make extra-sure DOM is fully // loaded before you bootstrap. This is probably optional, given that the async // data call will probably take significantly longer than DOM load. YMMV. // Has the added virtue of keeping your XHR junk out of global scope. angular.element(document).ready(function() { //first, we create the callback that will fire after the data is down function xhrCallback() { var myData = this.responseText; // the XHR output // here's where we attach a constant containing the API data to our app // module. Don't forget to parse JSON, which `$http` normally does for you. MyApp.constant('NavData', JSON.parse(myData)); // now, perform any other final configuration of your angular module. MyApp.config(['$routeProvider', function ($routeProvider) { $routeProvider .when('/someroute', {configs}) .otherwise({redirectTo: '/someroute'}); }]); // And last, bootstrap the app. Be sure to remove `ng-app` from your index.html. angular.bootstrap(document, ['NYSP']); }; //here, the basic mechanics of the XHR, which you can customize. var oReq = new XMLHttpRequest(); oReq.onload = xhrCallback; oReq.open("get", "/api/overview", true); // your specific API URL oReq.send(); }) 

现在,你的NavData常量存在。 继续并将其注入到控制器或服务中:

 angular.module('MyApp') .controller('NavCtrl', ['NavData', function (NavData) { $scope.localObject = NavData; //now it's addressable in your templates }]); 

当然,使用裸露的XHR对象会剥去$http或JQuery为您处理的一些细节,但是这个例子没有特别的依赖关系,至less对于一个简单的get 。 如果你想要更多的权力来请求,加载一个外部库来帮助你。 但是我不认为在这种情况下访问angular色的$http或其他工具是不可能的。

(SO 相关post )

你可以做的是在.config中为应用程序创build路由的parsing对象,并在函数中传递$ q(promise object)和你所依赖的服务的名字,并parsing$ http在服务中的callback函数就像这样:

路由configuration

 app.config(function($routeProvider){ $routeProvider .when('/',{ templateUrl: 'home.html', controller: 'homeCtrl', resolve:function($q,MyService) { //create the defer variable and pass it to our service var defer = $q.defer(); MyService.fetchData(defer); //this will only return when the promise //has been resolved. MyService is going to //do that for us return defer.promise; } }) } 

Angular将不会渲染模板或使控制器可用,直到调用defer.resolve()为止。 我们可以在我们的服务中这样做:

服务

 app.service('MyService',function($http){ var MyService = {}; //our service accepts a promise object which //it will resolve on behalf of the calling function MyService.fetchData = function(q) { $http({method:'GET',url:'data.php'}).success(function(data){ MyService.data = data; //when the following is called it will //release the calling function. in this //case it's the resolve function in our //route config q.resolve(); } } return MyService; }); 

既然MyService有分配给它的数据属性的数据,并且路由parsing对象中的承诺已经解决了,我们的路由控制器就进入了生命周期,我们可以将服务中的数据分配给我们的控制器对象。

CONTROLLER

  app.controller('homeCtrl',function($scope,MyService){ $scope.servicedata = MyService.data; }); 

现在,我们在控制器范围内的所有绑定将能够使用来自MyService的数据。

所以我find了一个解决scheme 我创build了一个angularJS服务,我们将其称为MyDataRepository,并为其创build了一个模块。 然后我从服务器端控制器提供这个JavaScript文件:

HTML:

 <script src="path/myData.js"></script> 

服务器端:

 @RequestMapping(value="path/myData.js", method=RequestMethod.GET) public ResponseEntity<String> getMyDataRepositoryJS() { // Populate data that I need into a Map Map<String, String> myData = new HashMap<String,String>(); ... // Use Jackson to convert it to JSON ObjectMapper mapper = new ObjectMapper(); String myDataStr = mapper.writeValueAsString(myData); // Then create a String that is my javascript file String myJS = "'use strict';" + "(function() {" + "var myDataModule = angular.module('myApp.myData', []);" + "myDataModule.service('MyDataRepository', function() {" + "var myData = "+myDataStr+";" + "return {" + "getData: function () {" + "return myData;" + "}" + "}" + "});" + "})();" // Now send it to the client: HttpHeaders responseHeaders = new HttpHeaders(); responseHeaders.add("Content-Type", "text/javascript"); return new ResponseEntity<String>(myJS , responseHeaders, HttpStatus.OK); } 

然后,我可以在任何需要的地方注入MyDataRepository:

 someOtherModule.service('MyOtherService', function(MyDataRepository) { var myData = MyDataRepository.getData(); // Do what you have to do... } 

这对我来说很好,但如果有人有任何反馈意见,我很乐意接受。 }

另外,在执行实际控制器之前,可以使用以下技术来全局configuration服务: https : //stackoverflow.com/a/27050497/1056679 。 只需全局parsing数据,然后在run块中将其传递给您的服务。

您可以使用JSONPasynchronous加载服务数据。 JSONP请求将在初始页面加载期间进行,结果将在应用程序启动之前可用。 这样你就不用用冗余的解决方法来扩充你的路由。

你的HTML看起来像这样:

 <script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script> <script> function MyService { this.getData = function(){ return MyService.data; } } MyService.setData = function(data) { MyService.data = data; } angular.module('main') .service('MyService', MyService) </script> <script src="/some_data.php?jsonp=MyService.setData"></script> 

最简单的方法来获取任何初始化的使用ng-init目录。

只要把你想要获取初始化数据的ng-init div作用域放在那里

的index.html

 <div class="frame" ng-init="init()"> <div class="bit-1"> <div class="field pr"> <label ng-show="regi_step2.address" class="show-hide ct-1 ng-hide" style="">Country</label> <select class="form-control w-100" ng-model="country" name="country" id="country" ng-options="item.name for item in countries" ng-change="stateChanged()" > </select> <textarea class="form-control w-100" ng-model="regi_step2.address" placeholder="Address" name="address" id="address" ng-required="true" style=""></textarea> </div> </div> </div> 

index.js

 $scope.init=function(){ $http({method:'GET',url:'/countries/countries.json'}).success(function(data){ alert(); $scope.countries = data; }); }; 

注意:如果您的代码多于一个地方,则可以使用此方法。