11. Comunicación con el servidor
Guardaremos todos los ejercicios que hagamos en la carpeta Aplica el tag 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 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 |
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 atributoparams
, 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 |
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 httpconfig
. Podemos modificar este objetoconfig
, o bien crear uno nuevo. Se espera que esta función devuelva un objetoconfig
(bien sea el existente o el nuevo) o una promesa que contenga el objetoconfig
. -
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 httpresponse
. Podemos modificar este objetoresponse
o crear uno nuevo. Se espera que esta función devuelva un objetoresponse
(bien sea el existente o el nuevo) o una promesa que contenga el objetoresponse
. -
responseError
: este interceptor se llama cuando un interceptor previo lanza un error o se resuelve con un rechazo.