11. Comunicación con el servidor

Guardaremos todos los ejercicios que hagamos en la carpeta server

Aplica el tag server a la versión que quieres que se corrija.

Los ejercicios tienen una puntuación total de un punto, repartido equitativamente entre todos ellos.

Lo normal en una aplicación web es que, tarde o temprano, haya que comunicarse con el servidor para traernos algún tipo de dato, o bien para persistirlo. Es más, existen muchas aplicaciones que únicamente hacen CRUD, con lo que la comunicación con el servidor se convierte en algo esencial.

AngularJS dispone de una serie de APIs para comunicarse con cualquier backend a realizando peticiones XMLHttpRequest (XHR), o bien peticiones JSONP a través del servicio $http. Además, existe un servicio llamado $resource, especializado en la comunicación con interfaces RESTful.

JSONP, o "JSON with padding", es una técinca de comunicación que usan los programas escritos en JavaScript y que corren en un navegador web. Con JSONP, podemos realizar una petición de datos a un servidor que se encuentra en otro dominio, cosa habitualmente prohibida en un navegador web debido a la same-origin policy.

Dado que muchos navegadores no tienen penalización same-origin en las etiquetas script, lo qu ese hace es traerse la respuesta del servidor, envuelta en una llamada a una función.

Para que JSONP funcione, el servidor al que se realizan las peticiones debe saber que tiene que devolver los resultados formateados en JSONP. Para ello, normamente se genera una URL con un parámetro llamado callback=funcion_de_callback (ej: http://jsonplaceholder.typicode.com/users/1?callback=processUser)

11.1. El servicio $http

El servicio $http consiste en una API de propósito general para realizar peticiones XHR y JSONP. Es una API bastante sólida y sencilla de usar.

El servicio $http ofrece una serie de funciones que reciben como parámetros una URL y un objeto de configuración, para generar una petición HTTP. Devuelve una promesa de resultados con dos métodos: success y error.

Los métodos son equivalentes a los que podríamos hacer en una petición HTTP.

Para hacer las pruebas haremos uso de los servicios situados en JSONPlaceholder, que permite hacer uso del servicio $http sobre sus servidores, ya que tiene habilitado el soporte para CORS

11.1.1. $http.get

Realiza una petición GET, para obtener datos.

Parámetros:

  • url: URL destino

  • config: objeto de configuración opcional. A destacar el atributo params, que contiene un mapa de los parámetros a pasar.

El siguiente ejemplo pide el detalle de un usuario:

1 2 3 4 5 6 7 8 9 10 11 12 13
angular .module('httpModule', []) .controller('MainCtrl', function($scope, $http){ $http.get( 'http://jsonplaceholder.typicode.com/posts',{ params: {id:1} } ) .success(function(data){ $scope.resultdata = data; }) .error(function(data){ alert('Se ha producido un error') }); });
1 2 3 4 5 6 7
<div ng-app="httpModule" ng-controller="MainCtrl"> <h1>Resultado</h1> <pre> {{ resultdata[0] | json }} </pre> <hr /> </div>
EJERCICIO
Genera un pequeño programa, similar al del ejemplo, que realice una petición GET y devuelva el listado de comentarios para el post con ID 1.

11.1.2. POST

Realiza una petición POST, para dar de alta algún dato en el servidor.

Parámetros:

  • url: URL destino

  • data: datos a enviar.

  • config: objeto de configuración opcional.

El siguiente ejemplo se encarga de dar de alta un usuario:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
angular .module('httpModule', []) .controller('MainCtrl', function($scope, $http){ $http.post( 'http://jsonplaceholder.typicode.com/users', { "name": "Winchester McFly", "username": "wmcfly", "email": "wmcfly@ua.es", "address": { "street": "Calle del atún 22", }, "phone": "666 112233", "website": "http://winchester-mcfly.com/" } ) .success(function(data){ $scope.id = data.id; }) .error(function(data){ alert('Se ha producido un error') }); });
1 2 3 4 5 6 7
<div ng-app="httpModule" ng-controller="MainCtrl"> <h1>Resultado</h1> <pre ng-show="id"> Se ha dado de alta el usuario, con id: {{ id }} </pre> <hr /> </div>

Podemos ver cómo falla si hacemos una petición POST a /users/1

EJERCICIO
Genera un pequeño programa, similar al del ejemplo, que realice una petición POST realice el alta de una imagen.

11.1.3. PUT

Realiza una petición PUT, para actualizar algún elemento en el servidor.

Parámetros:

  • url: URL destino

  • data: datos a enviar

  • config: objeto de configuración opcional.

[http://codepen.io/alexsuch/pen/FvsuH]El siguiente ejemplo se encarga de actualizar el usuario con id = 1.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
angular .module('httpModule', []) .controller('MainCtrl', function($scope, $http){ $http.put( 'http://jsonplaceholder.typicode.com/users/1', { "name": "Winchester McFly", "username": "wmcfly", "email": "wmcfly@ua.es", "address": { "street": "Calle del atún 22", }, "phone": "666 112233", "website": "http://winchester-mcfly.com/" } ) .success(function(data){ $scope.data = data; }) .error(function(data){ alert('Se ha producido un error') }); });
1 2 3 4 5 6 7 8
<div ng-app="httpModule" ng-controller="MainCtrl"> <h1>Resultado</h1> <pre ng-show="data"> Se ha actualizado el usuario, sus nuevos datos son: {{ data | json }} </pre> <hr /> </div>
EJERCICIO
Genera un pequeño programa, similar al del ejemplo, que realice una petición PUT para actualizar el título del POST con id=1.

11.1.4. DELETE

Realiza una petición DELETE, para solicitar el borrado de algún elemento en el servidor.

Parámetros:

  • url: URL destino

  • config: objeto de configuración opcional.

En el siguiente ejemplo, eliminaremos un usuario.

1 2 3 4 5 6 7 8 9 10 11 12
angular .module('httpModule', []) .controller('MainCtrl', function($scope, $http){ $http.delete( 'http://jsonplaceholder.typicode.com/users/1') .success(function(data){ alert('Se ha eliminado el usuario con éxito') }) .error(function(data){ alert('Se ha producido un error') }); });
1 2 3 4
<div ng-app="httpModule" ng-controller="MainCtrl"> <h1>Resultado</h1> <hr /> </div>
EJERCICIO
Genera un pequeño programa, similar al del ejemplo, que contenga un botón y, al presionarlo, realice una petición DELETE para eliminar el POST con id=1.

11.1.5. JSONP

Realiza una petición JSONP.

Parámetros:

  • url: URL destino. El nombre del callback debe ser, obligatoriamente, JSON_CALLBACK

  • config: objeto de configuración opcional.

En este ejemplo haremos uso del servicio $http.jsonp para obtener los datos de un post.

1 2 3 4 5 6 7 8 9 10 11
angular .module('httpModule', []) .controller('MainCtrl', function($scope, $http){ $http.jsonp( 'http://jsonplaceholder.typicode.com/posts/1?callback=JSON_CALLBACK') .success(function(data){ $scope.data = data; }) .error(function(data){ alert('Se ha producido un error') }); });
1 2 3 4 5 6 7
<div ng-app="httpModule" ng-controller="MainCtrl"> <h1>Resultado</h1> <pre> {{ data | json }} </pre> <hr /> </div>
EJERCICIO
Genera un pequeño programa, similar al del ejemplo, que contenga un botón y, al presionarlo, realice una petición JSONP para obtener los datos del usuario con id=1.

11.2. Integración con servicios RESTful: el servicio $resource

Como hemos visto en los ejemplos anteriores, el uso habitual de los servicios RESTful es para exponer operaciones CRUD, haciéndolas accesibles a través de una URL que acepta diferentes métodos HTTP.

El servicio $http nos da la posibilidad de interactuar con este tipo de servicios de manera sencilla. Sin embargo, disponemos de otro servicio, $resource, que nos permite hacer lo mismo eliminando además el código redundante.

El servicio $resource se distribuye en un módulo separado del core de AngularJS llamado ngResource. Es por ello que tendremos que descargarnos su código fuente y declarar una dependencia con este módulo donde lo vayamos a utilizar.

Para probarlo, seguiremos haciendo uso de los servicios de ejemplo de jsonplaceholder.

En primer lugar, crearemos un resource para la colección de usuarios del servicio:

1
var User = $resource('http://jsonplaceholder.typicode.com/users/:id', {id:'@id'}});

A partir de esta URL, el servicio $resource creará par nosotros una serie de métodos para interactuar con el servicio RESTful.

Si nos centramos en la sintaxis de la declaración, vemos que recibe dos parámetros:

El primero es obligatorio, y consiste en una URL que puede estar parametrizada. Los parámetros irán siempre prefijados por el símbolo de los dos puntos :, de igual manera que hacíamos con los servicios de routing.

En cuanto al segundo parámetro, es opcional y consiste en el conjunto de valores por defecto para los parámetros de la URL. Podemos sobreescribirlos luego en las llamadas a métodos concretos.

Si alguno de los parámetros es una función, se ejecutará siempre antes de cada uso.

En caso de que en la URL parametrizada no tenga alguno de los parámetros, se pasará como parametro de búsqueda en la URL. Ejemplo: para la URL /camera/:brand y los parámetros {brand:'canon', filter:'EOS 1100d'}, obrendríamos la URL /camera/canon?filter=EOS%201100D

Si el valor del parámetro va precedido por una arroba @, entonces el valor de ese parámetro se extraerá del objeto que pasemos cuando invoquemos una acción, como veremos más adelante en los ejemplos.

El servicio acepta también un tercer parámetro, que veremos tras los ejemplos.

Volviendo al código que hemos generado

1
var User = $resource('http://jsonplaceholder.typicode.com/users/:id', {id:'@id'}});

Veamos las operaciones que podemos realizar con él

11.2.1. Query

Forma: User.query(params, successCallback, errorCallback)

Realiza una petición GET, y espera recibir un array de ítems en la respuesta JSON.

Como vemos en el siguiente ejemplo, el atributo params es opcional, así como el la función de callback de error:

1 2 3 4 5 6 7 8 9 10 11
angular.module('restful', ['ngResource']) .controller('MainCtrl', function($scope, $resource){ var User = $resource( 'http://jsonplaceholder.typicode.com/users/:id', {id:'@id'} ); var userList = User.query(function(userList) { $scope.userList = userList; }); });
1 2 3 4
<div ng-app="restful" ng-controller="MainCtrl"> <h3>Query</h3> <pre>{{ userList | json}}</pre> </div>

Aunque, si queremos, podemos acceder a la promesa de resultados que genera la petición del servicio $http de la siguiente manera:

1 2 3 4 5 6 7 8 9 10 11 12 13
angular.module('restful', ['ngResource']) .controller('MainCtrl', function($scope, $resource){ var User = $resource( 'http://jsonplaceholder.typicode.com/users/:id', {id:'@id'} ); var userList = User .query().$promise .then(function(userList) { $scope.userList = userList; }); });

11.2.2. Get

Forma: User.get(params, successCallback, errorCallback)

Realiza una petición GET al servidor, y espera recibir un objeto como resultado de la respuesta JSON.

1 2 3 4 5 6 7 8 9 10 11
angular.module('restful', ['ngResource']) .controller('MainCtrl', function($scope, $resource){ var User = $resource( 'http://jsonplaceholder.typicode.com/users/:id', {id:'@id'} ); var user = User.get({id:1}, function(user) { $scope.user = user; }); });
1 2 3 4
<div ng-app="restful" ng-controller="MainCtrl"> <h3>GET</h3> <pre>{{ user | json}}</pre> </div>

En este caso, hemos pasado un objeto como primer parámetro que tiene el atributo id. Éste reemplazará el valor en el template de la url por el valor 1.

11.2.3. Save

Forma: User.save(params, payloadData, successCallback, errorCallback).

Envía una petición POST al servido. El cuerpo de la petición será el objeto que pasemos en el atributo payloadData.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
angular.module('restful', ['ngResource']) .controller('MainCtrl', function($scope, $resource){ var userToSave = { "id": 1, "name": "Winchester McFly", "username": "wmf", "email": "wmf@hdh.com", }; var User = $resource( 'http://jsonplaceholder.typicode.com/users/:id', {id:'@id'} ); User.save( userToSave, function(){ $scope.message = 'usuario guardado con éxito'; }, function(){ $scope.message = 'error al guardar'; } ); });
1 2 3 4
<div ng-app="restful" ng-controller="MainCtrl"> <h3>Save</h3> <pre>{{ message | json}}</pre> </div>

En este caso, hemos introducido función de callback de error, ya que la API no nos permite realizar peticiones POST.

11.2.4. Delete

Formas:

  • User.delete(params, successCallback, errorCallback)

  • User.remove(params, successCallback, errorCallback)

Realiza una petición HTTP DELETE al servidor. Ejemplo:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
angular.module('restful', ['ngResource']) .controller('MainCtrl', function($scope, $resource){ var User = $resource( 'http://jsonplaceholder.typicode.com/users/:id', {id:'@id'} ); User.delete( {id:1}, function(){ $scope.message = 'Usuario eliminado correctamente' }, function(){ $scope.message = 'Eror al eliminar' } ); });
1 2 3 4
<div ng-app="restful" ng-controller="MainCtrl"> <h3>Save</h3> <pre>{{ message | json}}</pre> </div>

11.2.5. Definiendo acciones nuevas

Los métodos vistos (query, get, save y delete) son los únicos métodos que proporciona el servicio $resource, con el que podríamos comunicarnos con una gran cantidad de servicios RESTful.

Pero, ¿qué pasa si me comunico con una API que usa POST para guardar ítems nuevos, mientras espera PUT para actualizar ítems existentes? ¿Ya no es válido el servicio $resource?

Aunque no viene un método PUT por defecto en el servicio, sí que tenemos la posibilidad de crearlo. Es aquí donde entra en juego el tercer parámetro que habíamos obviado hasta ahora en la creación del servicio.

En él podemos definir nuevas acciones en nuestro servicio. Se trata de un hash donde declararemos todas las acciones custom que queramos añadir. La documentación de AngularJS detalla al completo todos los parámetros que recibe. Nosotros, declararemos una función update que realizará una petición PUT al servidor:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
angular.module('restful', ['ngResource']) .controller('MainCtrl', function($scope, $resource){ var userToUpdate = { "id": 1, "name": "Winchester McFly", "username": "wmf", "email": "wmf@hdh.com", }; var User = $resource( 'http://jsonplaceholder.typicode.com/users/:id', {id:'@id'}, { update: {method:'PUT'} } ); User.update( userToUpdate, function(data){ $scope.message = data; }, function(){ $scope.message = 'error al actualizar'; } ); });
1 2 3 4
<div ng-app="restful" ng-controller="MainCtrl"> <h3>Save</h3> <pre>{{ message | json}}</pre> </div>
EJERCICIO
Adapta los ejemplos para conseguir aplicaciones que hagan lo mismo con comentarios (query, get, update).

11.2.6. Métodos a nivel de instancia

Puede que haya llamado la atención la declaración var User = $resource(…​), por haber usado mayúsculas. Esto es porque $resource genera una clase, y todos los métodos que hemos visto los hemos invocado a nivel de constructor.

Sin embargo, también podemos crear instancias de la clase User, lo que expone métodos a nivel de dicha instancia. Los métodos serán los mismos, pero prefijados por el símbolo del dólar $.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
angular.module('restful', ['ngResource']) .controller('MainCtrl', function($scope, $resource){ var data = { "id": 1, "name": "Winchester McFly", "username": "wmf", "email": "wmf@hdh.com", }; var User = $resource('http://jsonplaceholder.typicode.com/users/:id', {id:'@id'}, {update: {method:'PUT'}}); var u1 = new User(data); var u2 = new User(data); var u3 = new User(data); u1.$delete( function(res){ $scope.message1 = res; }, function(res){ $scope.message1 = 'error al eliminar'; } ); u1.$save( function(res){ $scope.message2 = res; }, function(res){ $scope.message2 = 'error al guardar'; } ); u1.$update( function(res){ $scope.message3 = res; }, function(res){ $scope.message3 = 'error al actualizar'; } ); });
1 2 3 4 5 6 7 8 9 10
<div ng-app="restful" ng-controller="MainCtrl"> <h3>Delete</h3> <pre>{{ message1 | json}}</pre> <h3>Save</h3> <pre>{{ message2 | json}}</pre> <h3>Update</h3> <pre>{{ message3 | json}}</pre> </div>

11.3. Interceptores

El servicio $http de AngularJS nos permite registrar interceptores que se ejecutarán en cada petición. Éstos resultan muy útiles cuando queremos realizar algún tipo de procesamiento sobre todas, o prácticamente todas las peticiones.

Supongamos que queremos comprobar cuándo tenemos permisos para realizar una petición. Para ello, podemos definir un interceptor que comprueba el código de estado de la respuesta y, si es un 401 (HTTP 401 Unauthorized), relanza lanza un evento indicando que se está realizando una operación no autorizada. Además, modificará todas las peticiones que enviemos, añadiendo las cabeceras de autorización básica.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86
angular .module('auth', []) .factory('AuthService', ['$log', function ($log) { var instance = {}; var authServiceLastDate = new Date(); var userData = null; var authToken = null; var doCheck = function(){ if((new Date()).add(-30).minutes().getTime() > authServiceLastDate) { $log.debug('Session expired.'); authServiceLastDate = null; userData = null; } authServiceLastDate = (new Date()).getTime(); }; instance.setUserData = function (userData) { authServiceLastDate = (new Date()).getTime(); userData = userData; }; instance.getUserData = function () { doCheck(); return userData; }; instance.deleteUserData = function () { userData = null; }; instance.createBasicAuthToken = function(login, password) { return btoa(login + ':' + password); }; instance.setToken = function (token) { authServiceLastDate = (new Date()).getTime(); authToken = token; }; instance.getToken = function () { doCheck(); return authToken; }; instance.deleteToken = function () { authToken = null; }; return instance; }]); .factory('AuthInterceptor', ['$rootScope', '$q', 'AuthService', function ($rootScope, $q, AuthService) { var instance = {}; instance.request = function(config) { config.headers = config.headers || {}; if (!!AuthService.getToken()) { config.headers.Authorization = 'Basic ' + AuthService.getToken(); } else { delete config.headers.Authorization; } return config; }; instance.response = function(response) { if (response.status === 401) { AuthService.deleteUserData(); AuthService.deleteToken(); $rootScope.$emit('auth.unauthorized', []); } if(response.data.status && response.data.status === 'ERROR') { //Force error return $q.reject(response); } return response; }; return instance; }]) .config(function($httpProvider){ $httpProvider.interceptors.push('AuthInterceptor'); });

Los interceptores son servicios de tipo factoría que registramos en el $httpProvider, añadiéndolos a la cola $httpProvider.interceptors. Al hacerse en un provider, tenemos que realizar esta operación en la fase de configuración.

Hay dos tipos de interceptores, y dos tipos de interceptores de rechazo:

  • request: estos interceptores reciben como parámetro un objeto http config. Podemos modificar este objeto config, o bien crear uno nuevo. Se espera que esta función devuelva un objeto config (bien sea el existente o el nuevo) o una promesa que contenga el objeto config.

  • requestError: este interceptor se llama cuando un interceptor previo lanza un error o se resuelve con un rechazo.

  • response: estos interceptores reciben como parámetro un objeto http response. Podemos modificar este objeto response o crear uno nuevo. Se espera que esta función devuelva un objeto response (bien sea el existente o el nuevo) o una promesa que contenga el objeto response.

  • responseError: este interceptor se llama cuando un interceptor previo lanza un error o se resuelve con un rechazo.