AngularJS: inicializa el servicio con datos asincrónicos

17 minutos de lectura

AngularJS inicializa el servicio con datos asincronicos
probando 123

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 myData vuelve, obtendré una excepción de puntero nulo. Por lo que puedo decir al leer algunas de las otras preguntas aquí y aquí, tengo algunas opciones, pero ninguna de ellas parece muy clara (tal vez me estoy perdiendo 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 asincrónicos tardan más de lo que tarda en inicializarse todo, obtengo una excepción de puntero nulo cuando llamo doStuff()

Usar objetos de promesa

Esto probablemente funcionaría. El único inconveniente en todos los lugares a los que llamo MyService es que tendré que saber que doStuff() devuelve una promesa y todo el código nos lo tendrá a nosotros. then para interactuar con la promesa. Preferiría esperar hasta que myData esté de vuelta antes de cargar mi aplicación.

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

Var Javascript global
Podría enviar mi JSON directamente a una variable Javascript global:

HTML:

<script type="text/javascript" src="https://stackoverflow.com/questions/16286605/data.js"></script>

datos.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 javascript global que huele mal.

¿Son estas mis únicas opciones? ¿Alguna 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 asincrónicamente recorre el código para extraer datos de un servidor con $httpluego guarde los datos en un servicio, luego inicie una aplicación.

    –Steven Wexler

    1 de julio de 2016 a las 3:13

AngularJS inicializa el servicio con datos asincronicos
joakimbl

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

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

Agregar resolve a la configuración de su ruta:

app.config(function($routeProvider){
  $routeProvider
    .when("https://stackoverflow.com/",{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;
      }
    }})
  }):

No se creará una instancia de su controlador 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=vista previa

  • ¡Muchas gracias por su respuesta! Funcionaría para mí si no tuviera ya un servicio en el mapa de resolución que usa MyService. Actualicé tu plunker con mi situación: plnkr.co/edit/465Cupaf5mtxljCl5NuF?p=preview. ¿Hay alguna forma de hacer que MyOtherService espere a que MyService se inicialice?

    – probando 123

    30 de abril de 2013 a las 13:07


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

    – joakimbl

    30 de abril de 2013 a las 13:32

  • Probé esto y todavía encontré algunos problemas porque tengo directivas y otros controladores (el controlador que uso con $routeProvider está manejando las cosas de navegación primaria y secundaria… es decir, ‘MyOtherService’) que necesitan esperar hasta ‘MyService ‘ esta resuelto. Seguiré intentándolo y actualizaré esto con cualquier éxito que tenga. Solo desearía que hubiera un gancho en angular 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.

    – probando 123

    1 de mayo de 2013 a las 4:39

  • Aquí una pregunta: ¿cómo asignará el resolve propiedad 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 entonces la instanciación del controlador?

    – callmekatootie

    28 mayo 2013 a las 20:57

  • Ummm, ¿qué sucede si no está utilizando el 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 asíncrona, pero tan pronto como tenga más de un controlador y agregue servicios, BOOM es imposible.

    – cableado_en

    13 de marzo de 2014 a las 18:29

1646745141 357 AngularJS inicializa el servicio con datos asincronicos
JBCP

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

(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 autoejecutable para obtener el servicio $http, solicitar la configuración e inyectarla en una constante llamada CONFIG cuando esté disponible.

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

Esta es una ligera mejora con respecto a la solución de Martin, que difería la obtención de la configuración hasta que el documento estuviera listo. Hasta donde yo sé, no hay razón para retrasar la llamada $http por eso.

Examen de la unidad

Nota: Descubrí que esta solución no funciona bien cuando se realizan pruebas unitarias cuando el código está incluido en su app.js expediente. 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.html archivo, por lo que la infraestructura de prueba de unidad Grunt/Karma/Jasmine no lo ve.

  • La regla como ‘no contaminar el alcance global’ debe seguirse solo en la medida en que mejore 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?

    -David Walker

    14 de julio de 2014 a las 0:46


  • 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 corre el riesgo de aplastar otros módulos que no lo necesitan. Por ejemplo, si usó una variable de ‘configuración’ global, existe la posibilidad de que otro código de terceros también esté buscando la misma variable.

    – JBCP

    4 de agosto de 2014 a las 14:42

  • 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 árbitro: tutoriales.jenkov.com/angularjs/… Gracias por esta solución.

    – a2f0

    1 de febrero de 2015 a las 15:42


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

    – IrishDubGuy

    25 de julio de 2015 a las 19:06


1646745142 506 AngularJS inicializa el servicio con datos asincronicos
felipe

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

Utilizando resolve en las 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 $timeout en lugar 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.

ACTUALIZAR 2

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

Ejemplo que carga datos desde el 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');
    }
  }
});

  • deferredBootstrapper es el camino a seguir

    – fatlinesofcode

    15 de abril de 2014 a las 4:48

El caso de “arranque manual” puede obtener acceso a los servicios de Angular creando manualmente un inyector antes del arranque. Este inyector inicial será independiente (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 los servicios principales de Angular, basta con cargar ngMe gusta esto:

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, utilizar el module.constant mecanismo para hacer que los datos estén disponibles para su aplicación:

myApp.constant('myAppConfig', data);

Esta myAppConfig ahora 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, podría simplemente 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 asíncronas 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 utilizar el ng-cloak directiva para evitar que la plantilla no analizada se muestre durante el trabajo. También puede proporcionar algún tipo de indicación de carga en el DOM, al proporcionar algo de HTML que se muestra solo hasta que se inicializa 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>

yo creé un ejemplo completo y funcional de este enfoque en Plunker, cargando la configuración desde un archivo JSON estático como ejemplo.

1646745142 964 AngularJS inicializa el servicio con datos asincronicos
XML

Tuve el mismo problema: me encanta el resolve object, 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 deben 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 funcione?

Use bootstrap manual y una constante angular. Un XHR ingenuo obtiene sus datos y usted arranca angular en su devolución de llamada, que se ocupa de sus problemas asíncronos. En el siguiente ejemplo, ni siquiera necesita crear una variable global. Los datos devueltos existen solo en el ámbito angular como un inyectable, y ni siquiera están presentes dentro de los controladores, servicios, etc. a menos que los inyecte. (Al igual que inyectaría la salida de su resolve objeto en el controlador para obtener una vista enrutada). Si prefiere interactuar con esos datos como un servicio, puede crear un servicio, inyectar los datos y nadie se dará cuenta.

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 NavData constante 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 simple elimina una serie de sutilezas que $http o JQuery 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 poder para su solicitud, cargue una biblioteca externa para ayudarlo. Pero no creo que sea posible acceder a angular $http u otras herramientas en este contexto.

(publicación relacionada con SO)

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("https://stackoverflow.com/",{
          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 renderizará la plantilla ni hará que el controlador esté disponible hasta que se haya llamado 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 cobra vida 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 ámbito del controlador podrán usar los datos que se originaron en MyService.

AngularJS inicializa el servicio con datos asincronicos
probando 123

Así que 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="https://stackoverflow.com/questions/16286605/path/myData.js"></script>

Lado del servidor:

@RequestMapping(value="https://stackoverflow.com/questions/16286605/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);
}

Luego 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 su 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 espera los datos y permite la asignación al alcance. ¡La búsqueda continúa!

    – rocío

    25 de mayo de 2013 a las 18:08

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

    – probando 123

    27 mayo 2013 a las 19:38

¿Ha sido útil esta solución?

Esta web utiliza cookies propias y de terceros para su correcto funcionamiento y para fines analíticos y para mostrarte publicidad relacionada con sus preferencias en base a un perfil elaborado a partir de tus hábitos de navegación. Al hacer clic en el botón Aceptar, acepta el uso de estas tecnologías y el procesamiento de tus datos para estos propósitos. Configurar y más información
Privacidad