476 votes

Initialiser le service de AngularJS avec des données asynchrones

J'ai un AngularJS service que je veux initialiser avec certains de données asynchrone. 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 de l'appeler doStuff() avant myData obtient en retour je reçois une exception de pointeur null. Aussi loin que je puisse dire à la lecture de certaines des autres questions posées ici et ici j'ai un peu d'options, mais aucune d'entre elles semblent très propre (peut-être que je suis absent quelque chose):

Le programme d'installation du Service avec "exécuter"

Lors de la configuration de mon application ce faire:

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

Puis mon service devrait ressembler à ceci:

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

Cela fonctionne de temps en temps, mais si les données asynchrone arrive à prendre plus de temps qu'il en faut pour tout pour obtenir initialisé j'obtiens une exception de pointeur null lorsque j'appelle doStuff()

Utilisation promesse objets

Ce serait probablement travailler. Le seul inconvénient c'partout j'appelle MyService je vais avoir de savoir que doStuff() renvoie une promesse et tout le code sera à nous, then d'interagir avec la promesse. Je préfère juste attendre jusqu'à ce myData est de retour avant le chargement du ma demande.

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

Mondial Javascript Var Je pourrais envoyer mon JSON directement global à une variable Javascript:

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

Ce serait trop de travail, mais ensuite, j'ai un javascript global variable qui sent mauvais.

Sont mes options? Sont une de ces options mieux que les autres? Je sais que c'est une bonne question, mais je voulais montrer que j'ai essayé d'explorer toutes mes options. De toute orientation serait très apprécié.

327voto

joakimbl Points 9081

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

Exposer 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 de votre parcours 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 pas obtenir instancié avant que toutes les dépendances sont 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

88voto

JBCP Points 2397

Basé sur Martin Atkins solution, voici une complète, concise pur-Angulaire de la 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']);
        });
    }
  );
})();

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

Une fois complètement, nous attendons jusqu'à ce que le document est prêt et ensuite l'amorçage de l'Angulaire de l'app.

C'est une légère amélioration par rapport Martin solution, qui reportés de l'extraction de la configuration jusqu'à ce que après le document est prêt. Autant que je sache, il n'y a aucune raison de retarder l' $http appel pour que.

Les Tests Unitaires

Note: j'ai découvert cette solution ne fonctionne pas bien lorsque l'unité de test lorsque le code est inclus dans votre app.js le fichier. La raison pour cela est que le code ci-dessus s'exécute immédiatement lorsque le fichier JS est chargé. Cela signifie que le framework de test (Jasmin dans mon cas) n'ont pas la possibilité de fournir une maquette de mise en œuvre de l' $http.

Ma solution, que je ne suis pas complètement satisfait, a été de déplacer ce code à notre index.html le fichier, de sorte que le Grunt/Karma/Jasmin de test de l'unité de l'infrastructure ne le voit pas.

49voto

philippd Points 331

J'ai utilisé une approche similaire à celle décrite par @XMLilley mais je voulais avoir la possibilité d'utiliser AngularJS services comme l' $http à la charge de la configuration et de poursuivre l'initialisation sans l'utilisation de bas niveau de l'Api de jQuery.

À l'aide de resolve sur les routes a aussi pas une option, car j'avais besoin de valeurs disponibles en tant que constantes lorsque mon application est démarrée, et même, en module.config() blocs.

J'ai créé une petite application AngularJS qui se charge de la configuration, des ensembles comme des constantes de l'application et de 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']);
});

Le voir en action (à l'aide d' $timeout au lieu de $http) ici: http://plnkr.co/edit/FYznxP3xe8dxzwxs37hi?p=preview

Mise à JOUR

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

Mise à JOUR 2

Parce que j'avais besoin dans de multiples projets, je viens de publier une charmille module qui prend en charge ce: https://github.com/philippd/angular-deferred-bootstrap

Exemple de chargement de données à partir du back-end et définit une constante appelée APP_CONFIG sur le AngularJS module:

deferredBootstrapper.bootstrap({
  element: document.body,
  module: 'MyApp',
  resolve: {
    APP_CONFIG: function ($http) {
      return $http.get('/api/demo-config');
    }
  }
});

44voto

Martin Atkins Points 3034

Le "manuel de bootstrap" cas peuvent accéder aux Angulaire des services par la création manuelle d'un injecteur avant de bootstrap. Ce premier injecteur seront seul (ne pas être attaché à tous les éléments) et inclure uniquement un sous-ensemble des modules qui sont chargés. Si vous avez besoin de base Angulaire des services, il suffit juste de charge 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 l' module.constant mécanisme pour rendre les données disponibles pour votre application:

myApp.constant('myAppConfig', data);

Cette myAppConfig peut maintenant être injecté juste 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 petite application, vous avez juste injecter le global configuration directement dans votre service, au détriment de la diffusion des connaissances sur le format de configuration dans l'application.

Bien sûr, depuis les opérations asynchrones ici va bloquer le démarrage de l'application, et donc bloquer la compilation/liaison du modèle, il est sage d'utiliser l' ng-cloak directive pour empêcher la unparsed modèle d'apparaître pendant le travail. Vous pouvez également fournir une sorte de chargement indication dans les DOM , en fournissant le code HTML qui sera affiché seulement 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, chargement de la configuration à partir d'un statique fichier JSON comme un exemple.

16voto

XMLilley Points 2351

J'ai eu le même problème: j'aime l' resolve objet, mais cela ne fonctionne que pour le contenu de ng-view. Si vous avez des contrôleurs de premier niveau, nav, disons) qui existent en dehors de ng-view et qui a besoin d'être initialisé avec les données avant le routage même commence à se produire? Comment pouvons-nous éviter de coucher sur le côté serveur juste pour faire ce travail?

Manuel de démarrage et angulaire constante. Un naiive XHR vous permet de vous vos données, et vous bootstrap angulaire dans son rappel, qui concerne votre async questions. Dans l'exemple ci-dessous, vous n'avez même pas besoin de créer une variable globale. Les données renvoyées n'existe que dans angulaire portée comme un produit injectable, et n'est même pas présent à l'intérieur de contrôleurs, services, etc. à moins que vous l'injecter. (Comme vous injecter de la sortie de votre resolve objet dans le contrôleur pour une acheminé vue.) Si vous préférez, par la suite, d'interagir avec ces données en tant que service, vous pouvez créer un service, d'injecter des données, et personne ne pourra jamais l'être 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 NavData constante existe. Aller de l'avant et à l'injecter dans un contrôleur ou d'un service:

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

Bien sûr, à l'aide d'un nu XHR objet élimine un certain nombre de subtilités qui $http ou JQuery permettrait de prendre en charge pour vous, mais cet exemple fonctionne avec aucun des dépendances, 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 est possible d'accéder angulaire de l' $http ou d'autres outils dans ce contexte.

(DONC related post)

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X