AngularJS: inicialize o serviço com dados assíncronos


475

Eu tenho um serviço AngularJS que desejo inicializar com alguns dados assíncronos. 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 myDatavoltar, receberei uma exceção de ponteiro nulo. Pelo que sei ao ler algumas das outras perguntas feitas aqui e aqui , tenho algumas opções, mas nenhuma delas parece muito limpa (talvez esteja faltando alguma coisa):

Serviço de instalação com "executar"

Ao configurar meu aplicativo, faça o seguinte:

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 algumas vezes, mas se os dados assíncronos levarem mais tempo do que o necessário para que tudo seja inicializado, recebo uma exceção de ponteiro nulo quando ligo doStuff()

Use objetos de promessa

Provavelmente isso funcionaria. A única desvantagem em todos os lugares que eu chamo de MyService, terei que saber que doStuff () retorna uma promessa e todo o código precisará de nós thenpara interagir com a promessa. Prefiro esperar até que myData esteja de volta 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 de Javascript Var eu poderia enviar meu JSON diretamente para uma variável global Javascript:

HTML:

<script type="text/javascript" src="data.js"></script>

data.js:

var dataForMyService = { 
// myData here
};

Em seguida, ele estará disponível ao inicializar MyService:

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

Isso funcionaria também, mas então 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? Sei que essa é uma pergunta bastante longa, mas queria mostrar que tentei explorar todas as minhas opções. Qualquer orientação seria muito apreciada.


angular - o bootstrap percorre o código de forma assíncrona para extrair dados de um servidor $http, salvar dados em um serviço e inicializar um aplicativo.
Steven Wexler

Respostas:


327

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

Exponha uma promessa 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:'<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;
      }
    }})
  }):

Seu controlador não será instanciado antes que todas as dependências sejam resolvidas:

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

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


1
Muito obrigado pela sua resposta! Funcionaria para mim se eu ainda não tivesse um serviço no mapa de resolução que usa o MyService. Atualizei seu desentupidor com minha situação: plnkr.co/edit/465Cupaf5mtxljCl5NuF?p=preview . Existe alguma maneira de fazer o MyOtherService aguardar a inicialização do MyService?
usar o seguinte comando

2
Acho que encadearia as promessas no MyOtherService - atualizei o afunilador com encadeamento e alguns comentários - como isso fica? plnkr.co/edit/Z7dWVNA9P44Q72sLiPjW?p=preview
joakimbl

3
Eu tentei isso e ainda encontrei alguns problemas porque tenho diretivas e outros controladores (o controlador que eu uso com $ routeProvider está lidando com um material de navegação primário e secundário ... que é 'MyOtherService') que precisa esperar até 'MyService ' está resolvido. Vou continuar tentando e atualizá-lo com qualquer sucesso que tenha. Eu só queria que houvesse um gancho angular onde eu pudesse esperar o retorno dos dados antes de inicializar meus controladores e diretivas. Obrigado novamente por sua ajuda. Se eu tivesse um controlador principal que envolvesse tudo, isso teria funcionado.
Test123

43
Uma pergunta aqui - como você atribuirá a resolvepropriedade a um controlador que não é mencionado em $routeProvider. Por exemplo <div ng-controller="IndexCtrl"></div>,. Aqui, o controlador é mencionado explicitamente e não é carregado via roteamento. Nesse caso, como atrasaria a instanciação do controlador?
Callmekatootie 28/05

19
Ummm, e se você não estiver usando o roteamento? É quase como dizer que você não pode escrever um aplicativo angular com dados assíncronos, a menos que você use o roteamento. A maneira recomendada de inserir dados em um aplicativo é carregá-los de forma assíncrona, mas assim que você tiver mais de um controlador e fornecer serviços, o BOOM será impossível.
11133 wired_in

88

Com base na solução de Martin Atkins, aqui está uma solução completa e concisa de ângulo puro:

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

Essa solução usa uma função anônima de execução automática para obter o serviço $ http, solicitar a configuração e injetar em uma constante chamada CONFIG quando estiver disponível.

Uma vez completamente, aguardamos até que o documento esteja pronto e inicializamos o aplicativo Angular.

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

Teste de Unidade

Nota: Descobri que esta solução não funciona bem ao testar a unidade quando o código está incluído no seu app.jsarquivo. 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 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 o nosso index.htmlarquivo, para que a infraestrutura de teste da unidade Grunt / Karma / Jasmine não o veja.


1
Regras como 'não poluem o escopo global' devem ser seguidas apenas na medida em que melhorem nosso código (menos complexo, mais sustentável, mais seguro, etc.). Não vejo como essa solução é melhor do que simplesmente carregar os dados em uma única variável global. o que estou perdendo?
David004

4
Ele permite que você use o sistema de injeção de dependência da Angular para acessar a constante 'CONFIG' nos módulos que precisam, mas você não corre o risco de derrotar outros módulos que não precisam. Por exemplo, se você usou uma variável global 'config', é possível que outro código de terceiros também esteja procurando a mesma variável.
JBCP 04/08/2018

1
Sou um novato angular, aqui estão algumas notas sobre como resolvi a dependência do módulo de configuração no meu aplicativo: gist.github.com/dsulli99/0be3e80db9b21ce7b989 ref: tutorials.jenkov.com/angularjs/… Obrigado por esta solução.
dps 01/02

7
Ele é mencionado em um comentário em uma das outras soluções manuais de bootstrap abaixo, mas como um novato angular que não o localizou, posso apenas salientar que você precisa excluir a diretiva ng-app no ​​código html para que funcione corretamente - está substituindo a inicialização automática (via ng-app) por este método manual. Se você não remover o aplicativo ng, o aplicativo poderá realmente funcionar, mas você verá vários erros de fornecedores desconhecidos no console.
IrishDubGuy

49

Usei uma abordagem semelhante à descrita por @XMLilley, mas queria ter a capacidade de usar os serviços AngularJS, como $httpcarregar a configuração e fazer uma inicialização adicional sem o uso de APIs de baixo nível ou jQuery.

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

Criei um pequeno aplicativo AngularJS que carrega a configuração, os define 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-o em ação (usando em $timeoutvez 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

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

Exemplo que carrega dados do back-end 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');
    }
  }
});


44

O caso "inicialização manual" pode obter acesso aos serviços Angular criando manualmente um injetor antes da inicialização. Esse injetor inicial permanecerá sozinho (não será anexado a nenhum elemento) e incluirá apenas um subconjunto dos módulos carregados. Se tudo o que você precisa são serviços Angular básicos, basta carregar ng, desta forma:

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 module.constantmecanismo para disponibilizar dados para o seu aplicativo:

myApp.constant('myAppConfig', data);

myAppConfigAgora isso 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ê pode simplesmente injetar a configuração global diretamente no seu serviço, à custa de espalhar conhecimento sobre o formato de configuração em todo o aplicativo.

Obviamente, como as operações assíncronas aqui bloquearão a inicialização do aplicativo e, assim, a compilação / vinculação do modelo, é aconselhável usar a ng-cloakdiretiva para impedir que o modelo não analisado apareça durante o trabalho. Você também pode fornecer algum tipo de indicação de carregamento no DOM, fornecendo algum HTML que é mostrado apenas até a inicialização do 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>

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


Eu não acho que você precise adiar o $ http.get () até depois que o documento estiver pronto.
JBCP

@JBCP sim, você está certo de que funciona tão bem se você trocar os eventos para que não esperemos que o documento fique pronto até depois que a resposta HTTP for retornada, com a vantagem de poder iniciar o HTTP solicitar mais rapidamente. Somente a chamada de autoinicialização precisa esperar até que o DOM esteja pronto.
Martin Atkins

2
Eu criei um módulo pavilhão com a sua abordagem: github.com/philippd/angular-deferred-bootstrap
philippd

@ MartinAtkins, acabei de descobrir que sua ótima abordagem não funciona com o Angular v1.1 +. Parece que as versões anteriores do Angular simplesmente não entendem "então" até que o aplicativo seja inicializado. Para vê-lo em seu Plunk, substitua o URL angular por code.angularjs.org/1.1.5/angular.min.js
vkelman

16

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

Use a inicialização manual e uma constante angular . Um XHR ingênuo obtém seus dados e você inicializa angular no retorno de chamada, o que lida com os problemas de async. 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. (Por mais que você injete a saída do seu resolveobjeto no controlador para obter uma visão roteada.) Se você preferir interagir posteriormente com esses dados como um serviço, poderá criar um serviço, injetar os dados e ninguém será 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 NavDataconstante existe. Vá em frente e injete-o em um controlador ou serviço:

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

Obviamente, o uso de um objeto XHR simples remove uma série de detalhes que o $httpJQuery cuidaria de você, mas este exemplo funciona sem dependências especiais, pelo menos para uma simples get. Se você deseja um pouco mais de energia para sua solicitação, carregue uma biblioteca externa para ajudá-lo. Mas não acho que seja possível acessar $httpferramentas angulares ou outras neste contexto.

( Postagem relacionada ao SO )


8

O que você pode fazer é no seu .config para o aplicativo, criar o objeto de resolução para a rota e, na função, passar em $ q (objeto da promessa) e o nome do serviço do qual você está dependendo, e resolver a promessa no função 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 nem disponibilizará o controlador até que defer.resolve () seja chamado. 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 possui os dados atribuídos a sua propriedade de dados e a promessa no objeto 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 objeto de controlador.

CONTROLADOR

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

Agora toda a nossa ligação no escopo do controlador poderá usar os dados originados no MyService.


Vou dar uma chance a isso quando tiver mais tempo. Isso é semelhante ao que os outros estavam tentando fazer no ngModules.
testing123 27/05

1
Eu gosto dessa abordagem e a usei antes, mas atualmente estou tentando descobrir como fazer isso de maneira limpa quando tenho várias rotas, cada uma das quais pode ou não depender de dados pré-buscados. Alguma idéia sobre isso?
Ivarni 29/05

Aliás, estou inclinado a fazer com que cada serviço que requer dados pré-buscados faça a solicitação quando inicializado e retorne uma promessa e, em seguida, configure objetos de resolução com os serviços exigidos pelas diferentes rotas. Eu só queria que houvesse uma maneira menos detalhada.
Ivarni

1
@dewd É isso que eu estava buscando, mas eu preferiria muito se houvesse alguma maneira de dizer "buscar todas essas coisas primeiro, independentemente da rota carregada" sem precisar repetir meus bloqueios de resolução. Todos eles têm algo de que dependem. Mas não é um grande negócio, que seria apenas sentir um pouco mais DRY :)
ivarni

2
esse é o caminho que acabei seguindo, exceto que tive que criar resolveum objeto com uma propriedade sendo a função. por isso acabou sendoresolve:{ dataFetch: function(){ // call function here } }
aron.duby

5

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

HTML:

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

Lado do servidor:

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

Posso injetar MyDataRepository sempre que 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. }


Eu gosto da sua abordagem modular. Descobri que $ routeScope está disponível para o serviço que solicita dados e você pode atribuir dados a ele no retorno de chamada $ http.success. No entanto, o uso de $ routeScope para itens não globais cria um odor e os dados devem realmente ser atribuídos ao escopo $ do controlador. Infelizmente, acho que sua abordagem, embora inovadora, não é ideal (embora respeite encontrar algo que funcione para você). Tenho certeza de que deve haver uma resposta apenas do lado do cliente que, de alguma forma, aguarde os dados e permita que a atribuição alcance. A busca continua!
dewd

Caso isso seja útil para alguém, recentemente vi algumas abordagens diferentes para os módulos que outras pessoas escreveram e adicionaram ao site ngModules. Quando tiver mais tempo, terei que começar a usar um desses ou descobrir o que eles fizeram e adicioná-lo às minhas coisas.
testing123


1

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

Você html ficaria assim:

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

-1

A maneira mais fácil de buscar qualquer diretório inicializado é usar o ng-init.

Basta colocar o escopo ng-init div onde você deseja buscar dados 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;
    });
  };

NOTA: você pode usar essa metodologia se não tiver o mesmo código em mais de um local.


Não é aconselhável usar ngInit de acordo com os documentos: docs.angularjs.org/api/ng/directive/ngInit
fodma1
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.