| Tabla de contenidos |
|---|
Importación
Para la navegación en Vue utilizamos el paquete vue-router. Podemos ver la documentación oficial con todos los detalles aquí.
...
Para el UI del Portal de Servicios unificado, cada servicio definirá sus rutas en un archivo module.routes.js, que se situará en el directorio raíz del servicio, y tendrá esta estructura:
Rutas de un servicio
| Bloque de código | ||
|---|---|---|
| ||
import { defineAsyncComponent router} from './router'; |
Y también en la parte del new Vue:
| Bloque de código | ||
|---|---|---|
| ||
new Vue({ router, render: hvue'; const ExpedientesView = defineAsyncComponent(() => h(App), }).$mount('#app') |
Definir rutas
Para definir las rutas, tendremos un directorio router/ dentro de src/, que contendrá index.js. En él podemos definir una lista con un objeto por cada ruta, de la siguiente forma:
| Bloque de código | ||
|---|---|---|
| ||
{ path: '/frameworkFundeweb' import(/* webpackChunkName: "CEXP_ExpedientesView" */'./views/ExpedientesView.vue')); const rutaProyecto = '/consulta/expedienteacademico'; export default [ { path: `${rutaProyecto}/`, name: 'frameworkCEXP fundewebExpedientes', component: ExpedientesView, meta: { requiresAuth: FrameworkFundeweb } |
Aquí nos definimos la ruta que tendremos en la url, un nombre que incluiremos opcionalmente, para referenciarlo desde otras partes del código en lugar de escribir la ruta, y por último el componente que se mostrará cuando naveguemos a la ruta.
true,
},
},
{ ... },
{ ... },
]; |
Todos las rutas serán lazy, se cargarán los componentes cuando se navegue a ellos, para evitar que se tenga que cargar todo al inicio de la aplicación. Para eso, como vemos, utilizamos defineAsyncComponent y en el import especificamos webpackChunkName, especificando un nombre distintivo (no poner "home", por ejemplo, mejor poner como en el ejemplo, algo distintivo de tu servicio)Un ejemplo de fichero completo sería este:
| Bloque de código | ||
|---|---|---|
| ||
import { defineAsyncComponent Vue} from 'vue' import VueRouter from 'vue-router' import Home from '.; const nombreInterno1 = defineAsyncComponent(() => import(/* webpackChunkName: "nombreFragmentoEnDist1" */'./views/HomeComponente1.vue')); const nombreInterno2 = defineAsyncComponent(() => import(/* Profile from '.webpackChunkName: "nombreFragmentoEnDist2" */'./views/ProfileComponente2.vue')); |
Después, como vemos, definimos la ruta base del proyecto, que utilizaremos en cada una de las rutas.
Añadir variables del proyecto (meta) para nuestras rutas. Funcionalidades extra
Por último, para las rutas que requieran estar autenticado, especificamos incluimos esto:
| Bloque de código | ||
|---|---|---|
| ||
meta: Vue.use(VueRouter); const routes = [ { pathrequiresAuth: "/", name: "home", component: Home }, { path: '/profile', name: 'profile', component: Profile, props: true } ] const router = new VueRouter({ mode: 'history', routes }) export default router |
Si nos fijamos, podemos poner en cada ruta un campo props a true. Con esto, cuando naveguemos a esa ruta, los parámetros que se le pasen cuyo nombre coincida con un prop definido en el componente se asignará a este directamente. Es importante tener en cuenta que para hacer esto, tendremos que hacer la navegación utilizando el name, y no el path, pues si no, no funciona. Para pasar las props al navegar, lo haremos especificándolos como campos dentro de un objeto que se pase a params:
...
| language | js |
|---|
...
true,
}, |
Además, existen (edición 21/02/2023) dos nuevas variables meta que se pueden añadir a cada una de nuestras rutas, :
- needs2FA: Se añadirá a aquellas rutas que necesiten un factor doble de autenticación por parte del usuario, aunque todo este factor de autenticación deberá comprobarse en cada llamada al backend. Es para darle una mejor experiencia al usuario cuando navegue entre pantallas.
- hiddenFeature: Se utilizará cuando queramos mostrar algo a algún usuario y que llegue hasta producción pero no esté preparado para funcionar al 100% (bien porque no se tengan datos definitivos, bien porque no haya ni backend) y se podrá activar para navegar visitando
/feature-flag/nuestra/rutaSi no la activamos, enviará al usuario a un error 404.
Como con requiresAuth, no es necesario añadirlas si su valor va a ser false.
Al final, una ruta completa quedaría de este estilo:
| Bloque de código | ||||||
|---|---|---|---|---|---|---|
| ||||||
meta: {
requiresAuth: true,
needs2FA: true,
hiddenFeature: true,
group: 'user',
application: 'profile-data',
}, |
| Advertencia |
|---|
El name de la ruta debe ser ÚNICO EN TODO EL PROYECTO. Si hay nombres de rutas repetidos, no funcionará. Por lo tanto, tendremos que evitar nombres generales com "Home". Lo mejor que podemos hacer es poner un prefijo característico de nuestro servicio. Por ejemplo, para el servicio Consulta de Expedientes, el nombre de la única ruta que tiene es "CEXP Expedientes", utilizando CEXP para distinguirlo. |
Añadir rutas al portal
Para añadir las rutas al router global de POSE, tendremos que modificar el archivo app.routes.js, situado en src/. Hay que importar el archivo previo e incluirlo al final del objeto que devuelve el router, de esta forma.
Import:
| Bloque de código | ||
|---|---|---|
| ||
import nombreRutasComponente from './nombreGrupo/nombreProyecto/nombreComponente'; |
Declaración:
| Bloque de código | ||
|---|---|---|
| ||
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:
Definir sub-rutas
Podemos definir rutas hijas de una ruta ya existente, añadiéndolas al parámetro children de la ruta padre:
...
En este ejemplo la ruta padre sería /about, y la ruta hija /about/detalle. A la hora de definir rutas hijas, es importante tener en cuenta que el path no debe incluir la barra ("/"), pues si lo hace lo toma como lo que hay inmediatamente después de la url de nuestro sitio.
Añadir rutas dinámicamente
Podemos añadir rutas en tiempo de ejecución, con la instrucción addRoute que nos proporciona el propio router. Para ello, en archivos JavaScript tendremos que importar el router, y si es desde nuestros componentes podemos acceder directamente a través de this.$router. La función addRoute tiene un primer parámetro opcional en el que le debemos indicar el nombre de la ruta padre si queremos que sea una sub-ruta, y el otro parámetro será el objeto de la ruta:
...
...
// Ruta normal
this.$router.addRoute({ path: "/algo", name: "Algo", component: Algo });
// Sub ruta
this.$router.addRoute("NombrePadre", { path: "algo", name: "Algo", component: Algo });Podemos encontrar este método, así como el resto de métodos que ofrece Vue Router en su documentación API Reference.
Un detalle a tener en cuenta es que si redirigimos a un sitio externo, las rutas añadidas dinámicamente no son persistentes, cuando volvamos esas rutas no existirán, habría que añadirlas de nuevo.
Vista del router
El router habrá que incluirlo como un componente más . Así, podemos definir la parte de la pantalla que cambiará cuando se cambie de ruta, pudiendo dejar fijo, por ejemplo, un menú lateral, y que sólo se tenga que cargar el contenido de la página.Para esto, en el archivo .vue inicial, el que se carga al principio, basta con incluir (en POSE ya está incluido), así:
| Bloque de código | ||
|---|---|---|
| ||
<router-view></router-view> |
...
| Bloque de código | ||
|---|---|---|
| ||
<router-link :to="{ name: 'nuevaComunicacion', params: { idReenviar: fila.data.id }}"> |
Como vemos, se le indica a dónde navegar con el nombre que hemos definido previamente para la ruta. Además podemos pasar datos como parámetro, con params (también vale para pasar props al componente al que se navega). Después, en el componente al que se navega, podemos coger dichos datos referenciando a this.$route.params. En este ejemplo, el objeto que se ha pasado se cogería con this.$route.params.idReenviar.
Si queremos hacerlo de forma programática, en la parte de JavaScript con this.$router.push():
| Bloque de código | ||
|---|---|---|
| ||
// path
this.$router.push('/home')
// path
this.$router.push({ path: '/home' })
// nombre + parámetros path (....um.es/loquesea?userId=123)name
this.$router.push({ name: 'user', params: { userId: '123' } })
// nombre + parámetros query (....um.es/loquesea?userId=123)
this.$router.push({ name: 'user', params: { userId: '123' } }) |
Aquí vemos las opciones de path params y query params. Para utilizar los path params, es necesario navegar por nombre, no utilizando el path. Para obtener sus valores en el componente al que se navega, lo podemos hacer con:
| Bloque de código | ||
|---|---|---|
| ||
// path params
this.$route.params.nombreparametro
// query params
this.$route.query.nombreparametro |
...
Home' }) |
En Composition API, la sintaxis cambia, en vez de acceder a this.$router y this.$route, disponemos de useRouter() y useRoute():
| Bloque de código | ||
|---|---|---|
| ||
<script setup>
import { userRouter, useRoute } from 'vue-router'
const router = useRouter()
const route = useRoute()
</script> |
Y ya se accede como visto anteriormente, pero usando router o route:
| Bloque de código |
|---|
// path
router.push('/home')
// path
router.push({ path: '/home' })
// name
router.push({ name: 'Home' }) |
Interceptores de navegación
...
Estos métodos pueden ser globales o locales a cada ruta.
Interceptores globales
beforeEach
Podemos registrar interceptores de manera global para definir un comportamiento que se ejecute justo antes de producirse la navegación a cualquier ruta. Esto lo hacemos con router.beforeEach, de esta manera:
| Bloque de código | ||
|---|---|---|
| ||
router.beforeEach((to, from, next) => {
// ...
}) |
Los parámetros de esta función son:
to: objeto con la información de la ruta a la que se navega.
from: objeto con la información de la ruta desde la que se navega.
next: es una función que permite reanudar la navegación. Nos permite llamarla de diferentes maneras para tener diferentes comportamientos:
next()reanuda la navegación hacia la ruta indicada ento.next('/')redirige a la ruta que se le pase como parámetro.next(false)aborta la navegación.next(new Error())aborta la navegación y se ejecuta el callbackonError.
Hay que asegurarse de que sólo se llama a next()una vez, en caso contrario podemos encontrarnos errores. Por ejemplo, esto estaría mal:
| Bloque de código | ||
|---|---|---|
| ||
// BAD
router.beforeEach((to, from, next) => {
if (to.name !== 'Login' && !isAuthenticated) next({ name: 'Login' })
// if the user is not authenticated, `next` is called twice
next()
}) |
La forma correcta sería esta:
| Bloque de código | ||
|---|---|---|
| ||
// GOOD
router.beforeEach((to, from, next) => {
if (to.name !== 'Login' && !isAuthenticated) next({ name: 'Login' })
else next()
}) |
Se pueden registrar múltiples interceptores del mismo tipo, y estos se ejecutarán en orden de aparición, es decir, el primero en registrarse es el primero que se ejecuta.
Los interceptores nos pueden venir bien para comprobar algún estado global de la aplicación, como la autorización del usuario.
beforeResolve
Se pueden registrar estos interceptores con router.beforeResolve, de la misma forma que los beforeEach. La diferencia es que estos se ejecutan justo antes de que se confirme la navegación, después de haber ejecutado los interceptores de ruta y de componentes, y que los componentes de ruta asíncronos se resuelvan.
afterEach
También se pueden registrar globalmente interceptoresafterEachque se ejecutarán después de confirmar la navegación. Se definen como losbeforeEach, pero sin un parámetronext, pues ya se ha confirmado la navegación:
...
| language | js |
|---|
...
(no los utilizaremos) o locales a cada ruta.
Interceptores por ruta
Se pueden definir interceptores beforeEnter directamente en el objeto que define las rutas, con la misma notación que los interceptores globales before:
...
| Bloque de código | ||
|---|---|---|
| ||
beforeRouteEnter (to, from, next) {
// called before the route that renders this component is confirmed.
// does NOT have access to `this` component instance,
// because it has not been created yet when this guard is called!
},
beforeRouteUpdate (to, from, next) {
// called when the route that renders this component has changed.
// This component being reused (by using an explicit `key`) in the new route or not doesn't change anything.
// For example, for a route with dynamic params `/foo/:id`, when we
// navigate between `/foo/1` and `/foo/2`, the same `Foo` component instance
// will be reused (unless you provided a `key` to `<router-view>`), and this hook will be called when that happens.
// has access to `this` component instance.
},
beforeRouteLeave (to, from, next) {
// called when the route that renders this component is about to
// be navigated away from.
// has access to `this` component instance.
} |
Volver arriba tras navegar
Por defecto, al navegar a otra ruta, se mantiene el scroll que teníamos en la ruta anterior. Si queremos que al hacerlo la página se ponga de nuevo al inicio, arriba del todo, tenemos que definir así el scrollBehavior al crear la instancia del router:
| Bloque de código | ||
|---|---|---|
| ||
const router = new VueRouter({ ..., scrollBehavior (to, from, savedPosition) { return { x: 0, y: 0 }; } }) |
Podemos consultar la documentación oficial para más detalles acerca del scrollBehavior.
Cacheo de assets
Para que se puedan hacer despliegues correctamente en Kubernetes, Cuando se cambia el nombre a un chunk de webpack, bien por hash bien porque se le cambia manualmente el nombre, si el usuario tiene cacheados según qué ficheros (o el HTML que lo llama o bien el JS) le dará la llamada un 404 si no se siguen dos pasos en el proyecto:
El primero, a nivel de nginx / apache / servidor que despliegue los assets: Evitar el cacheo de los servicios que devuelvan como tipo text/html. Para que el usuario no cachee el documento que realiza las llamadas a los JS/CSS. Como son aplicaciones SPA, los html son pequeños y no afecta excesivamente al rendimiento para el beneficio que da.
El segundo, a nivel de Vue Router, reiniciando la aplicación si se da un 404 en la carga de un chunk. Se ha añadido este código al router para gestionar el error:
| Bloque de código | ||
|---|---|---|
| ||
router.onError((error) => {
const hasBeenReloaded = localStorage.getItem('reloadChunkError') === 'on' || false;
const hasError = /loading chunk \d* failed./i.test(error.message);
if (hasError && !hasBeenReloaded) {
localStorage.setItem('reloadChunkError', 'on');
window.location.reload();
} else if (hasError) {
// eslint-disable-next-line no-alert
alert('No tienes la última versión de la aplicación. Por favor, recarga la aplicación');
}
}); |
...
