web-dev-qa-db-fra.com

AngularJS: initialiser le service avec des données asynchrones

J'ai un service AngularJS que je veux initialiser avec des données asynchrones. Quelque chose comme ça:

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 fonctionnera pas car si quelque chose essaye d'appeler doStuff() avant que myData ne soit récupéré, j'obtiendrai une exception de pointeur nul. D'après ce que j'ai pu lire à la lecture de certaines des questions posées ici et ici j'ai quelques options, mais aucune ne semble très propre (peut-être me manque-t-il quelque chose):

Setup Service avec "run"

Lors de la configuration de mon application, procédez comme suit:

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

Ensuite, 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 que nécessaire pour tout initialiser, je reçois une exception de pointeur nul lorsque j'appelle doStuff().

Utiliser des objets de promesse

Cela fonctionnerait probablement. Le seul inconvénient est que partout où j'appelle MyService, je devrai savoir que doStuff () renvoie une promesse et que tout le code nous devra then pour interagir avec la promesse. Je préférerais simplement attendre le retour de myData 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>

data.js:

var dataForMyService = { 
// myData here
};

Ensuite, il serait disponible lors de l’initialisation de MyService:

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

Cela fonctionnerait aussi, mais alors j'ai une variable javascript globale qui sent mauvais.

Est-ce que ce sont mes seules options? L'une de ces options est-elle meilleure que les autres? Je sais que la question est assez longue, mais je voulais montrer que j’ai essayé d’explorer toutes mes options. Toute orientation serait grandement appréciée. 

458
testing123

Avez-vous jeté un œil à $routeProvider.when('/path',{ resolve:{...} ? Cela peut rendre la promesse un peu plus propre:

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

Ajoutez resolve à votre configuration d'itinéraire:

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

319
joakimbl

Basé sur la solution de Martin Atkins, voici une solution complète, concise et purement angulaire:

(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 à exécution automatique pour obtenir le service $ http, demander la configuration et l'injecter dans une constante appelée CONFIG lorsqu'elle devient disponible. 

Une fois que nous avons terminé, nous attendons que le document soit prêt, puis amorçons l'application Angular. 

Il s’agit là 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. Pour autant que je sache, il n'y a aucune raison de retarder l'appel $ http pour cela. 

Tests unitaires

Remarque: j'ai découvert que cette solution ne fonctionnait pas correctement lors des tests unitaires lorsque le code est inclus dans votre fichier app.js. La raison en est que le code ci-dessus est exécuté immédiatement lorsque le fichier JS est chargé. Cela signifie que la structure de test (Jasmine dans mon cas) n'a pas la possibilité de fournir une implémentation fictive de $http

Ma solution, dont je ne suis pas complètement satisfaite, consistait à déplacer ce code dans notre fichier index.html. L'infrastructure de test unitaire Grunt/Karma/Jasmine ne le voit pas.

86
JBCP

J'ai utilisé une approche similaire à celle décrite par @XMLilley, mais je souhaitais pouvoir utiliser des services AngularJS tels que $http pour charger la configuration et poursuivre l'initialisation sans utiliser d'API de bas niveau ni de jQuery.

Utiliser resolve sur les itinéraires n’était pas non plus une option, car j’avais besoin que les valeurs soient disponibles en tant que constantes au démarrage de mon application, même dans les blocs module.config().

J'ai créé une petite application AngularJS qui charge la configuration, les définit comme des constantes sur l'application réelle et l'amorce.

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

METTRE &AGRAVE; JOUR

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

UPDATE 2

Parce que j'en avais besoin dans plusieurs projets, je viens de publier un module Bower qui prend en charge ceci: https://github.com/philippd/angular-deferred-bootstrap

Exemple qui charge des données à partir du back-end et définit 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
philippd

Le cas de "démarrage manuel" peut accéder aux services Angular en créant manuellement un injecteur avant le démarrage. Cet injecteur initial sera autonome (ne sera attaché à aucun élément) et inclura seulement un sous-ensemble des modules chargés. Si tout ce dont vous avez besoin, ce sont 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 mécanisme module.constant pour rendre les données disponibles pour votre application:

myApp.constant('myAppConfig', data);

Cette myAppConfig peut maintenant être injectée comme n'importe quel autre service, et en particulier, elle 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 l'application.

Bien sûr, étant donné que les opérations asynchrones bloquent ici le démarrage de l'application et donc la compilation/la liaison du modèle, il est recommandé d'utiliser la directive ng-cloak pour empêcher le modèle non analysé de s'afficher pendant le travail. Vous pouvez également fournir une sorte d'indication de chargement dans le DOM, en fournissant du code HTML affiché uniquement jusqu'à l'initialisation d'AngularJS:

<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 exemple complet et fonctionnel de cette approche sur Plunker, chargeant la configuration à partir d'un fichier JSON statique à titre d'exemple.

42
Martin Atkins

J'ai eu le même problème: j'aime l'objet resolve, mais cela ne fonctionne que pour le contenu de ng-view. Et si vous avez des contrôleurs (pour la navigation de niveau supérieur, disons) qui existent en dehors de ng-view et qui doivent être initialisés avec des données avant même que le routage ne commence à se produire? Comment pouvons-nous éviter de nous débrouiller côté serveur juste pour que cela fonctionne?

Utilisez un bootstrap manuel et une constante angulaire. Un naiive XHR vous récupère vos données et vous amorcez angular dans son rappel, qui traite 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 renvoyées n'existent que dans la portée angulaire sous forme d'injectable et ne sont même pas présentes dans les contrôleurs, les services, etc. sauf si vous les injectez. (De la même manière que vous injecteriez la sortie de votre objet resolve dans le contrôleur pour une vue routée.) Si vous préférez ensuite interagir avec ces données en tant que service, vous pouvez créer un service, injecter des données et personne ne sera jamais le 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 installez-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, l’utilisation d’un objet XHR nu supprime un certain nombre de subtilités que $http ou JQuery prendraient en charge pour vous, mais cet exemple fonctionne sans dépendance particulière, du moins pour une simple get. Si vous souhaitez un peu plus de puissance pour votre demande, chargez une bibliothèque externe pour vous aider. Mais je ne pense pas qu'il soit possible d'accéder au $http de angular ou à d'autres outils dans ce contexte. 

(SO poste lié )

14
XML

Ce que vous pouvez faire, c’est dans votre fichier .config car l’application est de créer l’objet de résolution pour la route et dans la fonction, passez $ q (objet de promesse) et le nom du service dont vous dépendez, puis résolvez la promesse dans fonction de rappel pour le $ 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;
          }
      })
}

Angular ne rendra pas le modèle ou rendra le contrôleur disponible tant que la fonction defer.resolve () n'aura pas été appelée. Nous pouvons le faire dans notre service:

UN 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 attribué les données à sa propriété data et que la promesse dans l'objet de résolution de la route a été résolue, notre contrôleur pour la route entre en vie et nous pouvons affecter les données du service à notre objet de contrôleur.

MANETTE

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

Désormais, toutes nos liaisons dans le cadre du contrôleur pourront utiliser les données provenant de MyService.

8
dewd

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

HTML:

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

Du 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 ensuite injecter MyDataRepository où j'en ai besoin:

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

Cela a bien fonctionné pour moi, mais je suis ouvert à tout commentaire si quelqu'un en a un .}

5
testing123

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

2
Slava Fomin II

Vous pouvez utiliser JSONP pour charger de manière asynchrone des données de service . La demande JSONP sera effectuée lors du chargement initial de la page et les résultats seront disponibles avant le démarrage de votre application. De cette façon, vous n'aurez pas à gonfler votre routage avec des résolutions redondantes.

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
hegemon