AngularJS: inicializa o serviço com dados asynchronouss

Eu tenho um serviço AngularJS que quero inicializar com alguns dados asynchronouss. Algo assim:

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

Obviamente, isso não funcionará, porque se algo tentar chamar doStuff() antes de myData retornar, obterei uma exceção de ponteiro nulo. Tanto quanto eu posso dizer de ler algumas das outras perguntas feitas aqui e aqui eu tenho algumas opções, mas nenhuma delas parece muito limpa (talvez eu esteja faltando alguma coisa):

Serviço de Configuração com “run”

Ao configurar meu aplicativo, faça isso:

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

Então meu serviço ficaria assim:

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

Isso funciona em parte do tempo, mas se os dados asynchronouss doStuff() mais do que demora para que tudo seja inicializado, recebo uma exceção de ponteiro nulo quando eu chamo doStuff()

Use objects prometidos

Isso provavelmente funcionaria. A única desvantagem é que em todo lugar eu chamo de MyService eu vou ter que saber que doStuff () retorna uma promise e todo o código nos terá then para interagir com a promise. Eu prefiro apenas esperar até myData voltar antes de carregar o meu aplicativo.

Bootstrap Manual

 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 Eu poderia enviar meu JSON diretamente para uma variável Javascript global:

HTML:

  

data.js:

 var dataForMyService = { // myData here }; 

Então estaria disponível ao inicializar o MyService :

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

Isso também funcionaria, mas eu tenho uma variável javascript global que cheira mal.

Essas são minhas únicas opções? Uma dessas opções é melhor que as outras? Eu sei que esta é uma pergunta muito longa, mas eu queria mostrar que tentei explorar todas as minhas opções. Qualquer orientação seria muito apreciada.

Você já deu uma olhada no $routeProvider.when('/path',{ resolve:{...} ? Isso pode tornar a promise um pouco mais limpa:

Exponha uma promise em seu serviço:

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

Adicione resolve à sua configuração de rota:

 app.config(function($routeProvider){ $routeProvider .when('/',{controller:'MainCtrl', template:'
From MyService:
{{data | json}}

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

Seu controlador não será instanciado antes que todas as dependencies sejam resolvidas:

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

Eu fiz um exemplo em plnkr: http://plnkr.co/edit/GKg21XH0RwCMEQGUdZKH?p=preview

Com base na solução de Martin Atkins, aqui está uma solução completa, concisa e angular pura:

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

Esta solução usa uma function anônima auto-executável para obter o serviço $ http, solicitar a configuração e injetá-lo em uma constante chamada CONFIG quando estiver disponível.

Uma vez completamente, esperamos até que o documento esteja pronto e então inicializemos o aplicativo Angular.

Este é um pequeno aprimoramento da solução de Martin, que adiou a busca da configuração até que o documento esteja pronto. Tanto quanto eu sei, não há razão para atrasar a chamada $ http para isso.

Testes Unitários

Nota: Descobri que essa solução não funciona bem quando o teste de unidade é feito quando o código é incluído no arquivo app.js A razão para isso é que o código acima é executado imediatamente quando o arquivo JS é carregado. Isso significa que a estrutura de teste (Jasmine no meu caso) não tem a chance de fornecer uma implementação simulada de $http .

Minha solução, com a qual não estou completamente satisfeito, foi mover esse código para nosso arquivo index.html , portanto a infraestrutura de teste de unidade Grunt / Karma / Jasmine não o vê.

Eu usei uma abordagem semelhante à descrita pelo @XMLilley, mas queria ter a capacidade de usar serviços AngularJS como $http para carregar a configuração e fazer boot adicional sem o uso de APIs de baixo nível ou jQuery.

Usar resolve em rotas também não era uma opção porque eu precisava que os valores estivessem disponíveis como constantes quando meu aplicativo é iniciado, mesmo em blocos module.config() .

Eu criei um pequeno aplicativo AngularJS que carrega a configuração, define-os como constantes no aplicativo real e o inicializa.

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

Veja em ação (usando $timeout vez de $http ) aqui: http://plnkr.co/edit/FYznxP3xe8dxzwxs37hi?p=preview

ATUALIZAR

Eu recomendaria usar a abordagem descrita abaixo por Martin Atkins e JBCP.

ATUALIZAÇÃO 2

Porque eu precisava em vários projetos, acabei de lançar um módulo de bower que cuida disso: https://github.com/philippd/angular-deferred-bootstrap

Exemplo que carrega dados do backend e define uma constante chamada APP_CONFIG no módulo AngularJS:

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

O caso “bootstrap manual” pode obter access aos serviços Angular criando manualmente um injetor antes do bootstrap. Esse injetor inicial será independente (não será anexado a nenhum elemento) e includeá apenas um subconjunto dos módulos carregados. Se tudo o que você precisa é de serviços básicos do Angular, basta carregar o ng , assim:

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

Você pode, por exemplo, usar o mecanismo module.constant para disponibilizar dados ao seu aplicativo:

 myApp.constant('myAppConfig', data); 

Este myAppConfig agora pode ser injetado como qualquer outro serviço e, em particular, está disponível durante a fase de configuração:

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

ou, para um aplicativo menor, você poderia simplesmente injetar a configuração global diretamente em seu serviço, às custas da disseminação de conhecimento sobre o formato de configuração em todo o aplicativo.

Naturalmente, como as operações assíncronas aqui bloquearão o bootstrap do aplicativo e bloquearão a compilation / vinculação do modelo, é aconselhável usar a diretiva ng-cloak para evitar que o modelo não analisado seja exibido durante o trabalho. Você também pode fornecer algum tipo de indicação de carregamento no DOM, fornecendo algum HTML que seja mostrado apenas até que o AngularJS inicialize:

 

Loading the app.....

Done loading the app!

Eu criei um exemplo completo e funcional dessa abordagem no Plunker, carregando a configuração de um arquivo JSON estático como exemplo.

Eu tive o mesmo problema: eu amo o object resolve , mas isso só funciona para o conteúdo do ng-view. E se você tiver controladores (para o nível superior de navegação, digamos) que existam fora do ng-view e que precisem ser inicializados com dados antes que o roteamento comece a acontecer? Como evitamos nos mexer no lado do servidor apenas para que isso funcione?

Use bootstrap manual e uma constante angular . Um XHR ingênuo obtém seus dados e você inicializa o angular em seu retorno de chamada, que lida com seus problemas asynchronouss. No exemplo abaixo, você nem precisa criar uma variável global. Os dados retornados existem apenas no escopo angular como injetáveis, e nem estão presentes dentro de controladores, serviços, etc., a menos que você os injete. (Assim como você iria injetar a saída do seu object de resolve no controlador para uma visão roteada.) Se você preferir depois interagir com esses dados como um serviço, você pode criar um serviço, injetar os dados e ninguém nunca será o Mais sábio.

Exemplo:

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

Agora, sua constante NavData existe. Vá em frente e injete em um controlador ou serviço:

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

É claro que usar um object XHR simples elimina uma série de sutilezas que o $http ou o JQuery cuidariam de você, mas este exemplo funciona sem dependencies especiais, pelo menos para uma get simples. Se você quiser um pouco mais de energia para sua solicitação, carregue uma biblioteca externa para ajudá-lo. Mas eu não acho que é possível acessar o $http do Angular ou outras ferramentas neste contexto.

( Post relacionado SO)

O que você pode fazer é no seu .config para o app é criar o object resolve para a rota e na function passar em $ q (object promise) e o nome do serviço que você está dependendo, e resolver a promise no function de retorno de chamada para o $ http no serviço da seguinte forma:

CONFIGURAÇÃO DE ROTA

 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 não renderizará o modelo ou disponibilizará o controlador até que defer.resolve () tenha sido chamado. Nós podemos fazer isso em nosso serviço:

SERVIÇO

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

Agora que o MyService tem os dados atribuídos à sua propriedade de dados e a promise no object de resolução de rota foi resolvida, nosso controlador para a rota entra em ação e podemos atribuir os dados do serviço ao nosso object de controlador.

CONTROLADOR

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

Agora toda a nossa binding no escopo do controlador será capaz de usar os dados que se originaram do MyService.

Então eu encontrei uma solução. Eu criei um serviço angularJS, vamos chamá-lo de MyDataRepository e criei um módulo para ele. Eu então sirvo este arquivo javascript do meu controlador do lado do servidor:

HTML:

  

Lado do servidor:

 @RequestMapping(value="path/myData.js", method=RequestMethod.GET) public ResponseEntity getMyDataRepositoryJS() { // Populate data that I need into a Map Map myData = new HashMap(); ... // 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(myJS , responseHeaders, HttpStatus.OK); } 

Eu posso então injetar MyDataRepository onde eu precisar:

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

Isso funcionou muito bem para mim, mas estou aberto a qualquer feedback se alguém tiver algum. }

Além disso, você pode usar as seguintes técnicas para provisionar seu serviço globalmente, antes que os controladores reais sejam executados: https://stackoverflow.com/a/27050497/1056679 . Basta resolver seus dados globalmente e, em seguida, passá-lo para o seu serviço no bloco de run , por exemplo.

Você pode usar o JSONP para carregar de forma assíncrona os dados do serviço. A solicitação JSONP será feita durante o carregamento inicial da página e os resultados estarão disponíveis antes do início do aplicativo. Desta forma, você não terá que inchar seu roteamento com resoluções redundantes.

Você html ficaria assim:

    

A maneira mais fácil de buscar qualquer boot usa o diretório ng-init.

Basta colocar ng-init div scope onde você deseja buscar os dados do init

index.html

 

index.js

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

OBSERVAÇÃO: você pode usar essa metodologia se não tiver o mesmo código mais que um lugar.

Intereting Posts