javascript - promesas - AngularJS: Inicializar el servicio con datos asíncronos.
promesas angular 4 (10)
¿Ha $routeProvider.when(''/path'',{ resolve:{...}
un vistazo a $routeProvider.when(''/path'',{ resolve:{...}
? Puede hacer que la promesa se vuelva un poco más limpia:
Expone una promesa a tu 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 resolve
a 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 creará una instancia 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
Tengo un servicio AngularJS que quiero inicializar con algunos datos asíncronos. 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 a doStuff()
antes de que regrese myData
, obtendré una excepción de puntero nulo. Por lo que puedo decir, al leer algunas de las otras preguntas formuladas here y here , tengo algunas opciones, pero ninguna de ellas parece muy limpia (tal vez me esté perdiendo algo):
Servicio de configuración con "run"
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 parte del tiempo, pero si los datos asíncronos toman más tiempo del necesario para que todo se inicialice, obtengo una excepción de puntero nulo cuando llamo a doStuff()
Usa objetos de promesa
Esto probablemente funcionaría. El único inconveniente en todos los sitios a los que llamo MyService. Tendré que saber que doStuff () devuelve una promesa y que todo el código tendrá que interactuar con la promesa. Preferiría simplemente esperar hasta que myData esté de vuelta antes de cargar mi aplicación.
Bota 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 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 demostrar que he tratado de explorar todas mis opciones. Cualquier orientación sería muy apreciada.
Además, puede usar las siguientes técnicas para aprovisionar su servicio globalmente, antes de que se ejecuten los controladores reales: https://.com/a/27050497/1056679 . Simplemente resuelva sus datos globalmente y luego páselos a su servicio en bloque de run
, por ejemplo.
Así que encontré una solución. Creé un servicio angularJS, lo llamaremos MyDataRepository y creé un módulo para ello. 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);
}
Luego puedo inyectar MyDataRepository donde sea que 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. }
Basada en la solución de Martin Atkins, aquí hay una solución completa, concisa y pura en ángulo:
(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 completo, esperamos hasta que el documento esté listo y luego reinicie la aplicación Angular.
Esta es una leve mejora con respecto a la solución de Martin, que aplazó la búsqueda de la configuración hasta que el documento esté listo. Que yo sepa, 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 realiza una prueba unitaria cuando el código está incluido en su archivo app.js
El motivo 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 archivo index.html
, por lo que la infraestructura de prueba de la unidad Grunt / Karma / Jasmine no lo ve.
El caso "manual bootstrap" puede obtener acceso a los servicios Angular creando manualmente un inyector antes de 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 angulares centrales, es suficiente con solo cargar ng
, de esta manera:
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 mecanismo module.constant
para hacer que los datos estén disponibles para su aplicación:
myApp.constant(''myAppConfig'', data);
Este 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, simplemente 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 asíncronas aquí bloquearán el arranque de la aplicación y, por lo tanto, bloquearán la compilación / enlace de la plantilla, es aconsejable usar la directiva ng-cloak
cloak para evitar que la plantilla sin analizar aparezca durante el trabajo. También puede proporcionar algún tipo de indicación de carga en el DOM, proporcionando algún HTML que se muestre solo hasta que se inicialice 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>
Creé un ejemplo completo y funcional de este enfoque en Plunker, cargando la configuración desde un archivo JSON estático como ejemplo.
La forma más fácil de recuperar cualquier directorio de inicio ng-init de uso.
Simplemente coloque el ámbito ng-init div 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 utilizar esta metodología si no tiene el mismo código más de un lugar.
Lo que puede hacer es en su .config para la aplicación crear el objeto de resolución para la ruta y en la función pasar en $ q (objeto de promesa) y el nombre del servicio en el que depende, y resolver la promesa en el Función de devolución de llamada para el $ http en el servicio como tal:
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 procesará 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 controlador.
CONTROLADOR
app.controller(''homeCtrl'',function($scope,MyService){
$scope.servicedata = MyService.data;
});
Ahora todos nuestros enlaces en el alcance del controlador podrán utilizar los datos que se originaron en MyService.
Puede utilizar JSONP
para cargar datos de servicio de forma asíncrona. La solicitud JSONP se realizará durante la carga inicial de la página y los resultados estarán disponibles antes de que se inicie la aplicación. De esta manera, no tendrá que inflar su ruta con resoluciones redundantes.
Usted 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>
Tuve el mismo problema: amo el objeto de resolve
, pero eso solo funciona para el contenido de ng-view. ¿Qué sucede si tiene controladores (para la 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 ocurrir? ¿Cómo podemos evitar estar en el lado del servidor solo para hacer que funcione?
Utilice bootstrap manual y una constante angular . Un XHR naiive obtiene sus datos, y usted inicia de forma 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 se inyecte. (De la misma forma en que inyectaría la salida de su objeto de resolve
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á el único. 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, su constante NavData
existe. Adelante e inyectarlo 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 algunas de las sutilezas que $http
o JQuery se encargarían de usted, pero este ejemplo funciona sin dependencias especiales, al menos para una simple get
. Si desea un poco más de energía para su solicitud, cargue una biblioteca externa para ayudarlo. Pero no creo que sea posible acceder a $http
u otras herramientas de angular en este contexto.
(Mensaje relacionado tan )
Utilicé un enfoque similar al descrito por @XMLilley, pero quería tener la capacidad de usar los servicios de AngularJS como $http
para cargar la configuración y realizar una mayor inicialización sin el uso de API de bajo nivel o jQuery.
El uso de resolve
en rutas tampoco era una opción porque necesitaba que los valores estuvieran disponibles como constantes cuando se inicia mi aplicación, incluso en los bloques module.config()
.
Creé una pequeña aplicación de AngularJS que carga la configuración, las configura como constantes en la aplicación real y la ejecuta.
// 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
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.
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'');
}
}
});