Comenzamos un nuevo capítulo, después de haber realizado la introducción a las características de Qwik y sus diferencias con otras tecnologías Javascript.
En el anterior capítulo nos hemos quedado en el punto en el que hemos creado nuestro primer proyecto donde se ha implementado el enrutamiento (Routing
) a un nivel muy básico con un par de rutas.
Lo que vamos a ver en este capítulo es una continuación de lo visto con anterioridad con Qwik City
, donde lo veremos con todo detalle, tratando todos los aspectos más importantes y necesarios para poder trabajar de diferentes formas con ello.
Esto será lo que veremos en este capítulo junto con los conceptos que vamos a aprender.
El enrutamiento (Routing
) es una forma de asignar direcciones URL públicas de un sitio a componentes específicos declarados en nuestra aplicación.
En Qwik tenemos Qwik City
, que utiliza el enrutamiento basado en directorios.
Información con ejemplos y más detalles sobre Qwik City
.
Enlace a la documentación:
https://shorten-up.vercel.app/4vwjQzLolW
¿Qué significa enrutamiento basados en directorios
?
La estructura de su directorio de rutas va a especificar las URL públicas que el usuario va a poder hacer uso en su aplicación.
Una vez que ya nos hemos sumergido en este apartado tan importante y fantástico a la vez, vamos paso por paso, viendo los diferentes apartados.
Antes de empezar a trabajar nuestro proyecto, dejamos de lado el proyecto inicial 01-first-steps y creamos uno nuevo, siguiendo los pasos expuestos en el capítulo anterior. Como sugerencia os animo a que lo llaméis 02-routing.
El resultado paso a paso os lo iré dando con las diferentes variantes que iré creando a lo largo del capítulo como 02-01-routing-basic, 02-02-routing-markdown, etc.
Con este paso que hemos dado, ya tenemos preparado el proyecto sin ninguna configuración para poder ir viendo el progreso paso a paso.
En el directorio de rutas de Qwik City
(src/routes
), añadimos una nueva carpeta que será la carpeta-ruta-principal-de-la-ruta
+ un fichero index
que en conjunto harán que se asignen a una ruta URL de manera automática.
Por ejemplo, imaginaros que vemos una ruta con esta apariencia:
http://127.0.0.1:5173/admin
Sabemos que esta ruta se mostrará con el contenido del componente exportado en src/routes/admin/index
(ya sea .mdx
o .tsx
, entre otros formatos, que comentaré a continuación de los que podemos hacer uso).
src/
└── routes/
└── admin/
└── index.tsx # (.mdx, .jsx, ...) # /admin
Hay que tener en cuenta que el archivo del final de la ruta se debe de llamar index. Esto es SUPER importante.
Quizás en otros frameworks con los que habéis trabajado, se hace una distinción entre páginas y endpoints de datos o rutas API.
En Qwik City
no hay distinción, todos son endpoints en el fichero index.[tipo-de-fichero]
.
Podríamos añadir ficheros con otros nombres, pero para acceder a ellos como endpoints, NO van a servir.
Qwik City
admitirá los siguientes tipos de archivos para rutas, lo que si tenemos que hacer es SIEMPRE llamar al fichero index
, como se ha mencionado.
Nosotros podemos definir el fichero index.[tipo-de-fichero]
donde [tipo-de-fichero]
puede ser una de las siguientes extensiones:
.ts
.tsx
.js
.jsx
.md
.mdx
Para ver el funcionamiento de los nuevos endpoints vamos a ver un ejemplo con un caso QUE NO FUNCIONA en Qwik (por no estar creando el directorio con su fichero index
dentro) pero que probablemente funcione en otros frameworks o tecnologías con las que habéis trabajado, pero en Qwik no.
Creamos los siguientes ficheros y en todos añadimos el contenido del componente por defecto como este contenido en cada uno de los 4 ficheros (src/routes/index.tsx
no lo tocamos):
Siendo esta la estructura de los ficheros en estos momentos:
src/
└── routes/
├── contact.tsx
├── blog.tsx
├── portfolio.tsx
├── layout.tsx
└── index.tsx
Si hacemos la prueba con los siguientes endpoints teniendo como base la URL local, que en mi caso es http://127.0.0.1:5173
.
index.tsx
(http://127.0.0.1:5173
): FUNCIONA.portfolio.tsx
(http://127.0.0.1:5173/portfolio
): NO FUNCIONAEn este caso, como NO encuentra este recurso nos devuelve un error 404 con un mensaje general como el que se puede ver en la captura anterior.
Sobre el tema de personalizar esta página con el error 404 lo realizaremos en el capítulo correspondiente a cuando estemos trabajando con el apartado de layouts (plantillas).
Si probamos con blog.tsx
(http://127.0.0.1:5173/blog
) y con contact.tsx
(http://127.0.0.1:5173/contact
) nos pasará como con portfolio
, que NO encontrará la ruta y nos devuelve un error 404.
SOLO funcionará con index.tsx
que cumple correctamente la nomenclatura del nombre de fichero, dentro de routes
en la raíz de esta carpeta, por lo que como se ha podido ver, accede al contenido.
Por lo tanto, para que el acceso a una ruta SI funcione en nuestro proyecto Qwik debemos de dejar el contenido estructurado de la siguiente forma, con el directorio especificado para la página y dentro un fichero index.tsx
(Recordad las diferentes extensiones disponibles):
src/
└── routes/
├── portfolio/
│ └── index.tsx # /portfolio
├── contact/
│ └── index.tsx # /contact
├── blog/
│ └── index.tsx # /blog
└── index.tsx # /
Comprobando las rutas que nos han dado problemas:
/blog
Haremos lo mismo con /portfolio
y /contact
, nos debería de aparecer el contenido concreto a esas rutas, comprobadlo.
Puede pasar lo siguiente, que al ir a cargar cualquiera de las rutas en las que hemos visualizado el error “404 Not found” se muestre el contenido de la página en blanco, cosa que no debería:
Y en la consola un mensaje similar al siguiente:
Si ocurre esto, debemos de parar la ejecución del servidor y reiniciarlo. Volved a probar.
Al principio, el estructurar así los endpoints, puede parecer un trabajo extra, pero este enfoque tiene sus ventajas.
Una de esas ventajas es poder definir archivos de componentes en un directorio de ruta sin que se representen, pudiendo usarlo como un componente complementario para esa ruta que se añade al componente principal representado en index
.
Se podría considerar de la siguiente manera:
src/
└── routes/
├── index.tsx # /
...
└── blog/
├── index.tsx # /blog
└── other-component.tsx # Ignorado y NO mapea en URLs.
El fichero other-component.tsx
puede ser importado y usado dentro src/routes/blog/index.tsx
y la vez será completamente ignorado por Qwik City
a la hora de enrutar.
Si ejecutamos en el navegador la ruta http://127.0.0.1:5173/blog/other-component
nos dará un error 404.
Otra cosa es que decidamos añadir una nueva carpeta llamada other-component junto con el fichero index.tsx correspondiente dentro de src/routes/blog. En ese caso esa ruta si estaría disponible.
Una vez que tenemos claro como se definen las rutas, vamos a trabajar en lo que son los contenidos de esos endpoints.
El contenido de index
, de los puntos de entrada (endpoints), podremos implementarlo en formato componente o mediante el uso del renderizado de páginas con contenido markdown (extensiones .md
o .mdx
).
Nos vamos a centrar en cada uno de ellos para ver diferentes opciones y así saber cual nos puede interesar en algunos casos u otros, para llevar a cabo nuestros proyectos personales y profesionales.
Para devolver HTML para una ruta específica, deberá implementar un componente. Para archivos .tsx
, el componente debe exportarse por defecto que llamaremos default
.
De manera alternativa, podremos renderizar contenido en formato Markdown con las extensiones .md
y .mdx
.
import { component$ } from '@builder.io/qwik';
export default component$(() => {
return <H1>Hello World!</H1>;
});
Para poder trabajar con rutas que visualizan contenido en formato Markdown, lo que es el modo de crear el path (endpoint), es el mismo procedimiento, creando la carpeta.
Lo único que va a cambiar es la extensión fichero que crearemos, que en vez de llamarse index.tsx
como hemos estado haciendo hasta este momento, será index.mdx
(o index.md
)
Añadimos una nueva ruta llamada /readme
junto con su fichero index
, que en este caso será con la extensión mdx
dentro de src/routes/readme/index.mdx
(o md).
src/
└── routes/
├── index.tsx # /
...
|── readme/
| └─── index.mdx # /readme
└── blog/
└─── index.tsx # /blog
En la primera introduciremos contenido estático y en el segundo nos da opción de darle vida utilizando componentes Qwik (como cuando trabajamos con React)
Una vez creado el fichero, añadimos el siguiente contenido:
Ahora si accedemos al endpoint asignado: http://127.0.0.1:5173/readme
.
Debería de mostrarse la información (recordad, que habrá que reiniciar para que se asigne el endpoint en el caso de no visualizarse el contenido):
Si añadimos un elemento de lista con un enlace:
# Readme Rutas
Trabajando con formato markdown
* [Twitter](https://twitter.com/mugan86)
Al recargar quedará de la siguiente forma:
Podemos finalizar este apartado haciendo uso de la característica de los ficheros mdx
añadiendo un componente como contenido que crearemos en este directorio.
Creamos el componente con el fichero hello.tsx
en src/routes/readme
, añadiendo este contenido:
import { component$ } from '@builder.io/qwik';
export default component$(() => (
<>
Hello using <b>.mdx</b> type file
</>
));
Ahora estando en el fichero index.mdx
de este directorio, usando el siguiente contenido:
import HelloReadme from './hello.tsx';
... (AQUÍ LO QUE TENÍAMOS HASTA AHORA)
<HelloReadme />
Lo añadimos al fichero .mdx
, dejando de la siguiente forma:
import HelloReadme from './hello.tsx';
# Readme Rutas
Trabajando con formato markdown
* [Twitter](https://twitter.com/mugan86)
Este es un ejemplo usando un componente de Qwik dentro de un fichero `mdx`
<HelloReadme />
Y este sería el resultado final dentro de la ruta asociada a la carpeta readme
que se encuentra en src/routes/readme
cuya ruta pública es: http://127.0.0.1:5173/readme
.
Y llegados a este punto ya sabemos como trabajar con los endpoints principales, gestionando los contenidos en diferentes formatos.
Abriendo el proyecto 02-01-routing-basic aquí:
Los parámetros de ruta son partes de las URL que se extraen en los parámetros.
Imaginaros que tenemos una página con una URL que contiene un parámetro llamado id-del-producto
, que puede ser cualquiera de los miles de productos que tenemos en nuestra base de datos.
Obviamente, no sería nada práctico crear una ruta para cada producto ya que no sería nada fácil de mantener y a la vez no se aplicarían buenas prácticas.
En su lugar, vamos a definir un parámetro de ruta (una parte de la URL) que se va a usar para extraer el id
de nuestro producto con id-del-producto
de manera dinámica.
Teniendo como referencia el parámetro para el id
del producto, especificamos el parámetro de ruta dentro del directorio con el formato [ <nombre-parametro> ]
donde teniendo el id
del producto llamado por ejemplo productId
, se quedará asignado como [productId]
(fijaros en los corchetes, son muy IMPORTANTES).
Dentro del proyecto los especificamos de la siguiente manera:
src/
└── routes/
├── index.tsx # /
...
└── product/
└── [productId]/
├── index.tsx # /product/1234
└── details/
└── index.tsx # /product/1234/details
En el proyecto se visualiza así:
Con lo que ahora mismo tendríamos dos URLs públicas que siguen este formato:
http://127.0.0.1:5173/product/[productId]
.http://127.0.0.1:5173/product/[productId]/details
.En [productId]
podemos tener cualquier valor ahora mismo independientemente de la cantidad de productos que tengamos almacenados en nuestra base de datos. Este tema ya no nos debería de preocupar por tener n
posibilidades dándonos mucha libertad.
Ahora, lo interesante es pasar a la acción y ver que realmente funcionan bien estas dos nuevas URLs públicas, analizando con más detalles en que opciones no funciona.
Reiniciar el servidor en el caso de que no detecte las nuevas rutas parametrizadas.
OK
: http://127.0.0.1:5173/product/1234
.
OK
: http://127.0.0.1:5173/product/xyz-567
.
404
: http://127.0.0.1:5173/product/xyz-567/34
.
Con una de las opciones, por ejemplo la opción /product/1234
, como se puede apreciar se carga el contenido de la página principal del producto, tal como se ha especificado en el componente del fichero index.tsx
en la ruta de ficheros src/routes/product/[productId]/index.tsx
:
OK
: http://127.0.0.1:5173/product/1234/details
404
: http://127.0.0.1:5173/product/1234/detail
(IMPORTANTE FIJARSE COMO TERMINA, falta la s
)
Con la primera de las opciones, /product/1234/details
, se cargará el contenido de la página de detalles del producto, tal como se ha especificado en el componente del fichero index.tsx
en la ruta de ficheros src/routes/product/[productId]/details/index.tsx
:
Ahora que ya sabemos como añadir información de manera dinámica mediante parámetros de ruta donde hemos añadido el id
de un producto, lo que nos haría falta es plasmarlo en la información de nuestra página y esto lo haremos mediante la obtención del parámetro ruta desde una URL.
Una vez que ya disponemos del valor [productId]
en la URL, necesitaremos recuperar el valor que se adjunta en ese parámetro y esto lo podemos hacer mediante el uso de la API useLocation()
.
useLocation()
Podemos ampliar conocimientos con lo que tenemos a continuación: https://shorten-up.vercel.app/iTOhgRj98s
Lo aplicamos en el fichero index.tsx
dentro de la carpeta details
, cuya ruta es src/routes/product/[productId]/details/index.tsx
Añadimos el uso de useLocation()
aplicando de la siguiente forma para coger la información que nos proporciona dicho hook:
import { component$ } from '@builder.io/qwik';
import { useLocation } from '@builder.io/qwik-city';
export default component$(() => {
// Obtenemos toda la información acerca de la ubicación respecto a la navegación
const location = useLocation();
return (
<div>
<div>Product Item Details [productId] - Info page</div>
<p>Href: {location.url.href}</p> // URL princ. + path + valor de parámetro de ruta
<p>Pathname: {location.url.pathname}</p> // path + valor de parámetro de ruta
<p>Product Id: {location.params.productId}</p> // Valor de parámetro de ruta
</div>
);
});
Una vez guardados los cambios, se reflejará de la siguiente manera introduciendo 1234
como valor del productId
en http://127.0.0.1:5173/product/1234/details/
.
Para poder obtener el valor del parámetro de ruta en la página principal del producto (src/routes/product/[productId]/index.tsx
) debemos de aplicar también el uso de useLocation()
y hacer lo mismo.
El código es el siguiente:
import { component$ } from '@builder.io/qwik';
import { useLocation } from '@builder.io/qwik-city';
export default component$(() => {
// Información acerca de la ubicación respecto a la navegación
const location = useLocation();
return (
<div>
<div>Product Item [productId] - Principal Page</div>
<p>Href: {location.url.href}</p>
<p>Pathname: {location.url.pathname}</p>
<p>Product Id: {location.params.productId}</p>
</div>
);
});
Esto lo estamos aplicando para afianzar lo aprendido y a la vez el resultado que debe de mostrar sería la siguiente información:
Aplicando lo anterior hemos conseguido tener en todo momento localizada la información de navegación sobre el recurso que estamos cargando con la URL pública seleccionada.
Abriendo el proyecto 02-02-routing-advanced podemos encontrar los siguientes cambios:
El enlace lo tenéis a continuación:
https://shorten-up.vercel.app/TsM4Nnv9_x
Vamos a aprender a continuación como añadir las rutas comodin de un directorio concreto, para facilitar más si sabe nuestro trabajo definiendo este apartado.
Además de parametrizar las rutas de un modo sencillo, tenemos la posibilidad de crear nuestras rutas comodín agregando un directorio como [...productIds]
(respecto al punto anterior se añaden ...
delante de la propiedad del parámetro que en este caso hemos definido como productIds
).
Esto va a funcionar como el ejemplo descrito anteriormente, pero también considerará sub-apartados del URI.
Antes de crear el nuevo directorio, eliminamos la carpeta [productId]
dentro del directorio src/routes/product
.
Ahora que ya hemos eliminado el rastro de la carpeta [productId]
añadimos la correspondiente para hacer uso de las rutas comodín llamada [...productIds]
.
src/
└── routes/
└── product/
└── [...productIds]
└── index.tsx
Y dentro del fichero index.tsx
añadimos el siguiente contenido, donde en el apartado de productIds
obtenemos un string concatenado con todos los ids
de productos que para mostrarlo como string, lo convertiremos mediante JSON.stringify()
:
import { component$ } from '@builder.io/qwik';
import { useLocation } from '@builder.io/qwik-city';
export default component$(() => {
const location = useLocation();
return (
<>
<div>Product Item [...productIds] - Principal Page</div>
<p>Href: {location.url.href}</p>
<p>Pathname: {location.url.pathname}</p>
<p>Product Id: {JSON.stringify(location.params.productIds)}</p>
</>
);
});
Usando la configuración establecida, ejecutamos varias url y conseguimos este resultado siguiendo con el uso del hook useLocation()
:
http://127.0.0.1:5173/product/203
http://127.0.0.1:5173/product/203/204/205/
Podríamos "extraer" esos ids
y mostrarlos de una manera más elegante, recorriendo el contenido, mediante la creación de un array con el objetivo de separar estos elementos mediante split
usando como parámetro "/"
:
import { component$ } from '@builder.io/qwik';
import { useLocation } from '@builder.io/qwik-city';
export default component$(() => {
const location = useLocation();
return (
<>
....
<p>Product Id:</p>{' '}
<ul>
{location.params.productIds.split('/').map((id, index) => {
return (id !== '') ? (
<li>
Product select ({index + 1}): {id}
</li>
): undefined
})}
</ul>
</>
);
});
Y el resultado, teniendo en cuenta las diferentes URLs públicas que ejecutaremos:
http://127.0.0.1:5173/product/203
http://127.0.0.1:5173/product/203/204/205/
¿Podemos especificar un límite para introducir los datos?
SI, es posible limitar la captura de los parámetros de ruta siempre y cuando añadamos un directorio que lo limite.
Para especificar este límite debemos de añadir un nuevo directorio (ó más de uno) con un nombre descriptivo de la página que limitará esa captura de valores de la ruta.
Por ejemplo, añadimos un directorio llamado details
dentro de [...productIds]
, en el que vamos a obtener todos los ids
anteriores y los mostramos de una manera detallada tal y como se hace en la página principal.
src/
└── routes/
└── product/
└── [...productIds]/
|── index.tsx
└── details/
└──index.tsx
Y este será el código que definiremos en este nuevo fichero, cuyo objetivo será diferenciarlo respecto al index.tsx
anterior que sería la página principal (src/routes/product/[...productIds]/index.tsx
):
import { component$ } from '@builder.io/qwik';
import { type DocumentHead, useLocation } from '@builder.io/qwik-city';
export default component$(() => {
const location = useLocation();
return (
<>
<div>Product - Page Details</div>
<p>Href: {location.url.href}</p>
<p>Pathname: {location.url.pathname}</p>
<p>Product Id:</p>{' '}
<ul>
{location.params.productIds.split('/').map((id, index) => {
return (id !== '') ? (
<li>
Producto seleccionado ({index + 1}): {id}
</li>
): undefined
})}
</ul>
</>
);
});
La configurado en este nuevo fichero dará opción a capturar esta ruta, limitada por details
:
http://127.0.0.1:5173/product/203/204/205/details/
Extrayendo únicamente 203/204/205
como los valores de los ids
de los productos. ¿Y cuál es el motivo para no mostrar details
?
No se muestra details
por considerarlo el final de la ruta y no tiene en cuenta ese valor utilizando el fichero que hemos creado en la siguiente ruta: src/routes/product/[...productIds]/details/index.tsx
.
Para considerar ese valor, tendríamos que haber añadido información después del valor .../details/<VALOR NUEVO>
.
Sabiendo lo siguiente, el resultado que obtenemos es este:
En cambio, si añadimos 2333
(o cualquier otro dato) después de details
cuya URL pública será:
http://127.0.0.1:5173/product/203/204/205/details/2333
Extraerá 203/204/205/details/2333
como los valores de los ids
de los productos ya que details
NO se ha especificado como final de la ruta a la hora de la introducción de la URL.
Por ese motivo no usa el fichero index.tsx
que se encuentra en la ruta src/routes/product/[...productIds]/details/index.tsx
.
El fichero que utilizará se encuentra en la ruta src/routes/product/[...productIds]/index.tsx
, mostrándose de la siguiente manera, como se ha especificado anteriormente:
¿Qué pensáis que ocurrirá si introducimos la siguiente URL?http://127.0.0.1:5173/product/203/204/205/details/2333/details
Antes de ejecutar nada, repasar lo aprendido y seguramente ya tengáis la respuesta correcta.
Quizás os suene todo esto un poco confuso, y por ese motivo os animo a que practiquéis añadiendo diferentes variantes en los parámetros y viendo resultados.
Siempre es bueno parar un poco, practicar lo aprendido hasta el momento y asimilar esos conceptos.
Abriendo el proyecto 02-03-routing-wildcard podemos encontrar los siguientes cambios:
El enlace lo tenéis a continuación:
https://shorten-up.vercel.app/lwue-5bSaf
En algunos casos, puede surgir la necesidad de mover nuestras carpetas, que definen esas rutas, a un subdirectorio sin que afecte el nombre de la ruta que va a generar Qwik City
en función de la estructura de carpetas, para poder tener más orden.
Puede darse el caso que tengamos varias rutas en el mismo nivel que deberían usar diferentes diseños como puede ser parte pública y parte privada por poner un ejemplo conocidos por todo el mundo.
Tanto para la primera situación o la segunda, podemos crear directorios entre paréntesis (p. ej. (products)
) que no afectará a las rutas originales.
Lo vamos a reflejar de la siguiente manera:
src/
└── routes/
└── (products)/
├── detail/
| └── index.tsx
└── preview/
└── index.tsx
Añadimos un contenido a cada uno de los ficheros index.tsx
para poder visualizarlo:
src/routes/(products)/detail/index.tsx
: Product Detailsrc/routes/(products)/preview/index.tsx
: Product PreviewRealizamos las pruebas correspondientes, especificando una URL pública ignorando completamente el directorio (products)
:
http://127.0.0.1:5173/detail
http://127.0.0.1:5173/preview
Como se ha mencionado products
NO SERÁ parte del pathname.
¿Qué pasará si pongo products
como parte de la URL pública? Probemos con el primero de los casos (SPOILER: al segundo le va a pasar lo mismo).
http://127.0.0.1:5173/products/detail
Abriendo el proyecto 02-04-routing-grouped podemos encontrar la siguiente novedad que nos será super útil en el Capítulo 4 - Layouts / Plantilas:
El enlace lo tenéis a continuación:
https://shorten-up.vercel.app/W7KPeoWoNM
Al finalizar este capítulo, deberíamos de ser capaces de:
Hemos podido aprender las diferentes formas que tenemos para gestionar las rutas y como obtener la información de los parámetros de rutas, desde lo más sencillo a conceptos más avanzados.
Con lo aprendido hasta ahora, podemos pasar al siguiente capítulo donde nos faltaría ver los aspectos de las plantillas anidadas, donde podremos construir diferentes plantillas en base a la ruta, como por ejemplo zona privada y pública y también el enrutamiento en base a los menús. Esto lo veremos a continuación.
Antes de eso, vamos a trabajar con los componentes, un concepto fundamental tanto en Qwik como React y tecnologías similares.