Migración y unificación de la UI: visión general

Antecedentes

Cuando empezamos a desarrollar esta nueva aplicación teníamos en mente una estructura de microservicios, separando tanto la UI como el API de los servicios implicados. Creímos que sería más fácil desarrollar aplicaciones en paralelo.

La realidad es que, tras varios servicios desarrollados, el API sí tiene una entidad propia mientras que la UI de éstos acaba siendo bastante simple, con pocas vistas y utilizando, en su mayoría, las mismas librerías y componentes comunes a todos los diferentes servicios.

Además, modificar algo simple, como un detalle en la cabecera, que estuviera en todas estas UI implicaba actualizar la librería de componentes común y volver a desplegar cada una de las aplicaciones. Sin contar con que cada UI tiene un pod de kubernetes independiente, con toda la infraestructura basada en nube que implica. 

Esto supone mucho más gasto, tanto en infraestructura como en tiempo de desarrollo de nuevas funcionalidades.

Soluciones elegidas para el problema

División de la aplicación en módulos

El primer paso ha sido unificar todos los directorios del código fuente en un único directorio llamado app. Dentro de este se ha dividido la aplicación en módulos siguiendo dos criterios:

Una vez separados por módulos, cada uno de ellos tendrá control sobre sus propias llamadas al API (que, aunque separada por microservicios y rutas, está en el mismo dominio), estado interno de sus servicios (Con Vuex), definición de rutas y las vistas que éstas cargarán, etc.

Intentando estandarizar el proyecto, también se ha decidido separar vistas de componentes, tal y como hace la instalación por defecto de Vue con vue-cli y router o Nuxt.

Llamadas a APIs backend

Cada módulo debe tener separadas sus propias llamadas al API. Heredarán todos de una capa sobre axios que enviará los tokens automáticamente, teniendo una única configuración para toda la aplicación. Aún así, si un equipo de desarrollo necesitara hacer llamadas propias o configurar su propia versión de axios podría perfectamente.

La idea es agrupar semánticamente las APIs. Si se tiene más de una dependencia con llamadas de API se pueden crear más de un fichero de llamadas. La solución de consenso en cuanto a nomenclatura ha sido poner a todos los ficheros de api este sufijo para distinguirlos fácilmente.

Componentes

Aquí se añadirán todos aquellos componentes que no se carguen directamente desde una ruta de vue router. Siguiendo las recomendaciones de Vue School para grandes aplicaciones se darán nombres correctos, obviamente, a cada componente pero no se harán subcarpetas dentro de esta carpeta, estando todos los componentes en la raíz.

Los componentes habrán de nombrarse usando únicamente PascalCase para seguir los estándares de uso de la comunidad JavaScript. 

No será obligatoria ninguna metodología de SCSS/CSS para poder aceptar las PR en el repositorio pero sí se comprobará que esté bien realizado, utilizando las variables CSS del proyecto y el uso de reglas válidas que no interfieran con el resto de grupos de desarrollo.

Igualmente, será obligatorio cumplir las reglas de los linter del proyecto, que se comprobarán antes de poder hacer push así como mientras se desarrolla. Se ayudará a los diferentes equipos de trabajo a configurar sus IDE de forma adecuada.

Vistas

Componentes que se cargan desde una ruta. Si está en la definición de las rutas del módulo se añadirá en esta carpeta. Las vistas, al fin y al cabo, siguen siendo componentes de Vue, pero, por legibilidad y estandarización del repositorio tal y como se trabaja en este tipo de proyectos, se dividen en estas dos categorías.

Obviamente, todas las reglas que se aplican a los componentes se aplicarán a las vistas. Al fin y al cabo, en Vue siguen siendo componentes.

Rutas

Todos los módulos definirán sus propias rutas en un fichero que se inyectará como dependencia en las rutas del sistema. La configuración de las rutas del sistema o de otros módulos deberán permanecer inalterables.

Como serán diferentes grupos de trabajo los que estarán envueltos, en caso de colisión de nombres se resolverá por los interesados, siempre pensando en las URL amigables de cara al usuario, no a la gestión interna del repositorio, que aún en 2022 hay que recordárselo a los desarrolladores.

Estado de la aplicación local al módulo

En lugar de almacenar el estado de la aplicación en una única store con todos los métodos, todos los módulos deberán tener sus propios estados locales a través de módulos de Vuex.

Esto añade una leve dificultad al desarrollo, ya que será necesario que se especifique el nombre al tratar tanto con el estado en sí como con mutaciones o acciones pero será mucho más fácil depurar cualquier fallo derivado del estado.

Con Vue 3 tenemos la opción de pasarnos para la gestión del estado la nueva librería Pinia pero, por el momento, necesitamos que pueda mantener más módulos y nos dé más información a la hora de los commits síncronos para depuración, ya que vamos a estar muchos grupos trabajando a la vez.

Actualización de los módulos

La actualización de Vue 2 a Vue 3 ha sido bastante sencilla. Únicamente ha habido que realizar los pasos que en la propia documentación de Vue han añadido para que el cambio sea totalmente retrocompatible. 

Por temas de tiempo de desarrollo, no se va a obligar a todos los equipos a volver a escribir su código en Vue 3 puro, por lo que la retrocompatibilidad era la única opción viable. Por suerte, es inmediato y funciona sin tener que modificar nada. 

Sólo hemos encontrado problemas con el componente que estábamos utilizando de calendario, que se ha sustituido por otro.

Como librería de componentes estamos usando PrimeVue. En esta nos encontramos un detalle a tener en cuenta. y es que al importar componentes que estén hechos de forma nativa en Vue 3, por el modo de retrocompatibilidad hay que especificar explícitamente que se importen como componentes Vue 3. Se hace de esta forma, al importar los componentes:

import Button from 'primevue/button';
import Dialog from 'primevue/dialog';
import Dropdown from 'primevue/dropdown';
import Message from 'primevue/message';
 
const compatMode3 = { MODE: 3 };
Dialog.compatConfig = compatMode3;
Dropdown.compatConfig = compatMode3;
Button.compatConfig = compatMode3;
Message.compatConfig = compatMode3;

Esto habrá que hacerlo para todos los componentes de Primevue, y para los de otras librerías que podamos utilizar que también tenga componentes Vue 3.


Migración paso a paso de la UI

Para realizar la migración de un componente en Vue 2 a Vue 3 han de realizarse los siguientes cambios:

Estructura de directorios

Se ha modificado la estructura lógica del proyecto con el fin de ayudar al desarrollo compartido por parte de múltiples grupos de trabajo.  Para mantener esta estructura, cada grupo de trabajo deberá realizar sus cambios en un directorio que penderá de src/app .  En él, se hará un subdirectorio por proyecto.  Todos los cambios del proyecto, deberán estar contenidos en ese subdirectorio.

Estructura directorios Vue3 FWjs

(Estructura directorios Vue3 FWjs)

Por otro lado, dado que la gran mayoría de elementos de la UI en Vue 2 han cambiado de ruta al migrar la UI a Vue 3, es necesario revisar las importaciones de los componentes.  Se detallarán los cambios concretos más adelante.

Rutas

Muchos elementos existentes en los ficheros de rutas son comunes a todos ellos, motivo por el que esta adaptación es la que más cambios requiere.

El fichero index.js del /router debe moverse a la raíz del proyecto a un fichero con nombre module.routes.js.

En él eliminamos:

En cuanto a la carga de las vistas, dado que todo se carga por lazy loading, se deben cambiar las llamadas que haya en los import del principio del fichero con una estructura similar a esta:

import nombreInterno1 from './views/Componente1.vue';
import nombreInterno2 from './views/Componente2.vue';

Y declararlas del siguiente modo:

import { defineAsyncComponent } from 'vue';

const nombreInterno1 = defineAsyncComponent(() => import(/* webpackChunkName: "nombreFragmentoEnDist1" */'./views/Componente1.vue'));
const nombreInterno2 = defineAsyncComponent(() => import(/* webpackChunkName: "nombreFragmentoEnDist2" */'./views/Componente2.vue'));

Se debe cambiar la definición del objeto routes de modo que quede así:

export default [
	{
		ruta1
	}
	{
		ruta2
	}
]

Además, el checkAuth se debe cambiar por:

meta: {
	requiresAuth: true,
},

en todas las entradas de rutas cuyas vistas requieran estar autenticado.

De este modo, la declaración de cada segmento de la ruta pasa de estar definido así:

{
	path: '/',
    name: 'NombreRuta',
    component: NombreComponente,
    beforeEnter: checkAuth,
},

a estar definido de este modo:

{
    path: '/',
    name: 'NombreRuta',
    component: NombreComponente,
    meta: {
      requiresAuth: true,
    },
},

Además, como los componentes se han cambiado de ubicación, es necesario adaptar el atributo path de las rutas.

Esta sería la estructura de las rutas de un proyecto FWJS en Vue 2:

 {
    path: '/',
    name: 'NombreInterno1',
    component: Componente1,
    meta: {
      requiresAuth: true,
    },
  },
  {
    path: '/url-componente2',
    name: 'NombreInterno2',
    component: Componente2,
    meta: {
      requiresAuth: true,
    }, 

  Y esta la de un proyecto FWJS en Vue 3:

export default [
    {
    path: '/nombreProyecto/',
    name: 'NombreInterno1',
    component: Componente1,
    meta: {
      requiresAuth: true,
    },
  },
  {
    path: '/nombreProyecto/url-componente2',
    name: 'NombreInterno2',
    component: Componente2,
    meta: {
      requiresAuth: true,
    },
  },
];

Viene bien recordar que la ruta con path '/' del módulo no debe llamarse 'home', ya que ésta debería ser la principal de micampus.

Una vez hechos estos cambios en el module.routes.js, hay que importar este nuevo fichero en el app.routes.js e incluir AL FINAL del objeto que devuelve el router la entrada correspondiente al module.routes del proyecto.

Import del fichero:

import nombreRutasComponente from './nombreGrupo/nombreProyecto/nombreComponente';

Declaración en el objeto:

  export default [
	...PoseRoutes,
	...MNCSRoutes,
    ...ultimoFicheroDeRutas,	<--
	{
    	path: '/:catchAll(.*)',
		...
	},
];

En la siguiente imagen se puede ver cómo ha quedado el fichero tras añadir el módulo de Mis Certificados:

(Estado app.routes.js tras enlazar con Mis Certificados)

Componentes y vistas

Para migrar un componente o una vista es necesario mover el fichero .vue antiguo a un subdirectorio llamado /components o /views que estará directamente bajo la raíz del proyecto. 

En cuanto al código fuente, solo hay que modificar las importaciones.  No es necesario realizar ningún otro tipo de adaptación.

Locales

Para facilitar los cambios, se recomienda que toda la internacionalización se encuentre dentro de la carpeta locales y no dentro de los componentes, pero es obligatorio internacionalizar los servicios que se realicen.

Crear una carpeta /locales en la raíz del proyecto y en ella crear un único fichero con prefijo i18n seguido del nombre del proyecto y extensión .json que contenga las cadenas para todos los idiomas.  El fichero debe tener esta estructura:

{
  "en": {
    "id1": "chain1",
    "id2": "chain2"
  },
  "es":{
    "id1": "cadena1",
    "id2": "cadena2"
  }
}


Ejemplo fichero internacionalización(Ejemplo fichero internacionalización)

Dado que las cadenas de internacionalización van a estar contenidas en el proyecto, la ruta para importar el fichero de internacionalización desde los distintos componentes tendrá la siguiente estructura:

<i18n src="../locales/i18nNombreProyecto.json"></i18n>



Llamada fichero internacionalización(Llamada fichero internacionalización)

APIs

El fichero index.js del /api debe moverse a la raíz del proyecto a un fichero con nombre nombreProyecto.api.js.

En él hay que adaptar apiRequest y baseApiURL:

import { apiRequest } from '@/api';
const moduleBaseApiURL = '/nombreProyecto/api/';

Aparte de estos cambios hay que eliminar la declaración de apiRequest y la importación de axios.

Stores

La Store se ha dividido en módulos, recomendamos ver src/app/app.store.modules.js para tener localizados los distintos módulos de la store que podamos necesitar.  Aunque los distintos módulos tienen un nombre autoexplicativo, destacamos dos elementos bastante utilizados que han cambiado de ubicación.

isLogged ya no está en el módulo Login de la store, sino en auth/. La importación de isLogged queda así:

computed: {
	...mapGetters('auth', ['isLogged']),
},


SET_TITLE ahora está ubicado en el módulo pages.  La nueva llamada queda de este modo:

mounted() {
	this.$store.commit('pages/SET_TITLE', 'Solicitar nuevo certificado');
	...
}

Modo de compatibilidad

Para que los componentes hechos en Vue 2 sigan funcionando sin problemas, se ha añadido un modo de compatibilidad en el proyecto que hace que se asuma por defecto que los componentes son de Vue 2. Esto, irónicamente, provoca que algunos componentes Vue 3 de librerías tengan algunos fallos en su funcionamiento. Para evitar esto, tendremos que indicar, tras importarlos, que tienen el modo de compatibilidad 3. Se hará así:

import Button from 'primevue/button';
import Dialog from 'primevue/dialog';
import Dropdown from 'primevue/dropdown';
import Message from 'primevue/message';
 
const compatMode3 = { MODE: 3 };
Dialog.compatConfig = compatMode3;
Dropdown.compatConfig = compatMode3;
Button.compatConfig = compatMode3;
Message.compatConfig = compatMode3;

Esto habrá que hacerlo, por ejemplo, con todos los componentes de Primevue mientras mantengamos el modo de compatibilidad.