Cómo construir aplicaciones web Angular mantenibles y escalables

Cuando nos enfrentamos a un proyecto web Angular de cierta envergadura y que va a tener un largo recorrido, hay que poner énfasis en conseguir un ritmo sostenible en su desarrollo, evitando deuda técnica y que, con el tiempo, el coste del cambio en el código no se vuelva exponencial.

Angular es un proyecto vivo, con una comunidad  fuerte y una vibrante actividad. Esto es lo que le da madurez al proyecto, pero también provoca un exceso informativo que es necesario acotar.

Después de haberlo puesto en práctica en varios proyectos, voy a darte unas cuantas pautas y buenas prácticas que seguimos en Biko en la construcción de aplicaciones web con Angular, que nos ayudan en su construcción, mantenimiento y evolución facilitando el cambio con el menor impacto posible.

Una aplicación web Angular escalable necesita código fácil de entender, modificar y evolucionar. Debemos evitar código duplicado y acoplado, por lo que iremos desarrollando pequeños componentes que realizan tareas concretas (responsabilidad única) y que colaboran entre ellos para conseguir la funcionalidad deseada.

angular

1. Código basado en componentes

Todo el html está construido únicamente por componentes de Angular que colaboran entre sí cuando es necesario. Diferenciamos dos categorías básicas de componentes:

  • views: componentes que se comunican con los servicios y se encargan de pintar el layout base de la página, o trozo de página, de la cual son responsables. Generalmente una página se renderiza con la composición de más de uno de estos componentes. También se encargan de nutrir de datos a componentes más genéricos y de actuar sobre eventos que ocurren en los mismos.
  • components: componentes muy reutilizables en su mayoría encargados de mostrar información y/o interactuar con el usuario. Por ejemplo: botones, alertas, listados, combos y otros componentes más específicos del negocio de la aplicación.

Una característica de estos componentes es que contienen todo el código necesario para funcionar y definen sus dependencias si requieren de otros componentes o librerías de terceros. Por ejemplo, un componente para mostrar un mapa de Google, podría estar compuesto por los siguientes ficheros:

  • map.component.ts: define el componente y contiene el código de su controlador
  • map.html: plantilla html del componente
  • map.scss: estilos del componente
  • map.api.ts: servicio que se conecta con google para operar con su API de mapas
  • map.module.ts: declaración del módulo Angular del componente

De esta manera tenemos el código poco acoplado, aislado del resto y fácilmente reutilizable.

El módulo es la pieza que une el resto para formar componente completo y quedaría algo así:

import * as angular from 'angular';

import { MapComponent } from './map.component';
import { MapApi } from './map.api';
import './map.scss';

export default angular.module('app.component.map', [])
  .component('bkMap',MapComponent())
  .service('mapApi', MapApi);

2. Gestión de dependencias y procesado del código

Un código modular genera muchas pequeñas piezas de código referenciadas entre sí. Para gestionar eficazmente esas referencias y ayudarnos a generar el código optimizado según nuestras necesidades para desplegar en los diferentes entornos usamos Webpack.

Esta herramienta se encarga de:

  • Recorrer todas las dependencias de cada módulo
  • Procesar cada dependencia (convertirla, compilarla, manipularla, etc.)
  • Generar un paquete de ficheros final con todo el código procesado, listo para ser ejecutado en un navegador web.

Cuando hablamos de dependencias no solo nos referimos al código javascript, sino a las plantillas html, código SASS, ficheros json, imágenes y todo aquello que requiera el código.

3. Entorno de pruebas

Las pruebas automatizadas nos ayudan a tener código fácil de probar, podemos refactorizar con mayor seguridad y nos teje una red de seguridad para futuros cambios.

Para pruebas unitarias nos gusta usar karma para ejecutar los tests y jasmine para escribirlos. Son herramientas maduras y con una gran comunidad por detrás si surgen dudas o problemas.

Para pruebas de integración desde la interfaz, usamos protractor, un framework basado en WebDriverJS y construido por el propio equipo de Angular. Nos ayuda a probar el código en instancias de navegadores reales.

4. Guía de estilos del código

Es importante seguir unas reglas a la hora de formatear nuestro código. Cuanto más grande es un equipo o empresa, más importante es seguir estas reglas porque, entre otras cosas, nos ayudan a:

  • Leer más rápido cualquier pieza de código, ya que todo el mundo lo escribe con el mismo formato. Esto hace más fácil la colaboración en los proyectos, aunque no hayas participado en él desde el comienzo.
  • Evitan ensuciar el historial de cambios con ajustes de formato de código.
  • Evitan conflictos en los merge provenientes de commits que contienen ajustes en el formato de código mientras otra persona está también modificado dicho código, algo frustrante y a veces complejo de solucionar.

Una vez consensuado cómo queremos formatear el código, puedes configurar herramientas que se encarguen de velar del formato por ti. Estas herramientas son los linters, y no solo velarán por el formato, sino que las puedes configurar para que te avisen de malas prácticas habituales.

Actualmente usamos eslint para el código Javascript y tslint para el código en TypeScript.

5. Estilos CSS modulares y mantenibles

Hoy en día el código CSS de un proyecto web es cada vez más complejo y debemos cuidarlo como el resto de nuestro código. A la hora de escribir código CSS tenemos en cuenta tres conceptos importantes:

  • Atomic Design: Nos ayuda a escribir código modular e ir creando, a partir de pequeñas piezas, otras más complejas.
  • Notación BEM: Nos da patrones y reglas para nombrar los estilos de nuestros componentes de interfaz. Estilos fáciles de saber cuándo y cómo deben ser usados, que evitan conflictos de nombres y nos permiten definir variantes de nuestros componentes.
  • ITCSS: Nos ayuda a construir una arquitectura del código CSS lo más fácilmente mantenible y evolucionable. Existe un gran post con recursos sobre ITCSS.

6. Definición de API’s REST y emulación del mismo

Si tu aplicación Angular necesita de acceso a datos, lanzar procesos de negocio, etc. seguramente use uno o más API’s que le provean de todo ello. También es habitual desarrollar en paralelo tanto la aplicación web como el API. Y, por lo general, montar casos de prueba con el API en desarrollo es complejo y costoso.

Este problema lo solucionamos creándonos un servidor fake que emula el API usando  NodeJS con ExpressJS o restify. Estas herramientas nos permiten tener un API fake en cuestión de horas para realizar todas las pruebas que necesitemos y montar fácilmente cualquier caso de uso que necesitemos.

Por otra parte, para realizar la comunicación con las API’s necesitas conocer su especificación.

Para ello usamos RAML o Swagger que nos permiten, de una manera formal, describir cómo funciona el API. Puede definirla el equipo encargado de su desarrollo o, mejor aún, escribirlo conjuntamente con ellos.

Existen multitud de soluciones alrededor de RAML y Swagger capaces de leer su formato y construir automáticamente documentación del API, servidores fake de pruebas, testeadores del API, etc. y parseadores de la definición del API para hacerte tu propia herramienta a medida.

Como ejemplo, usando RAML, si quisiéramos definir el recursos «users» de un API para obtenerlos sería algo así:

/api/users:
  get:
    description: Listado de usuarios
    queryParameters:
      tipo:
        description: Tipo de usuarios a obtener
          type: string
    responses:
      200:
        body:
          application/json:
            schema: !include usuarios.get.res.schema.json
            example: !include usuarios.get.res.example.json
      403:
        description: No se tienen permisos para obtener los datos

7. Automatización de tareas

Compilaciones, despliegues, preparar paquetes para los despliegues, etc. son tareas repetitivas, arduas y propensas a errores humanos si se realizan manualmente. Posiblemente también puedan robarte gran parte de tu tiempo.

Una buena inversión para tus proyectos es automatizar todas aquellas tareas repetitivas que te sea posible. Una buena herramienta para ello es gulp.

Gulp te permite automatizar muchas de tus tareas y procesos programándolas usando Javascript y permitiéndote usar cualquier módulo disponible en NodeJS.

Por ejemplo, si quisiéramos automatizar la optimización de imágenes (gif, jpg, png y svg) para que ocupen menos tamaño pero sin perder calidad, la tarea gulp podría ser algo así:

'use strict';
const gulp = require('gulp');
const imagemin = require('gulp-imagemin');
const cache = require('gulp-cache');
const size = require('gulp-size');

gulp.task('images', () =>
 gulp.src('src/assets/images/**/*')
   .pipe(cache(imagemin([
     imagemin.gifsicle({interlaced: true}),
     imagemin.jpegtran({progressive: true}),
     imagemin.optipng(),
     imagemin.svgo({plugins: [{cleanupIDs: false}]})
   ])))
   .pipe(gulp.dest('.tmp/assets/images'))
   .pipe(size({title: 'images'}))
);

8. Entorno de pruebas con el cliente

Una vez finalizada una historia, se la entregamos y enseñamos al cliente para que nos dé feedback. En los casos en los que necesitamos un servidor fake del API para enseñar la aplicación, este servidor fake lo desplegamos junto con la aplicación angular.

Un servicio que nos funciona muy bien para agilizar estos despliegues y enseñar a cliente es el servicio PaaS (platform as a service) de Openshift. Una vez configurado, desplegar una nueva versión de la aplicación y servidor fake del API consiste en hacer un push en el repositorio facilitado por Openshift, así de simple.

Tras el push, Openshift lanza sus hooks para parar servicios, desplegar el nuevo código y levantar nuevamente los servicios. Si lo necesitamos, podemos añadir nuevos hooks por medio para realizar cualquier tarea que necesitemos en el servidor.

Como puedes ver, son unas cuantas pautas, aunque no son las únicas. Conforme las vas incorporando a tu trabajo diario, las mejoras en los procesos y en el producto final son tan visibles que pronto te preguntarás cómo habías podido vivir si ello hasta ahora 😉 Así que si te animas y lo pruebas, si utilizas otras herramientas distintas o sigues algunas otras pautas que a ti te funcionan, ¡cuéntanoslo!