AngularJS: initialiser le service avec des données asynchrones

j'ai un service AngularJS que je veux initialiser avec quelques données asynchrones. Quelque chose comme ceci:

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

évidemment, cela ne marchera pas parce que si quelque chose essaie d'appeler doStuff() avant que myData ne revienne, j'obtiendrai une exception de pointeur nul. Pour autant que je puisse dire de la lecture de certaines des autres questions posées ici et ici j'ai quelques options, mais aucun d'entre eux semblent très propre (peut-être que je manque quelque chose):

le programme d'Installation du Service avec "exécuter"

lors de la mise en place de mon application faire ceci:

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

alors mon service ressemblerait à ceci:

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

cela fonctionne parfois, mais si les données asynchrones prennent plus de temps qu'il n'en faut pour que tout soit initialisé, j'obtiens une exception de pointeur nul lorsque j'appelle doStuff()

Utilisation promesse objets

ça devrait marcher. Le seul inconvénient c'est que partout où j'appelle MyService je vais devoir savoir que doStuff() renvoie une promesse et tout le code nous aura then pour interagir avec la promesse. Je préfère attendre que myData soit de retour avant de charger mon application.

Bootstrap Manuel

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 Je pourrais envoyer mon JSON directement à une variable Javascript globale:

HTML:

<script type="text/javascript" src="data.js"></script>
"1519120920 des données".js:

var dataForMyService = { 
// myData here
};

alors il serait disponible lors de l'initialisation de MyService :

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

ça marcherait aussi, mais j'ai une variable javascript globale qui sent mauvais.

sont-ils Mes seuls options? L'une de ces options est-elle meilleure que l'autre? Je sais que c'est une bonne question, mais je voulais montrer que j'ai essayé d'explorer toutes mes options. Toute orientation serait grandement appréciée.

452
demandé sur Community 2013-04-29 23:25:07

10 réponses

avez-vous eu un oeil à $routeProvider.when('/path',{ resolve:{...} ? Il peut rendre l'approche de la promesse un peu plus propre:

dévoiler une promesse à votre 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();
      }
    };
});

ajouter resolve à votre config route:

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

votre contrôleur ne sera pas instancié avant que toutes les dépendances soient résolues:

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

j'ai fait un exemple à plnkr: http://plnkr.co/edit/GKg21XH0RwCMEQGUdZKH?p=preview

317
répondu joakimbl 2014-06-27 10:25:57

basé sur la solution de Martin Atkins, voici une solution complète et concise à L'angle pur:

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

cette solution utilise une fonction anonyme auto-exécutable pour obtenir le service $http, demander la config, et l'injecter dans une constante appelée CONFIG quand il devient disponible.

une fois complètement, nous attendons jusqu'à ce que le document soit prêt et ensuite démarrer L'application angulaire.

il s'agit d'une légère amélioration par rapport à La solution de Martin, qui a différé la récupération de la configuration jusqu'à ce que le document soit prêt. Autant que je sache, il n'y a aucune raison de retarder l'appel http $pour cela.

Tests Unitaires

Note: j'ai découvert que cette solution ne fonctionne pas bien lors des tests unitaires lorsque le code est inclus dans votre fichier app.js . La raison en est que le code ci-dessus s'exécute immédiatement lorsque le fichier JS est chargé. Cela signifie que le test framework (Jasmine dans mon cas) n'a pas la chance de fournir une implémentation simulée de $http .

ma solution, dont je ne suis pas entièrement satisfait, a été de déplacer ce code dans notre fichier index.html , de sorte que L'infrastructure de test de l'unité Grunt/Karma/Jasmine ne le voit pas.

86
répondu JBCP 2014-03-07 17:47:33

j'ai utilisé une approche similaire à celle décrite par @XMLilley mais je voulais avoir la possibilité d'utiliser les services AngularJS comme $http pour charger la configuration et faire une initialisation plus poussée sans utiliser D'API de bas niveau ou jQuery.

utiliser resolve sur les routes n'était pas non plus une option car j'avais besoin que les valeurs soient disponibles comme constantes lorsque mon application est lancée, même dans les blocs module.config() .

j'ai créé un petit AngularJS l'application qui charge la configuration, les définit comme des constantes sur l'application réelle et les bootstraps.

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

voir en action (en utilisant $timeout au lieu de $http ) ici: http://plnkr.co/edit/FYznxP3xe8dxzwxs37hi?p=preview

mise à JOUR

je recommande d'utiliser l'approche décrite ci-dessous par Martin Atkins et JBCP.

mise à jour 2

parce que j'en avais besoin dans plusieurs projets, je viens de sortir un module bower qui s'occupe de ça: https://github.com/philippd/angular-deferred-bootstrap

exemple qui charge des données à partir de la fin et fixe une constante appelée APP_CONFIG sur le module AngularJS:

deferredBootstrapper.bootstrap({
  element: document.body,
  module: 'MyApp',
  resolve: {
    APP_CONFIG: function ($http) {
      return $http.get('/api/demo-config');
    }
  }
});
48
répondu philippd 2014-03-18 07:32:06

le cas "manuel bootstrap" peut accéder aux services angulaires en créant manuellement un injecteur avant bootstrap. Cet injecteur initial sera autonome (ne pas être attaché à des éléments) et ne comprendra qu'un sous-ensemble des modules qui sont chargés. Si tout ce que vous avez besoin est des services angulaires de base, il suffit de charger ng , comme ceci:

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

Vous pouvez, par exemple, utiliser le module.constant mécanisme pour rendre les données disponibles pour votre application:

myApp.constant('myAppConfig', data);

ce myAppConfig peut maintenant être injecté comme tout autre service, et en particulier il est disponible pendant la phase de configuration:

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

ou, pour une application plus petite, vous pouvez simplement injecter la configuration globale directement dans votre service, au détriment de la diffusion des connaissances sur le format de configuration dans toute l'application.

bien sûr, puisque les opérations async ici vont bloquer le bootstrap de l'application, et donc bloquer la compilation/liaison du template, il est sage d'utiliser la directive ng-cloak pour empêcher le template sans équivalent d'apparaître pendant le travail. Vous pouvez également fournir une sorte d'indication de chargement dans le DOM, en fournissant un HTML qui n'est affiché que jusqu'à ce que AngularJS initialise:

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

j'ai créé un travail terminé, exemple de cette approche sur Plunker, le chargement de la configuration à partir d'un fichier JSON statique à titre d'exemple.

42
répondu Martin Atkins 2013-12-08 01:13:26

j'ai eu le même problème: j'aime l'objet resolve , mais cela ne fonctionne que pour le contenu de ng-view. Que se passe-t-il si vous avez des contrôleurs (pour la navigation de haut niveau, par exemple) qui existent en dehors de la vue ng et qui doivent être initialisés avec des données avant même que le routage ne commence à se produire? Comment éviter de tourner autour du serveur juste pour que ça marche?

utiliser bootstrap manuel et une constante angulaire . Une XHR naiive vous donne votre data, et vous bootstrap angular dans son callback, qui traite de vos problèmes asynchrones. Dans l'exemple ci-dessous, vous n'avez même pas besoin de créer une variable globale. Les données retournées n'existent qu'en vue angulaire comme un injectable, et n'est même pas présent à l'intérieur des contrôleurs, des services, etc. à moins que vous l'injecter. (Tout comme vous injecteriez la sortie de votre objet resolve dans le contrôleur pour une vue routée.) Si vous préférez par la suite interagir avec ces données en tant que service, vous pouvez créer un service, injectez les données, et personne ne sera jamais plus sage.

exemple:

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

maintenant, votre constante NavData existe. Allez-y et injectez-le dans un contrôleur ou un service:

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

bien sûr, en utilisant un objet XHR nu enlève un certain nombre des subtilités que $http ou JQuery prendraient soin de vous, mais cet exemple fonctionne sans dépendances spéciales, au moins pour un simple get . Si vous voulez un peu plus de puissance pour votre demande, charger une bibliothèque externe pour vous aider. Mais je ne pense pas qu'il soit possible d'accéder à angular $http ou d'autres outils dans ce contexte.

(DONC poste )

13
répondu XML 2017-05-23 12:02:51

Ce que vous pouvez faire dans votre .config de l'application, c'est de créer de la détermination de l'objet pour la route et dans la fonction passe en $q (promesse de l'objet) et le nom du service que vous êtes en fonction, et de résoudre la promesse dans la fonction de rappel pour l' $http, dans le service comme suit:

ROUTE CONFIG

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

angulaire ne rend pas le modèle ou de rendre le contrôleur disponible jusqu'à déférer.resolve() a été appelé. Nous pouvons le faire dans notre service:

SERVICE

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

maintenant que MyService a les données attribuées à sa propriété de données, et que la promesse dans l'objet route resolve a été résolue, notre contrôleur pour la route entre en action, et nous pouvons attribuer les données du service à notre objet contrôleur.

contrôleur

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

maintenant toute notre reliure dans le domaine du contrôleur sera en mesure d'utiliser les données qui originaire de MyService.

8
répondu dewd 2013-05-25 21:52:13

alors j'ai trouvé une solution. J'ai créé un service angularJS, nous l'appellerons MyDataRepository et j'ai créé un module pour lui. Je sers ensuite ce fichier javascript à partir de mon contrôleur côté serveur:

HTML:

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

Côté Serveur:

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

je peux alors injecter MyDataRepository où que j'en aie besoin:

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

cela a fonctionné grand pour moi, mais je suis ouvert à tout commentaires si quelqu'un a des. }

5
répondu testing123 2013-05-02 14:15:20

aussi, vous pouvez utiliser les techniques suivantes pour fournir votre service globalement, avant que les contrôleurs réels soient exécutés: https://stackoverflow.com/a/27050497/1056679 . Il suffit de résoudre vos données globalement et de les transmettre à votre service dans le bloc run par exemple.

2
répondu Slava Fomin II 2017-05-23 10:31:11

vous pouvez utiliser JSONP pour charger de manière asynchrone les données de service. La requête JSONP sera faite pendant le chargement de la page initiale et les résultats seront disponibles avant le début de votre application. De cette façon, vous n'aurez pas à le gonfler votre routage redondant résout.

Vous html ressemblerait à ceci:

<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>
0
répondu hegemon 2016-07-06 13:55:13

la façon la plus facile de récupérer n'importe quelle initialise en utilisant le répertoire ng-init.

il suffit de mettre ng-init div scope où vous voulez récupérer des données init

de l'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>

de l'index.js

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

NOTE: vous pouvez utiliser cette méthodologie si vous n'avez pas le même code plus d'un endroit.

-1
répondu Roni 2016-03-16 13:59:46