El conocimiento es el nuevo dinero.
Aprender es la nueva manera en la que inviertes
Acceso Cursos

SPA Router en Qwik

· 10 min de lectura
SPA Router en Qwik

Antes de comenzar, vamos a alinearnos sobre las diferencias entre MPA-routing y SPA-routing. En resumen: En MPA-routing, la página hace un refresco completo en cada navegación.

SPA-routing utiliza la propiedad history del objeto ventana para gestionar el estado del enrutamiento. Así que para SPA-routing, la página no se actualiza completamente y el objetivo es sólo volver a renderizar partes de la página. Si echamos la vista atrás a tecnologías más antiguas, SPA-routing solía ser mucho más rápido, pero con Qwik-city la diferencia de rendimiento podría ser trivial.

Qwik ya lanzó su versión 1. Si quieres saber más te dejo el siguiente blog.

¿Por qué estoy escribiendo esto?


Bueno, porque... es un ejercicio genial... Aprendí mucho, me di contra paredes que no esperaba golpear, y me ayudó a entender puntos de dolor que experimenté con routers en otros frameworks. Pero eso no es suficiente, ¿verdad? No, ese no es mi único factor impulsor para desmitificar el enrutamiento SPA en Qwik. Creo que los enrutadores SPA tienen bastantes beneficios. Creo en Qwik, y creo que sería aún más impresionante verlo funcionar con SPA-routing.

Estado


Una de las ventajas de un SPA-router es que no perdemos el estado de la aplicación... Dado que la instancia de nuestra aplicación sólo se crea una vez y se mantiene viva, podemos mantener vivo el estado en nuestra aplicación.

No sólo podemos compartir estado entre componentes, sino también entre páginas.

A algunos usuarios les gusta tener la barra lateral colapsada, a otros no. Es un poco molesto cuando colapsas una barra lateral, luego navegas a otra página donde la barra lateral salta abierta de nuevo porque el estado no se comparte.

Si tu quieres conocer acerca de la versión 1 de Qwik te dejo el siguiente video.

El poder del estado en las rutas


Somos fans de poner el estado (params y searchParams) en las rutas. No todo el estado pertenece allí, pero mantener el estado en las rutas nos da algunos beneficios:

  • Podemos marcar una página sin perder ese estado.
  • Es posible copiar/pegar URLs para compartirlas con otras personas sin perder ese estado que se guarda en esa ruta.
  • Es libre de manejar, sin necesidad de frameworks complejos, sin complejidad en cuanto a invalidación de estados, etc.
  • Permite utilizar los botones de navegación del navegador para volver a estados anteriores y siguientes.

⚠️ Nota: Cuando usamos MPA-routing también podemos poner el estado en la url, pero cuanto más estado cambiase en la url, más refrescos de página tendríamos lo que resultaría en menos usabilidad.

Usabilidad


Tener que refrescar las páginas en cada cambio de ruta puede causar ciertas molestias a la usabilidad de nuestra aplicación.

  • La posición del cursor se olvida al refrescar.
  • El texto seleccionado se deselecciona al refrescar.
  • Una videollamada en curso o incluso una película que estemos viendo se cerrará al refrescar.
  • El sonido de fondo se interrumpe al actualizar.
  • Los diálogos abiertos, snackbars, banners y mensajes de éxito son difíciles de mostrar/mantener vivos en una actualización completa.

📝 Ej: Enviar el contenido de un formulario en la página A, y navegar a la página B en caso de éxito. ¿Cómo y cuándo mostrar un mensaje de éxito? Los frameworks MPA suelen llamar a esto mensajería "flash", pero es más fácil de gestionar en un SPA.

Rendimiento


Sólo queremos cargar lo que necesitamos, esa es la idea detrás de Qwik.

¿Tiene sentido recargar el mismo DOM cuando ya lo tenemos?
¿Tiene sentido volver a renderizar parte del DOM que ya está renderizado?

Por ejemplo, el menú.
Qwik tiene lazy-loading desde el primer momento. Funciona y funciona de maravilla. ¿Por qué no usarlo también para el enrutamiento?

Arquitectura


Un router outlet es básicamente un componente que renderizará una página en una especie de marcador de posición: el DOM de toda la página permanece igual, pero lo que está dentro del router outlet se actualiza.

Las salidas de enrutador pueden ser bastante potentes, especialmente si puedes anidarlas como podemos hacer con el enrutador de Angular.

El sistema de enrutamiento que escribí no soporta múltiples salidas de enrutador anidadas todavía, pero cuando tenemos múltiples salidas de enrutador podemos utilizarlas para optimizar nuestra arquitectura. Podríamos adjuntar un diálogo a una ruta para que podamos cerrar ese diálogo haciendo clic en el botón Atrás del navegador nativo.

No tenemos que mantener el estado de ese diálogo, sólo usamos la salida del enrutador para renderizar un componente y destruirlo cuando sea necesario.

Eventos


Cuando tenemos SPA-routing, es bueno ser notificado cuando algo en la URL cambia.

🤔 Imaginemos que estamos en una página de gestión de usuarios con funcionalidad de búsqueda, y queremos hacer la consulta de búsqueda marcable.

En ese caso, cuando el usuario escriba "Brecht", queremos que la URL cambie a /users/search?q=Brecht para que podamos marcarla. No queremos actualizar toda la página cada vez que el usuario escriba un carácter, ¿verdad? Eso daría lugar a problemas de cursor con la entrada de búsqueda.

🫣 Piensa también en el debouncing... Queremos que se nos notifique cuando cambie ese parámetro q específico. Cuando lo hace, realizamos una llamada XHR, y en caso de éxito volvemos a renderizar parte de la página con los resultados. ¿Sabes lo que es aún más impresionante? Si tenemos una actualización completa de la página, obtenemos exactamente el mismo resultado renderizado en el servidor, porque así de impresionante es Qwik.

Recuerda que si quieres aprender más acerca de Qwik puedes visitar nuestro

Escribiendo un SPA-router para Qwik


Esta versión del router está en una fase muy temprana y podría pulirse, pero los principios están ahí. Así que vamos a ir a través del código juntos.

La configuración


Aquí es donde todo comienza, tenemos que crear un archivo de configuración que asigna rutas a los componentes. Una ruta puede contener params. Como Angular y Nest, podemos usar la sintaxis : para definir params.

// routing/routing-types.ts
export type RoutingConfigItem = {
    component: any;
    path: string;
}
export type RoutingConfig = RoutingConfigItem[];
// routing-config.tsx
export const routingConfig:RoutingConfig = [
    {
        path: '',
        component: <Home/>
    },
    {
        path: 'users',
        component: <Users/>
    },
    {
        path: 'users/:id',
        component: <UserDetail/>
    }
  
]

La ruta base / se resolverá en la página de inicio, la ruta users al componente , y la users/:idal componente <UserDetail/>.

El estado


Qwik nos proporciona un mecanismo de estado. Queremos reflejar el estado en la URL al estado de Qwik. Primero necesitamos acceder a la URL en el servidor, luego pasarla a la función render.

A continuación, tenemos que pasar al  componente <Root/> , que pasa a lo largo del componente <App/> . Ese componente <App/>   inicializará el enrutador con la URL.

// entry.dev.tsx
...
render(document, <Root url={''}/>);
// entry.ssr.tsx
export function render(opts: RenderOptions) {
  return renderToString(<Root url={opts.url as string || ''} />, {
    manifest,
    ...opts,
  });
}
// root.tsx
export default (opts: { url: string }) => { 
  return (
    <html>
      ...
      <body>
        <App url={opts.url}/>
      </body>
    </html>
  );
};

Por lo tanto, lo que acabamos de hacer aquí es asegurarnos de que el componente <App/>  recibe la URL que se le pasa en todos los casos. Eso es todo. Ahora vamos a configurar el estado.

// routing/routing-state.ts
import {createContext} from '@builder.io/qwik';

export interface RoutingState {
    // we don't want to store `new URL()` because it is not serializable    
    url: string; 
    segments: string[];
}

export const ROUTING = createContext<RoutingState>('Routing');
// routing/routing.ts
import {ROUTING, RoutingState} from './routing-state';
import {useContextProvider, useStore} from '@builder.io/qwik';

// this one will be called by the <App/> component and initialize 
// the state once for the entire lifecycle of the application
export function initializeRouter(url: string): RoutingState {
    // create a store and state
    const routingState = useStore<RoutingState>(
        getRoutingStateByPath(url)
    );

    useContextProvider(ROUTING, routingState);
    return routingState;
}

// this will retrieve the routingstate by the path (the current url)
export function getRoutingStateByPath(path: string): RoutingState {
    const url = new URL(path);
    const segments = url.pathname.split('/');
    segments.splice(0, 1); // remove empty segment 
    return {
        url: path,
        segments
    }
}

La primera parte del estado está hecha, sólo tenemos que inicializar el router en el componente <App/> .

// containers/app/app.tsx
export const App = component$((opts: { url: string | undefined }) => {
    initializeRouter(opts.url);
    ...
});

Todo bien. Ahora queremos establecer el estado del router cuando la ruta cambia. Hay 2 escenarios:

  • El usuario hace clic en un enlace y quiere navegar hacia una página de nuestra aplicación navigateTo()
  • Los botones de navegación del navegador están siendo utilizados, y queremos escuchar esos eventos listenToRouteChanges()

Esta es una funcionalidad que sólo queremos ejecutar en el navegador, no en el servidor. Aquí usamos isServer, pero también podríamos usar isBrowser.

// routing/routing.ts
import {isServer} from '@builder.io/qwik/build';

// safely get the window object
export function getWindow(): Window | undefined {
    if (!isServer) {
       return typeof window === 'object' ? window : undefined
    }
    return undefined;
}

export function navigateTo(path: string, routingState: RoutingState): void {
    if (!isServer) {
        // we don't actually navigate, but push a new state to
        // the history object
        getWindow()?.history?.pushState({page: path}, path, path);
        setRoutingState(path, routingState);
    }
}

export function listenToRouteChanges(routingState: RoutingState): void {
    if (!isServer) {
        // when the navigation buttons are being used
        // we want to set the routing state
        getWindow()?.addEventListener('popstate', (e) => {
            const path = e.state.page;
            setRoutingState(path, routingState);
        })
    }
}

export function setRoutingState(path: string, routingState: RoutingState): void {
    const oldUrl = new URL(routingState.url);
    const newUrl = new URL(path, oldUrl);
    const {segments, url} = getRoutingStateByPath(newUrl.toString())
    routingState.segments = segments;
    routingState.url = url;
}

La salida del enrutador


Tenemos un objeto de configuración, proporcionamos el estado del enrutador, podemos obtener ese estado del router, y podemos escuchar los cambios que establecerán automáticamente el estado del enrutador.

Además también tenemos una función navigateTo() que actualizará el objeto history en lugar de recargar la página. Ahora queremos renderizar los componentes correctos para la ruta correcta dentro de una salida del router.

Nuestro componente de aplicación se ve así:

// containers/app/app.tsx
export const App = component$((opts: { url: string | undefined }) => {
    const routingState = initializeRouter(opts.url);
    return (
        <section>
            ... here comes the menu
            <RouterOutlet/>
        </section>
    );
});

Ahora vamos a crear nuestro componente <RouterOutlet/> . Tenemos los segmentos de la URL, y la configuración de enrutamiento que podemos asignar a un componente.

// routing/router-outlet.tsx
import {component$, useContext} from '@builder.io/qwik';
import {ROUTING} from './routing-state';
import {getMatchingConfig,} from './routing';
import {routingConfig} from '../routing-config';

export const RouterOutlet = component$(
    () => {
        const routingState = useContext(ROUTING);
        // render the correct component
        return getMatchingConfig(routingState.segments, routingConfig)?.component
    }
);

La función getMatchingConfig() traducirá los segmentos y la configuración en el componente real que queremos renderizar. Esto requiere cierta lógica para que coincida no sólo con el componente correcto, sino que también tenga en cuenta los parámetros. ¿Recuerdas esta parte de la configuración?

{
    path: 'users/:id',
    component: <UserDetail/>
}

No profundicemos demasiado en el siguiente código, basta con saber que hace la traducción por nosotros:

// routing/routing.ts
...
// go over all the RoutingConfigItem objects and if they match return the config
// so we know which compnent to render
export function getMatchingConfig(segments: string[], config: RoutingConfig): RoutingConfigItem {
    const found = config.find(item => segmentsMatch(segments, item))
    if (found) {
        return found;
    }
    return null;
}

export function segmentsMatch(pathSegments: string[], configItem: RoutingConfigItem): boolean {
    const configItemSegments = configItem.path.split('/');
    if (configItemSegments.length !== pathSegments.length) {
        return false;
    }
    const matches = pathSegments.filter((segment, index) => {
        return segment === configItemSegments[index] || configItemSegments[index].indexOf(':') === 0
    });
    return matches.length === pathSegments.length;
}

Ahora la aplicación debería funcionar. Debería mostrar el componente correcto en la URL correcta, pero aún no hemos llegado a ese punto.

¿Recuerdas la función listenToRouteChanges()? Todavía tenemos que llamarla. Podemos llamarla en el componente , pero tenemos que asegurarnos de que sólo la ejecutamos en el cliente: el objeto ventana no existe en el servidor.

Para ello, Qwik nos proporciona la función useClientEffect$. El router outlet tiene ahora este aspecto.

import {component$, useClientEffect$, useContext} from '@builder.io/qwik';
import {ROUTING} from './routing-state';
import {getMatchingConfig, listenToRouteChanges} from './routing';
import {routingConfig} from '../routing-config';

export const RouterOutlet = component$(
    () => {
        const routingState = useContext(ROUTING);
        useClientEffect$(() => {
            listenToRouteChanges(routingState);
        });
        return getMatchingConfig(routingState.segments, routingConfig)?.component
    }
);

El componente de enlace


La etiqueta de anclaje tradicional  <a> refrescará completamente la página, que no es lo que queremos. En su lugar, queremos escribir la función navigateTo() Vamos a crear un <Link/> que muestra una etiqueta de anclaje, pero evita la funcionalidad por defecto y llama al componente navigateTo() Cuando el usuario hace clic.

Utilizamos la función preventdefault:click para asegurarnos de que la navegación real está bloqueada, pero seguimos necesitando una sintaxis href propiedad para un buen SEO. A continuación, dentro del <a>  utilizamos una etiqueta <Slot/> para la proyección de contenidos. El sitio navigateTo() requiere la routingState, por lo que importamos useContext de Qwik para recuperar ese estado.

// routing/link.tsx
import {component$, Slot, useContext} from '@builder.io/qwik';
import {navigateTo} from './routing';

export const Link = component$((opts: { path: string }) => {
    const routingState = useContext(ROUTING);
    const {path} = opts;
    // check whether the link should be active or not
    const isActive = `/${routingState.segments.join('/')}` === path;
    return (
        <a
            // This will prevent the default behavior of the "click" event.
            preventdefault:click 
            // set the correct class when the link is active
            className={isActive ? 'link--active' : ''}
            href={path} onClick$={(e) => {
            navigateTo(path, routingState)
        }}><Slot/></a>
    );
});

El .tsx del componente de la aplicación tiene ahora este aspecto:

<section>
    <ul>
        <li>
            <Link path={'/'}>Home</Link>
        </li>
        <li>
            <Link path={'/users'} >users</Link>
        </li>
        <li>
            <Link path={'/users/1'}>Brecht</Link>
        </li>
    </ul>
    <RouterOutlet/>
</section>

Hemos configurado con éxito el enrutamiento SPA del lado del cliente con soporte de parámetros sin demasiado esfuerzo. Faltan dos cosas: funcionalidad para obtener parámetros de ruta y parámetros de búsqueda.

En la configuración tenemos {path: 'users/:id'}, y en la URL tenemos users/1, así que queremos algo como getParams(routingState).id que devuelva la cadena "1".

En routing/routing.ts añadimos 2 funciones más:

// routing/routing.tsx
export function getParams(routingState: RoutingState): { [key: string]: string } {
    const matchingConfig = getMatchingConfig(routingState.segments, routingConfig);
    const params = matchingConfig.path.split('/')
        .map((segment: string, index: number) => {
            if (segment.startsWith(':')) {
                return {
                    index,
                    paramName: segment.replace(':', '')
                }
            } else {
                return undefined
            }
        })
        .filter(v => !!v);
    const returnObj: { [key: string]: string } = {};
    params.forEach(param => {
        returnObj[param.paramName] = routingState.segments[param.index]
    })
    return returnObj;
}

export function getSearchParams(routingState: RoutingState): URLSearchParams {
    return new URL(routingState.url).searchParams;
}

Conclusión


¡Eso es todo! Tenemos un completo router SPA del lado del cliente sin mucho código, que funciona con lazy loading gracias a que Qwik lo provee out of the box.

Aprendizajes:

Tenemos que bloquear el enrutamiento tradicional mediante la creación de un componente de enlace personalizado que también empuja un nuevo estado al objeto de historia.

No podemos almacenar el prototipo de URL en el estado porque no es serializable y tener la cadena es suficiente.


Las rutas no deberían ser tan complejas. Logramos mucho con una pequeña cantidad de código.

useClientEffect$ es útil cuando sólo quieres ejecutar algo en el cliente.

También puedes ver el código fuente de esta demo. Espero que también te haya resultado interesante.

Fuente

Qwik Routing

Otros Artículos relacionados

Plataforma de cursos gratis sobre programación

Artículos Relacionados

Escribiendo menos tests y más largos
· 9 min de lectura
Usando Pipes para transformar datos
· 5 min de lectura
Llama3 sacale el máximo provecho
· 4 min de lectura