AngularJS : Initialize service with asynchronous data

I have an AngularJS service that I want to initialize with some asynchronous data. Something like this:

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

Obviously this won't work because if something tries to call doStuff() before myData gets back I will get a null pointer exception. As far as I can tell from reading some of the other questions asked here and here I have a few options, but none of them seem very clean (perhaps I am missing something):

Setup Service with "run"

When setting up my app do this:

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

Then my service would look like this:

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

This works some of the time but if the asynchronous data happens to take longer than it takes for everything to get initialized I get a null pointer exception when I call doStuff()

Use promise objects

This would probably work. The only downside it everywhere I call MyService I will have to know that doStuff() returns a promise and all the code will have to us then to interact with the promise. I would rather just wait until myData is back before loading the my application.

Manual Bootstrap

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

Global Javascript Var I could send my JSON directly to a global Javascript variable:

HTML:

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

data.js:

var dataForMyService = { 
// myData here
};

Then it would be available when initializing MyService :

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

This would work too, but then I have a global javascript variable which smells bad.

Are these my only options? Are one of these options better than the others? I know this is a pretty long question, but I wanted to show that I have tried to explore all my options. Any guidance would greatly be appreciated.


Have you had a look at $routeProvider.when('/path',{ resolve:{...} ? It can make the promise approach a bit cleaner:

Expose a promise in your service:

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

Add resolve to your route config:

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

Your controller won't get instantiated before all dependencies are resolved:

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

I've made an example at plnkr: http://plnkr.co/edit/GKg21XH0RwCMEQGUdZKH?p=preview


Based on Martin Atkins' solution, here is a complete, concise pure-Angular solution:

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

This solution uses a self-executing anonymous function to get the $http service, request the config, and inject it into a constant called CONFIG when it becomes available.

Once completely, we wait until the document is ready and then bootstrap the Angular app.

This is a slight enhancement over Martin's solution, which deferred fetching the config until after the document is ready. As far as I know, there is no reason to delay the $http call for that.

Unit Testing

Note: I have discovered this solution does not work well when unit-testing when the code is included in your app.js file. The reason for this is that the above code runs immediately when the JS file is loaded. This means the test framework (Jasmine in my case) doesn't have a chance to provide a mock implementation of $http .

My solution, which I'm not completely satisfied with, was to move this code to our index.html file, so the Grunt/Karma/Jasmine unit test infrastructure does not see it.


I used a similar approach to the one described by @XMLilley but wanted to have the ability to use AngularJS services like $http to load the configuration and do further initialization without the use of low level APIs or jQuery.

Using resolve on routes was also not an option because I needed the values to be available as constants when my app is started, even in module.config() blocks.

I created a small AngularJS app that loads the config, sets them as constants on the actual app and bootstraps it.

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

See it in action (using $timeout instead of $http ) here: http://plnkr.co/edit/FYznxP3xe8dxzwxs37hi?p=preview

UPDATE

I would recommend to use the approach described below by Martin Atkins and JBCP.

UPDATE 2

Because I needed it in multiple projects, I just released a bower module that takes care of this: https://github.com/philippd/angular-deferred-bootstrap

Example that loads data from the back-end and sets a constant called APP_CONFIG on the AngularJS module:

deferredBootstrapper.bootstrap({
  element: document.body,
  module: 'MyApp',
  resolve: {
    APP_CONFIG: function ($http) {
      return $http.get('/api/demo-config');
    }
  }
});
链接地址: http://www.djcxy.com/p/77560.html

上一篇: 如何在angularjs中包含/注入使用$ scope的函数到控制器中?

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