4. Módulos y servicios
Como hemos visto en la sesión de introducción, para definir cualquier elemento dentro de AngularJS tenemos que declarar, en primer lugar, un módulo:
1
2
3
4
5 angular
.module('helloApp', [])
.controller ('HelloCtrl', function($scope){
$scope.name = 'World';
});
El objeto angular define una serie de utilidades. Una de ellas es module, que permite definir módulos. Un módulo es como una especie de contenedor de objetos gestionados por AngularJS, como por ejemplo los controladores.
4.1. Module
Para que el inyector de AngularJS sepa cómo crear y conectar todos estos objetos, neceista un registro de recetas. Cada receta tiene un identificador del objeto y la descripción de cómo crearlo.
Una receta debe pertenecer a un módulo de AngularJS, que es un saco que contiene una o más recetas. Y además, un módulo también puede contener información sobre otros módulos.
Cuando se inicia una aplicación de AngularJS con un módulo, AngularJS crea una instancia de un injector, que es quien crea el registro de las recetas como una unión de las existentes en el core, el módulo de la aplicación y sus dependencias El inyector consulta al registro de recetas cuando ve que tiene que crear un objeto para la aplicación.
Para definir un módulo, indicamos su nombre como primer argumento. El segundo argumento es un array, en el que incluiremos otros módulos, en caso de tener alguna dependencia. Ya vimos algo de esto en la sesión de introducción cuando introdujimos las rutas.
La llamada a angular.module('helloApp', []) devuelve una instancia de un módulo recién creado. Una vez tenemos la instancia, podemos definir controladores. Como hemos visto en el ejemplo, ésto se hace llamando a la función controller(controllerName, controllerConstructor).
Una vez hemos definido un módulo, tenemos que informar a AngularJS de su existencia, cosa que haremos dando un valor a la directiva ng-app:
1 <body ng-app="helloApp">
La directiva ngApp designa el elemento raíz de nuestra aplicación, y habitualmente se coloca cerca del elemento raíz de la página, como las etiquetas <body> o <html>.
4.2. Servicios
Una vez hemos declarado un módulo, hemos dicho que podemos usarlo para registrar una serie de recetas para la creación de objetos de diversa índole.
Pero aunque sean de diversa índole, podríamos categorizar estos objetos en dos grandes grupos: servicios y objetos especializados.
Un servicio es un objeto cuya API está definida por el desarrollador que escribe dicho servicio.
Por su parte, un objeto especializado se ajusta a una API específica de AngularJS. Estos objetos pueden ser: controladores, directivas, filtros o animaciones.
El inyector de AngularJS necesita saber cómo crear estos objetos, y se lo decimos tipificando nuestro objeto a la hora de crearlo. Hay cinco tipos de recetas.
La más verbosa, pero también la más comprensible, es la del Provider. El resto (Value, factory, Service y constant) son sólo azúcar sintáctico sobre la definición de un Provider.
Veamos los diferentes escenarios para crear y usar servicios.
Todos los servicios en AngularJS son singletons [1]. Esto significa que el inyector usará las recetas como mucho una vez para crear el objeto. Posteriormente, éste se cacheará para la próxima vez que pueda hacer falta. |
Como veremos, los creadores de AngularJS llamaron Service a una de estas recetas que designan servicios. De manera que cuando veamos su término en inglés nos estaremos refiriendo a la receta en concreto, mientras que cuando hagamos referencia al término servicio nos estaremos refiriendo a cualquiera de ellos. |
4.2.1. Value
Digamos que queremos un servicio muy sencillo, llamado clientId, que nos devuelve un String que representa el identificador de usuario que usamos para alguna API remota. Lo podríamos definir de esta manera
1
2 var myApp = angular.module('myApp', []);
myApp.value('clientId', 'a123456le54321x');
Hemos creado un módulo de AngularJS llamado myApp. Posteriormente, hemoss dicho que este módulo contiene la receta para construir el servicio clientId, que en este caso únicamente devuelve una cadena.
Si quisiéramos mostrarlo vía two-way data binding lo haríamos de la siguiente manera:
1
2
3 myApp.controller('DemoController', ['clientId', '$scope', function DemoController(clientId, $scope) {
$scope.clientId = clientId;
}]);
1
2
3
4
5 <html ng-app="myApp">
<body ng-controller="DemoController">
Client ID: {{clientId}}
</body>
</html>
En este ejemplo, hemos usado la receta Value para dársela a DemoCtrl cuando
invoca al servicio clientId
.
4.2.2. Factory
Un Value es fácil de escribir, pero se echan de menos unos elementos importantes que necesitamos a menudo a la hora de escribir servicios. Así, pasaremos a conocer un elemento más complejo, llamado factory. Una factory nos permite:
-
tener dependencias con otros servicios
-
inicializar el servicio
-
inicialización perezosa
Una factory construye un nuevo servicio mediante una función con cero o más argumentos. Estos argumentos son dependencias con otros servicios, que se inyectarán en tiempo de creación.
Una factory no es más que una versión más potente de un Value, de manera que podemos reescribir el servicio clientId de la siguiente manera:
1
2
3 myApp.factory('clientId', function clientIdFactory() {
return 'a12345654321x';
});
Pero dado que el token no es más que un a cadena, quizá crear un Value sería más apropiado en este caso, y el código sería además más sencillo de interpretar.
Digamos que, por ejemplo, queremos crear un servicio encargado de calcular un token para autenticarse contra una API remota. Este token se llamará apiToken y se calculará en función del valor de clientId, y de una clave secreta guardada en el almacenamiento local del navegador:
1
2
3
4
5
6
7
8
9
10
11 myApp.factory('apiToken', ['clientId', function apiTokenFactory(clientId) {
var encrypt = function(data1, data2) {
// NSA-proof encryption algorithm:
return (data1 + ':' + data2).toUpperCase();
};
var secret = window.localStorage.getItem('myApp.secret');
var apiToken = encrypt(clientId, secret);
return apiToken;
}]);
En el código anterior, vemos cómo se define el servicio apiToken con la receta de una factory que depende del servicio clientId. Entonces crea un token de autenticación a través de una encriptación hiperpotente indescifrable por la NSA [2].
Entre las best practices se recomienda nombrar una factory de la forma <nombreDelServicio>Factory. Aunque nadie lo requiere, ayuda a la hora de revisar código, o bien a la hora de debuggear. |
De igual manera que un Value una factory puede crear un servicio de cualquier tipo, ya sea una primitiva, un objeto, una función o una instancia de un tipo de dato propio.
4.2.3. Service
Los desarrolladores JavaScript usan tipos de datos custom para escribir código OO. Veamos cómo podríamos lanzar un Unicornio [3] al espacio mediante un servicio unicornLauncher, que es una instancia del siguiente objeto:
1
2
3
4
5
6
7
8
9 function UnicornLauncher(apiToken) {
this.launchedCount = 0;
this.launch = function() {
// make a request to the remote api and include the apiToken
...
this.launchedCount++;
}
}
Ya podemos lanzar unicornios al espacio, pero démonos cuenta que nuestro lanzador requiere un apiToken. Lo bueno es que ya habíamos creado una factory que resolvía este problema.
1
2
3 myApp.factory('unicornLauncher', ["apiToken", function(apiToken) {
return new UnicornLauncher(apiToken);
}]);
Éste es, precisamente, el caso de uso más adecuado para un Service.
La receta de un Service produce un servicio, de igual manera que habíamos visto con Value y factory, pero lo hace invocando un constructor mediante el operador new. El constructor puede recibir cero o más argumentos, que representan dependencias que necesita la instancia de este tipo.
Además, un Service sigue un patrón de diseño llamado constructor injection. De manera que es el propio AngularJS quien se encarga de instanciar un nuevo objeto de la clase dada. Como nuestro UnicornLauncher tiene un constructor, podemos reemplazar la factory por un Service, de la siguiente manera:
1 myApp.service('unicornLauncher', ["apiToken", UnicornLauncher]);
4.2.4. Provider
Como se ha dicho anteriormente, el Provider es la base sobre la que se crean el resto de servicios que hemos visto en los apartados anteriores. Precisamente porque es la base sobre la que se asientan el resto, no será difícil comprender que es la que nos ofrece mayores posibilidades. Pero en la mayoría de los casos todo lo que ofrece es excesivo, y será recomendable hacer uso de los otros servicios.
La receta de un Provider se define como un tipo propio que implementa un método $get. Este método es una función factoría [4], como el que se usa en una factory. De hecho, si definimos una factory, lo que se hace es crear un Provider vacío cuyo método $get apunta directamente a nuestra función.
Deberíamos usar un Provider cuando queremos introducir cierta configuración que esté disponible a para toda la aplicación. Para asegurar esto, esto debe hacerse antes de que se ejecute la aplicación, en una fase llamada fase de configuración. De esta manera, podemos crear servicios reutilizables cuyo comportamiento podría cambiar ligeramente entre aplicaciones.
Por ejemplo, nuestro lanzador de unicornios es tan potente y útil que lo van a usar muchas de las aplicaciones del parque de aplicaciones de nuestra empresa. Por defecto, el lanzador de unicornios los proyecta al espacio sin ningún tipo de protección ni escudo. Pero en algunos planetas, la atmósfera es tan pesada que debemos proteger a nuestros unicornios con papel de plata para evitar que se incineren al atravesar la atmósfera y así evitar su extinción. Estaría muy bien que pudiéramos configurar esto nuestro lanzador, y usarlo en la app que haga falta. Lo haríamos configurable de la siguiente manera:
1
2
3
4
5
6
7
8
9
10
11
12
13
14 myApp.provider('unicornLauncher', function UnicornLauncherProvider() {
var useTinfoilShielding = false;
this.useTinfoilShielding = function(value) {
useTinfoilShielding = !!value;
};
this.$get = ["apiToken", function unicornLauncherFactory(apiToken) {
// let's assume that the UnicornLauncher constructor was also changed to
// accept and use the useTinfoilShielding argument
return new UnicornLauncher(apiToken, useTinfoilShielding);
}];
});
Para activar el traje espacial de papel de plata, necesitamos crear una función config en la API de nuestro módulo, e inyectar en ella el unicornLauncherProvider:
1
2
3 myApp.config(["unicornLauncherProvider", function(unicornLauncherProvider) {
unicornLauncherProvider.useTinfoilShielding(true);
}]);
Observemos que el Provider ha sido inyectado en la función de configuración. Esta inyección la hace in provider injector, que es distinto del inyector de instancias habitual que usaremos en el resto de nuestra aplicación. Este inyector únicamente trabaja con providers, ya que el resto de objetos no se crean en la fase de configuración.
Durante la inicialización de la aplicación, antes de que AngularJS haya creado ningún servicio, configura e instancia los providers. A esto lo llamamos la fase de configuración del ciclo de vida de la aplicación. Durante esta fase, como hemos dicho, los servicios no son accesibles porque aún no se han creado.
Una vez se ha finalizado la fase de configuración, ya no se puede interactuar con un Provider y empieza el proceso de crear servicios. A esta parte del ciclo de vida de la aplicación se le conoce como fase de ejecución.
4.2.5. Constant
Hemos visto cómo AngularJS divide el ciclo de vida de una aplicación en las fases de configuración y ejecución, y cómo se puede dotar de configuración a la aplicación a través de la función config. Dado que la función config se ejecuta en una fase en la que no tenemos servicios disponibles, no se tiene acceso ni siquiera a objetos sencillos creados con la utilidad Value.
Sin embargo, podemos tener valores tan simples como un prefijo de una url, que no necesiten dependencias o configuración, y que sean útiles en las fases de configuración y ejecución. Para esto sirve la utilidad constant.
Supongamos que nuestro servicio unicornLauncher puede estampar, en un unicornio, el nombre del planeta contra el que está siendo lanzado en la fase de configuración. El nombre del planeta es específico para cada aplicación, y también lo usan muchos controladores en tiempo de ejecución. Podemos definir entonces el nombre del planeta como una constante:
1 myApp.constant('planetName', 'Greasy Giant');
Ahora, podemos configurar nuestro unicornLauncherProvider de la siguiente manera:
1
2
3
4 myApp.config(['unicornLauncherProvider', 'planetName', function(unicornLauncherProvider, planetName) {
unicornLauncherProvider.useTinfoilShielding(true);
unicornLauncherProvider.stampText(planetName);
}]);
Y dado que una constant hace que el valor también esté disponible en la fase de ejecución, podemos usarla también en nuestros controladores:
1
2
3
4 myApp.controller('DemoController', ["clientId", "planetName", function DemoController(clientId, planetName) {
this.clientId = clientId;
this.planetName = planetName;
}]);
1
2
3
4
5
6
7 <html ng-app="myApp">
<body ng-controller="DemoController as demo">
Client ID: {{demo.clientId}}
<br>
Planet Name: {{demo.planetName}}
</body>
</html>
4.3. Objetos de propósito especial
Anteriormente, hemos mencionado que tenemos objetos de propósito especial, cuya funcionalidad es diferente de la que ofrece un servicio. Estos objetos extienden el framework como plugins, implementando interfaces definidas por AngularJs. Estas interfaces son: controller, directive, filter y animation.
A excepción del objeto controller, el inyector usa la receta de una factory para crear estos objetos.
Ya hemos visto algo de estos objetos en las sesiones anteriores, y profundizaremos más en los siguietnes capítulos.
4.4. En resumen
Un inyector usa una serie de recetas para crear dos tipos de objetos: servicios y objetos de propósito especial. Para crear servicios, usamos cinco tipos de receta dinstintos: Value, factory, Service, Provider y constant.
De ellos, los más comunes son factory y Service, y sólo se distinguen en que los Service funcionan mejor con tipos de objetos ya definidos, y una factory devuelve funciones y primitivas JavaScript. Un Provider es la receta "padre" de todos ellos, que no son más que azúcar sintáctico de un Provider. Un Provider es la receta más potente, pero no es necesaria a menos que necesitemos un componente reutilizable que requiera de algún tipo de configuración a nivel de aplicación, y es por esto que es el único elemento disponible en la fase de configuración de un aplicación.
4.5. Ejercicios
Aplica el tag modules a la versión que quieres que se corrija.
Los ejercicios se realizarán sobre el carrito de la compra de la sesión anterior.
4.5.1. Creación de una factoría (0,67 puntos)
En nuestro módulo, debemos crear una factoría. La llamaremos productsFactory
y expondrá un único método, llamado getProducts()
, que devolverá el listado de productos que teníamos en el controlador. Inyectaremos la factoría en el controlador, y ahora el listado de productos será $scope.products = productsFactory.getProducts()
.
Creación de un servicio (0,67 puntos)
En nuestro módulo, también crearemos un servicio al que llamaremos shoppingCartService
. Dicho servicio tendrá tres métodos: addToCart(product)
, getCart()
y getTotal()
. Inyectaremos el servicio en nuestro controlador, donde refactorizaremos nuestra lógica: $scope.addToCart = shoppingCartService.addToCart
, $scope.total = shoppingCartService.getTotal()
y $scope.cart = shoppingCartService.getCart()
.