AngularJS: inicializa el servicio con datos asincrónicos


475

Tengo un servicio AngularJS que quiero inicializar con algunos datos asincrónicos. Algo como esto:

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, esto no funcionará porque si algo intenta llamar doStuff()antes de que myDatavuelva, obtendré una excepción de puntero nulo. Por lo que puedo decir al leer algunas de las otras preguntas formuladas aquí y aquí , tengo algunas opciones, pero ninguna de ellas parece muy limpia (tal vez me falta algo):

Servicio de configuración con "ejecutar"

Al configurar mi aplicación, haga esto:

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

Entonces mi servicio se vería así:

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

Esto funciona algunas veces, pero si los datos asíncronos tardan más de lo que se tarda en inicializar todo, obtengo una excepción de puntero nulo cuando llamo doStuff()

Usa objetos de promesa

Esto probablemente funcionaría. El único inconveniente en todas partes que llamo MyService tendrá que saber que doStuff () devuelve una promesa y todo el código nos tendrá thenque interactuar con la promesa. Prefiero esperar hasta que myData regrese antes de cargar mi aplicación.

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 Podría enviar mi JSON directamente a una variable global de Javascript:

HTML:

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

data.js:

var dataForMyService = { 
// myData here
};

Entonces estaría disponible al inicializar MyService:

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

Esto también funcionaría, pero luego tengo una variable global de JavaScript que huele mal.

¿Son estas mis únicas opciones? ¿Una de estas opciones es mejor que las otras? Sé que esta es una pregunta bastante larga, pero quería mostrar que he tratado de explorar todas mis opciones. Cualquier orientación sería muy apreciada.


angular: bootstrap recorre el código de forma asincrónica para extraer datos de un servidor $http, luego guardar datos en un servicio y luego arrancar una aplicación.
Steven Wexler

Respuestas:


327

¿Has echado un vistazo $routeProvider.when('/path',{ resolve:{...}? Puede hacer que el enfoque de la promesa sea un poco más limpio:

Exponer una promesa en su servicio:

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

Agregue resolvea su configuración de ruta:

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

Su controlador no se instanciará antes de que se resuelvan todas las dependencias:

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

He hecho un ejemplo en plnkr: http://plnkr.co/edit/GKg21XH0RwCMEQGUdZKH?p=preview


1
¡Muchas gracias por su respuesta! Me funcionaría si aún no tuviera un servicio en el mapa de resolución que utiliza MyService. Actualicé su plunker con mi situación: plnkr.co/edit/465Cupaf5mtxljCl5NuF?p=preview . ¿Hay alguna forma de hacer que MyOtherService espere a que MyService se inicialice?
testing123

2
Supongo que encadenaría las promesas en MyOtherService. He actualizado el plunker con encadenamiento y algunos comentarios. ¿Cómo se ve esto? plnkr.co/edit/Z7dWVNA9P44Q72sLiPjW?p=preview
joakimbl

3
Probé esto y todavía me encontré con algunos problemas porque tengo directivas y otros controladores (el controlador que uso con $ routeProvider está manejando un material de navegación primario y secundario ... que es 'MyOtherService') que necesitan esperar hasta 'MyService ' esta resuelto. Seguiré intentando y actualizando esto con cualquier éxito que tenga. Solo desearía que hubiera un gancho en ángulo donde pudiera esperar a que regresen los datos antes de inicializar mis controladores y directivas. De nuevo, gracias por tu ayuda. Si tuviera un controlador principal que envolviera todo, esto habría funcionado.
testing123

43
Una pregunta aquí: ¿cómo va a asignar la resolvepropiedad a un controlador que no se menciona en $routeProvider? Por ejemplo, <div ng-controller="IndexCtrl"></div>. Aquí, el controlador se menciona explícitamente y no se carga a través del enrutamiento. En tal caso, ¿cómo se retrasaría la creación de instancias del controlador?
callmekatootie

19
Ummm, ¿qué pasa si no estás usando enrutamiento? Esto es casi como decir que no puede escribir una aplicación angular con datos asincrónicos a menos que use enrutamiento. La forma recomendada de obtener datos en una aplicación es cargarla de forma asincrónica, pero tan pronto como tenga más de un controlador y entregue servicios, BOOM es imposible.
wired_in

88

Basado en la solución de Martin Atkins, aquí hay una solución completa, concisa, puramente angular:

(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 solución utiliza una función anónima de ejecución automática para obtener el servicio $ http, solicitar la configuración e inyectarlo en una constante llamada CONFIG cuando esté disponible.

Una vez completamente, esperamos hasta que el documento esté listo y luego arranquemos la aplicación Angular.

Esta es una ligera mejora sobre la solución de Martin, que aplazó la obtención de la configuración hasta que el documento esté listo. Hasta donde yo sé, no hay razón para retrasar la llamada $ http para eso.

Examen de la unidad

Nota: He descubierto que esta solución no funciona bien cuando se realizan pruebas unitarias cuando el código está incluido en su app.js archivo. La razón de esto es que el código anterior se ejecuta inmediatamente cuando se carga el archivo JS. Esto significa que el marco de prueba (Jasmine en mi caso) no tiene la oportunidad de proporcionar una implementación simulada de$http .

Mi solución, con la que no estoy completamente satisfecho, fue mover este código a nuestro index.htmlarchivo, para que la infraestructura de prueba de la unidad Grunt / Karma / Jasmine no lo vea.


1
Se deben seguir reglas como 'no contaminar el alcance global' solo en la medida en que mejoren nuestro código (menos complejo, más fácil de mantener, más seguro, etc.). No puedo ver cómo esta solución es mejor que simplemente cargar los datos en una sola variable global. ¿Qué me estoy perdiendo?
david004

44
Le permite usar el sistema de inyección de dependencia de Angular para acceder a la constante 'CONFIG' en los módulos que lo necesitan, pero no se arriesga a bloquear otros módulos que no lo necesitan. Por ejemplo, si usó una variable global 'config', existe la posibilidad de que otro código de terceros también esté buscando la misma variable.
JBCP

1
Soy un novato angular, aquí hay algunas notas sobre cómo resolví la dependencia del módulo de configuración en mi aplicación: gist.github.com/dsulli99/0be3e80db9b21ce7b989 ref: tutorials.jenkov.com/angularjs/… Gracias por esta solución.
dps 01 de

77
Se menciona en un comentario en una de las otras soluciones manuales de arranque a continuación, pero como un novato angular que no lo vio, puedo señalar que debe eliminar su directiva ng-app en su código html para que esto funcione correctamente - está reemplazando el bootstrap automático (a través de ng-app) con este método manual. Si no saca la aplicación ng, la aplicación en realidad puede funcionar, pero verá varios errores de proveedores desconocidos en la consola.
IrishDubGuy

49

Utilicé un enfoque similar al descrito por @XMLilley, pero quería tener la capacidad de usar los servicios de AngularJS, como $httpcargar la configuración y realizar una inicialización adicional sin el uso de API de bajo nivel o jQuery.

El uso resolveen rutas tampoco era una opción porque necesitaba que los valores estuvieran disponibles como constantes cuando se inicia mi aplicación, incluso en module.config()bloques.

Creé una pequeña aplicación AngularJS que carga la configuración, las establece como constantes en la aplicación real y la inicia.

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

Véalo en acción (usando en $timeoutlugar de $http) aquí: http://plnkr.co/edit/FYznxP3xe8dxzwxs37hi?p=preview

ACTUALIZAR

Recomendaría utilizar el enfoque descrito a continuación por Martin Atkins y JBCP.

ACTUALIZACIÓN 2

Debido a que lo necesitaba en múltiples proyectos, acabo de lanzar un módulo Bower que se encarga de esto: https://github.com/philippd/angular-deferred-bootstrap

Ejemplo que carga datos del back-end y establece una constante llamada APP_CONFIG en el módulo AngularJS:

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

11
deferredBootstrapper es el camino a seguir
fatlinesofcode

44

El caso "bootstrap manual" puede obtener acceso a servicios angulares creando manualmente un inyector antes del bootstrap. Este inyector inicial estará solo (no se adjuntará a ningún elemento) e incluirá solo un subconjunto de los módulos que se cargan. Si todo lo que necesita son servicios centrales de Angular, es suficiente con cargarlos ng, así:

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

Puede, por ejemplo, usar el module.constantmecanismo para hacer que los datos estén disponibles para su aplicación:

myApp.constant('myAppConfig', data);

Esto myAppConfigahora se puede inyectar como cualquier otro servicio, y en particular está disponible durante la fase de configuración:

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

o, para una aplicación más pequeña, puede inyectar la configuración global directamente en su servicio, a expensas de difundir el conocimiento sobre el formato de configuración en toda la aplicación.

Por supuesto, dado que las operaciones asincrónicas aquí bloquearán el arranque de la aplicación y, por lo tanto, bloquearán la compilación / vinculación de la plantilla, es aconsejable usar la ng-cloakdirectiva para evitar que la plantilla no analizada aparezca durante el trabajo. También podría proporcionar algún tipo de indicación de carga en el DOM, proporcionando algún HTML que se muestre solo hasta que AngularJS se inicialice:

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

He creado un ejemplo completo, trabajando de este enfoque en Plunker, cargar la configuración de un archivo JSON estática como ejemplo.


No creo que necesite diferir $ http.get () hasta que el documento esté listo.
JBCP

@JBCP sí, tiene razón en que funciona igual de bien si intercambia los eventos para que no esperemos a que el documento esté listo hasta que se devuelva la respuesta HTTP, con la ventaja de que posiblemente pueda comenzar el HTTP Solicitar más rápido. Solo la llamada de arranque debe esperar hasta que el DOM esté listo.
Martin Atkins


@ MartinAtkins, acabo de descubrir que su gran enfoque no funciona con Angular v1.1 +. Parece que las primeras versiones de Angular simplemente no entienden "entonces" hasta que la aplicación se inicia. Para verlo en su Plunk, reemplace la URL angular con code.angularjs.org/1.1.5/angular.min.js
vkelman

16

Tuve el mismo problema: me encanta el resolveobjeto, pero eso solo funciona para el contenido de ng-view. ¿Qué sucede si tiene controladores (para navegación de nivel superior, digamos) que existen fuera de ng-view y que necesitan inicializarse con datos antes de que el enrutamiento comience a suceder? ¿Cómo evitamos perder el tiempo en el lado del servidor solo para que eso funcione?

Use bootstrap manual y una constante angular . Un ingenuo XHR le proporciona sus datos y arranca angularmente en su devolución de llamada, que se ocupa de sus problemas asincrónicos. En el ejemplo a continuación, ni siquiera necesita crear una variable global. Los datos devueltos existen solo en alcance angular como inyectables, y ni siquiera están presentes dentro de los controladores, servicios, etc., a menos que los inyecte. (Tanto como inyectaría la salida de suresolve objeto en el controlador para una vista enrutada). Si prefiere interactuar posteriormente con esos datos como un servicio, puede crear un servicio, inyectar los datos, y nadie será más sabio. .

Ejemplo:

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

Ahora, tu NavDataconstante existe. Continúe e inyéctelo en un controlador o servicio:

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

Por supuesto, el uso de un objeto XHR desnudo elimina algunas de las sutilezas que $httpJQuery se encargaría de usted, pero este ejemplo funciona sin dependencias especiales, al menos para un simple get. Si desea un poco más de potencia para su solicitud, cargue una biblioteca externa para ayudarlo. Pero no creo que sea posible acceder a $httpherramientas angulares u otras en este contexto.

(Publicación relacionada con SO )


8

Lo que puede hacer en su .config para la aplicación es crear el objeto de resolución para la ruta y en la función pasar $ q (objeto de promesa) y el nombre del servicio del que depende, y resolver la promesa en el función de devolución de llamada para $ http en el servicio de esta manera:

CONFIGURACIÓN DE RUTA

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 no representará la plantilla ni hará que el controlador esté disponible hasta que se llame a defer.resolve (). Podemos hacer eso en nuestro servicio:

SERVICIO

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

Ahora que MyService tiene los datos asignados a su propiedad de datos, y la promesa en el objeto de resolución de ruta se ha resuelto, nuestro controlador para la ruta comienza a funcionar, y podemos asignar los datos del servicio a nuestro objeto de controlador.

CONTROLADOR

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

Ahora todos nuestros enlaces en el alcance del controlador podrán usar los datos que se originaron en MyService.


Le daré una oportunidad cuando tenga más tiempo. Esto se parece a lo que otros intentaban hacer en ngModules.
testing123

1
Me gusta este enfoque y lo he usado antes, pero actualmente estoy tratando de resolver cómo hacerlo de manera limpia cuando tengo varias rutas, cada una de las cuales puede o no depender de datos previamente capturados. ¿Alguna idea sobre eso?
ivarni

Por cierto, me estoy inclinando para que cada servicio que requiere datos captados previamente haga la solicitud cuando se inicializa y devuelva una promesa y luego configure objetos de resolución con los servicios requeridos por las diferentes rutas. Solo desearía que hubiera una forma menos detallada.
ivarni

1
@dewd A eso era a lo que aspiraba, pero preferiría que hubiera alguna forma de decir "buscar todo esto primero, independientemente de la ruta cargada" sin tener que repetir mis bloques de resolución. Todos tienen algo de lo que dependen. Pero no es un gran problema, se sentiría un poco más SECO :)
ivarni

2
Esta es la ruta que terminé tomando, excepto que tuve que hacer resolveun objeto con una propiedad como función. así que terminó siendoresolve:{ dataFetch: function(){ // call function here } }
aron.duby

5

Entonces encontré una solución. Creé un servicio angularJS, lo llamaremos MyDataRepository y creé un módulo para él. Luego sirvo este archivo javascript desde mi controlador del lado del servidor:

HTML:

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

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

Entonces puedo inyectar MyDataRepository donde lo necesite:

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

Esto funcionó muy bien para mí, pero estoy abierto a cualquier comentario si alguien tiene alguno. }


Me gusta tu enfoque modular. Descubrí que $ routeScope está disponible para el servicio que solicita datos y puede asignarle datos en la devolución de llamada $ http.success. Sin embargo, el uso de $ routeScope para elementos no globales crea un olor y los datos realmente deberían asignarse al controlador $ scope. Desafortunadamente, creo que su enfoque, aunque innovador, no es ideal (sin embargo, respeto por encontrar algo que funcione para usted). Estoy seguro de que debe haber una respuesta solo del lado del cliente que de alguna manera espere los datos y permita la asignación al alcance. ¡La búsqueda continúa!
rocío

En caso de que sea útil para alguien, recientemente vi algunos enfoques diferentes que miran módulos que otras personas han escrito y agregado al sitio web ngModules. Cuando tenga más tiempo tendré que comenzar a usar uno de esos, o averiguar qué hicieron y agregarlo a mis cosas.
testing123


1

Puedes usar JSONP para cargar asincrónicamente los datos del servicio. La solicitud JSONP se realizará durante la carga inicial de la página y los resultados estarán disponibles antes de que comience su aplicación. De esta manera, no tendrá que aumentar su enrutamiento con resoluciones redundantes.

Tu HTML se vería así:

<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

La forma más fácil de obtener cualquier inicialización utiliza el directorio ng-init.

Simplemente coloque el alcance div ng-init donde desee obtener datos de inicio

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: puede usar esta metodología si no tiene el mismo código en más de un lugar.


No se recomienda usar ngInit de acuerdo con los documentos: docs.angularjs.org/api/ng/directive/ngInit
fodma1
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.