Question AngularJS: initialiser le service avec des données asynchrones


J'ai un service AngularJS que je veux initialiser avec certaines 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();
        }
    };
});

Evidemment cela ne fonctionnera pas car si quelque chose essaie d'appeler doStuff() avant myData reviens j'obtiendrai une exception de pointeur nul. Pour autant que je puisse dire en lisant certaines des autres questions posées ici et ici J'ai quelques options, mais aucune ne semble très propre (il me manque peut-être quelque chose):

Service d'installation avec "Exécuter"

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

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 une partie du temps mais si les données asynchrones prennent plus de temps que nécessaire pour que tout soit initialisé, j'obtiens une exception de pointeur nul lorsque j'appelle doStuff()

Utiliser des objets promis

Cela fonctionnerait probablement. Le seul inconvénient que j'appelle partout MyService Je vais devoir savoir que doStuff () renvoie une promesse et que tout le code nous sera nécessaire then d'interagir avec la promesse. Je préfère juste 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>

data.js:

var dataForMyService = { 
// myData here
};

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

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

Cela fonctionnerait aussi, mais 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 c'est une question assez longue, mais je voulais montrer que j'ai essayé d'explorer toutes mes options. Toute orientation serait grandement appréciée.


445
2018-04-29 19:25


origine


Réponses:


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

Exposez une promesse dans 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 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;
      }
    }})
  }):

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
2018-04-29 21:23



Sur la base de la solution de Martin Atkins, voici une solution pure et angulaire complète et concise:

(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 configuration et l'injecter dans une constante appelée CONFIG lorsqu'elle sera disponible.

Une fois complètement, nous attendons que le document soit prêt et que nous démarrions l'application Angular.

C'est une légère amélioration par rapport à la solution de Martin, qui a différé l'extraction de la config 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 app.js fichier. La raison en est que le code ci-dessus s'exécute immédiatement lorsque le fichier JS est chargé. Cela signifie que le cadre de test (Jasmine dans mon cas) n’a aucune chance de fournir une implémentation simulée de $http.

Ma solution, dont je ne suis pas complètement satisfait, était de déplacer ce code dans notre index.htmlfichier, donc l'infrastructure de test de l'unité Grunt / Karma / Jasmine ne le voit pas.


86
2018-01-17 15:04



J'ai utilisé une approche similaire à celle décrite par @XMLilley mais je voulais avoir la possibilité d'utiliser les services AngularJS comme $http charger la configuration et effectuer une initialisation supplémentaire sans utiliser des API de bas niveau ou jQuery.

En utilisant resolve sur les routes n'était pas non plus une option car j'avais besoin que les valeurs soient disponibles en tant que constantes lorsque mon application est démarrée, même dans module.config() des blocs.

J'ai créé une petite application AngularJS qui charge la configuration, les définit comme des constantes sur l'application 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 À 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 de calcul qui s'en occupe: 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
2017-11-09 01:27



Le boîtier "bootstrap manuel" peut accéder aux services Angular en créant manuellement un injecteur avant bootstrap. Cet injecteur initial sera autonome (ne sera attaché à aucun élément) et n'inclura qu'un sous-ensemble des modules chargés. Si tout ce dont vous avez besoin est de services angulaires de base, il suffit de charger ng, comme ça:

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 n'importe quel 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 asynchrones bloqueront le bootstrap de l’application, bloquant ainsi la compilation / liaison du template, il est conseillé d’utiliser le ng-cloak directive pour empêcher le modèle non analysé d'apparaître pendant le travail. Vous pouvez également fournir une sorte d'indication de chargement dans le DOM, en fournissant du code HTML qui s'affiche uniquement jusqu'à ce que AngularJS soit initialisé:

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


42
2017-12-08 01:13



J'ai eu le même problème: j'aime le resolve objet, 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 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? Comment pouvons-nous éviter de déverser côté serveur pour que cela fonctionne?

Utilisez le bootstrap manuel et une constante angulaire. Un XHR natif vous apporte vos données, et vous amorcez un rappel angulaire dans son rappel, 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 que dans une étendue angulaire en tant qu'injectable, et ne sont même pas présentes à l'intérieur des contrôleurs, services, etc. sauf si vous les injectez. (Tout comme vous injecterez la sortie de votre resolveobjet 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, injecter 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 NavData constante 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, l’utilisation d’un objet XHR dénudé élimine un certain nombre de $http ou JQuery prendrait soin de vous, mais cet exemple fonctionne sans dépendance particulière, au moins pour un simple get. Si vous voulez 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 à angular $http ou d'autres outils dans ce contexte.

(ALORS Article similaire)


13
2017-08-20 13:33



Ce que vous pouvez faire est dans votre .config pour l'application est de créer l'objet de résolution pour l'itinéraire et dans la fonction passer dans $ q (objet promesse) et le nom du service dont vous dépendez, et résoudre la promesse dans le fonction de rappel pour le $ http dans le service comme ceci:

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 ni ne mettra le contrôleur à disposition tant que defer.resolve () n'a pas été appelé. 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 les données assignées à sa propriété data, et que la promesse dans l'objet de résolution d'itinéraire a été résolue, notre contrôleur pour la route entre en action, et nous pouvons assigner les données du service à notre objet contrôleur.

MANETTE

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

Maintenant, toute notre liaison dans le cadre du contrôleur sera en mesure d'utiliser les données qui proviennent de MyService.


8
2018-05-25 21:41



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 puis servir ce fichier javascript à partir de 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 alors 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 à toute rétroaction si quelqu'un en a. }


5
2018-05-02 14:15



En outre, vous pouvez utiliser les techniques suivantes pour approvisionner 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 run bloquer par exemple.


2
2017-11-25 15:21



Vous pouvez utiliser JSONP pour charger les données de service de manière asynchrone. La requête 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
2017-07-06 13:55



Le moyen le plus simple d’extraire tout répertoire d’initialisation est ng-init.

Il suffit de mettre la portée ng-init div où vous voulez récupérer les données init

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

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


-1
2018-03-16 13:59