AngularJS:使用异步数据初始化服务


475

我有一个AngularJS服务,我想使用一些异步数据进行初始化。像这样:

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

显然,这是行不通的,因为如果doStuff()myData返回之前尝试进行调用,我将得到一个空指针异常。据我从阅读这里这里提出的其他一些问题中所知道的,我有一些选择,但是它们似乎都不是很干净(也许我错过了一些东西):

安装服务“运行”

设置我的应用时,请执行以下操作:

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

这有时会起作用,但是如果异步数据发生的时间比初始化所有东西所花费的时间长,则在调用时会出现空指针异常 doStuff()

使用承诺对象

这可能会起作用。我在任何地方调用MyService的唯一缺点是,我必须知道doStuff()返回了promise,所有代码都必须交给我们then交互才能与交互。我宁愿等到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
    });
});

全局Java语言Var 我可以将JSON直接发送到全局Java语言变量:

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

这也可以,但是我有一个全局的javascript变量,它闻起来不好。

这些是我唯一的选择吗?这些选择之一是否比其他选择更好?我知道这是一个很长的问题,但我想表明我已尝试探索所有选择。任何指导将不胜感激。


angular-bootstrap异步浏览代码以使用来从服务器提取数据$http,然后将数据保存在服务中,然后引导应用程序。
史蒂芬·韦克斯勒

Answers:


327

你看过了吗 $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到您的路由配置:

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


1
非常感谢你的回复!如果我在使用MyService的解析图中还没有任何服务,则对我来说将是有效的。我已根据情况更新了您的朋克:plnkr.co/edit/465Cupaf5mtxljCl5NuF?p=preview。有什么办法可以让MyOtherService等待MyService初始化?
testing123 2013年

2
我想我会在MyOtherService中链接诺言-我已经通过链接和一些注释更新了插件,这看起来如何?plnkr.co/edit/Z7dWVNA9P44Q72sLiPjW?p=preview
joakimbl 2013年

3
我尝试了这一点,但仍然遇到了一些问题,因为我有指令和其他控制器(我与$ routeProvider一起使用的控制器正在处理主要的辅助导航内容...即“ MyOtherService”),需要等到“ MyService”解决了。我将继续尝试并以任何成功的方式更新它。我只希望有一个钩子,可以在初始化控制器和指令之前等待数据返回。再次感谢您的帮助。如果我有一个包装了所有东西的主控制器,那就可以了。
test123

43
这里的问题是-您如何将resolve属性分配给未在中提到的控制器$routeProvider。例如,<div ng-controller="IndexCtrl"></div>。此处明确提到了控制器,并且未通过路由加载。在这种情况下,控制器的一个延迟实例将如何呢?
callmekatootie

19
嗯,如果您不使用路由怎么办?这几乎就像在说除非使用路由,否则您不能使用异步数据编写有角度的应用程序。建议将数据获取到应用程序中的方法是异步加载数据,但是一旦拥有多个控制器并投入服务,BOOM就不可能了。
wired_in

88

基于Martin Atkins的解决方案,这是一个完整,简洁的纯角度解决方案:

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

此解决方案使用自执行匿名函数来获取$ http服务,请求配置,并将其注入到名为CONFIG的常量(如果可用)中。

完全完成后,我们将等到文档准备就绪后再引导Angular应用程序。

这是对Martin解决方案的略微增强,该解决方案将获取配置推迟到文档准备好之后才进行。据我所知,没有理由为此延迟$ http调用。

单元测试

注意:我发现当app.js文件中包含代码时,此解决方案在单元测试时效果不佳。原因是上面的代码在加载JS文件后立即运行。这意味着测试框架(在我的例子中为Jasmine)没有机会提供的模拟实现$http

我不完全满意的解决方案是将这段代码移到我们的index.html文件中,因此Grunt / Karma / Jasmine单元测试基础结构看不到它。


1
仅在它们使我们的代码更好(更简单,更可维护,更安全等)的范围内,才应遵循“不污染全局范围”之类的规则。我看不到这种解决方案比仅将数据加载到单个全局变量中更好。我想念什么?
david004 2014年

4
它允许您使用Angular的依赖项注入系统来访问需要它的模块中的'CONFIG'常量,但是您不必冒犯其他不需要的模块的风险。例如,如果您使用全局'config'变量,则其他第三方代码也可能正在寻找相同的变量。
JBCP

1
我是一个有角的新手,以下是一些有关如何在应用程序中解决配置模块依赖关系的说明: gist.github.com/dsulli99/0be3e80db9b21ce7b​​989 ref:tutorials.jenkov.com/angularjs / ...感谢您提供此解决方案。
dps 2015年

7
在下面的其他手动引导程序解决方案之一的注释中提到了该方法,但是作为一个没有发现它的新手,我可以指出,您需要删除html代码中的ng-app指令才能使其正常工作-它使用此手动方法替换了自动引导程序(通过ng-app)。如果您不退出ng-app,则该应用程序可能会实际运行,但您会在控制台中看到各种未知的提供程序错误。
IrishDubGuy 2015年

49

我使用了与@XMLilley描述的方法类似的方法,但希望能够使用AngularJS服务,例如$http加载配置并进行进一步的初始化,而无需使用低级API或jQuery。

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

我创建了一个小的AngularJS应用程序,该应用程序加载配置,将其设置为实际应用程序上的常量,然后对其进行引导。

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

更新

我建议使用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');
    }
  }
});

11
deferredBootstrapper是必经之路
fatlinesofcode 2014年

44

通过在引导程序之前手动创建注射器,“手动引导程序”案例可以访问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现在可以注入就像任何其他服务,尤其是在配置阶段是可用的:

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

或者,对于较小的应用程序,您可以将全局配置直接注入到您的服务中,以牺牲在整个应用程序中传播有关配置格式的知识为代价。

当然,由于此处的异步操作将阻止应用程序的引导,从而阻止模板的编译/链接,因此明智的方法是使用ng-cloak指令防止未解析的模板在工作期间显示。您还可以通过提供一些仅在AngularJS初始化之前显示的HTML来在DOM中提供某种加载指示:

<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上创建这种方法的完整且有效的示例,以从静态JSON文件加载配置为例。


我认为您不需要将$ http.get()推迟到文档准备好之后。
JBCP 2014年

@JBCP是的,您是对的,如果您交换事件,它也可以正常工作,这样我们就不必等到文档准备好直到返回HTTP响应之后,这样做的好处是可以启动HTTP要求更快。仅引导调用需要等待DOM准备就绪。
Martin Atkins 2014年

2
我使用您的方法创建了一个Bower
philipd 2014年

@MartinAtkins,我只是发现您的出色方法不适用于Angular v1.1 +。看起来Angular的早期版本只是在启动应用程序之前才理解“然后”。要看到它在你普拉克与替代角URL code.angularjs.org/1.1.5/angular.min.js
vkelman

16

我遇到了同样的问题:我喜欢这个resolve对象,但这仅适用于ng-view的内容。如果您有ng-view之外的控制器(例如用于顶级导航),并且在路由甚至开始发生之前需要使用数据进行初始化,该怎么办?我们如何避免仅仅为了使之工作而在服务器端搞乱呢?

使用手动引导程序和角度常数。幼稚的XHR可以为您获取数据,并在其回调中引导角度,以处理您的异步问题。在下面的示例中,您甚至不需要创建全局变量。返回的数据仅在角度范围内作为可注入数据存在,除非您将其注入,否则甚至不存在于控制器,服务等内部。(就像将resolve对象的输出注入到路由视图的控制器中一样。)如果此后您希望与该数据作为服务进行交互,则可以创建服务,注入数据,没有人会更明智。 。

例:

//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对象会剥夺$httpJQuery可以处理的许多细节,但是此示例在没有特殊依赖性的情况下(至少对于simple而言)有效get。如果您希望自己的请求有更多功能,请加载外部库以帮助您。但是我认为不可能访问angular的$http在这种情况下工具或其他工具。

SO 相关的帖子


8

您可以在应用程序的.config中为该路由创建resolve对象,并在函数$ q中传递(承诺对象)和您所依赖的服务的名称,然后在服务中$ http的回调函数,如下所示:

路线配置

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

在调用defer.resolve()之前,Angular不会渲染模板或使控制器不可用。我们可以在我们的服务中做到这一点:

服务

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已将数据分配给它的data属性,并且路由解析对象中的promise已经解析,我们的路由控制器就会生效,我们可以将服务中的数据分配给控制器对象。

控制器

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

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


当我有更多时间时,我会试一试。这看起来类似于其他人在ngModules中尝试做的事情。
test123

1
我喜欢这种方法,并且在以前使用过它,但是当我有多个路由时,我目前正在尝试找出一种干净的方法,每个路由可能取决于也可能不取决于预取的数据。有什么想法吗?
ivarni

顺便说一句,我倾向于让每个需要预取数据的服务在初始化时发出请求,并返回一个Promise,然后使用不同路由所需的服务来设置resolve-objects。我只是希望有一个更简单的方法。
ivarni

1
@dewd这就是我的目标,但是如果有某种方式可以说“首先获取所有这些东西,而不管加载了哪条路由”,而不必重复我的解析块,我将更愿意。他们都有依赖的东西。但这不是什么大不了的事,它只会让您感到更加干燥:)
ivarni

2
这是我最终选择的路线,除了我必须创建resolve一个对象,并使其具有函数的功能。因此最终成为resolve:{ dataFetch: function(){ // call function here } }
aron.duby

5

所以我找到了解决方案。我创建了angularJS服务,我们将其称为MyDataRepository,并为其创建了一个模块。然后,我从服务器端控制器提供此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...
}

这对我来说非常有效,但是我愿意接受任何反馈(如果有任何反馈)。}


我喜欢您的模块化方法。我发现$ routeScope可用于请求数据的服务,您可以在$ http.success回调中为其分配数据。但是,对非全局项使用$ routeScope会产生异味,并且应该将数据确实分配给控制器$ scope。不幸的是,我认为您的方法虽然具有创新性,但却不理想(尽管要尊重找到适合您的东西)。我只是确定必须有一个仅客户端的答案,以某种方式等待数据并允许分配作用域。搜索继续!
2013年

如果对某人有用,我最近看到了一些其他方法来查看其他人编写并添加到ngModules网站的模块。当我有更多时间时,我将不得不开始使用其中之一,或者弄清楚他们做了什么并将其添加到我的东西中。
test123


1

您可以使用 JSONP用来异步加载服务数据。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>

-1

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

只需将ng-init div范围放在要获取初始化数据的位置

index.html

<div class="frame" ng-init="init()">
    <div class="bit-1">
      <div class="field p-r">
        <label ng-show="regi_step2.address" class="show-hide c-t-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;
    });
  };

注意:如果您的相同代码不超过一个位置,则可以使用此方法。


不建议您根据以下文档使用ngInit:docs.angularjs.org/api/ng/directive/ngInit
fodma1 2016年
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.