Paginación
La paginación puede ser un tema sorprendentemente complejo. Es fácil caer en trampas y no seguir las mejores prácticas. Esta página te ayudará a hacer la paginación “correctamente”. Es decir, si lees y comprendes esta página, tu cliente será más robusto, preparado para el futuro y tu vida será más fácil a largo plazo.
Si solo debes recordar una cosa de esta guía, debería ser que no debes construir tus propias URLs de paginación.
Cada respuesta paginada del módulo JSON:API ya tiene un enlace a la siguiente página de una colección, listo para que lo uses. Debes seguir ese enlace.
Al inicio de este documento, revisaremos algunas características importantes de la API y cómo implementar correctamente la paginación. Al final, encontrarás algunas respuestas a preguntas y advertencias comunes.
¿Cómo?
Cada respuesta paginada del módulo JSON:API incluye enlaces de paginación. Veamos un pequeño ejemplo:
{
"data": [
{"type": "sample--type", "id": "abcd-uuid-here"},
{"type": "sample--type", "id": "efgh-uuid-here"}
],
"links": {
"self": "<collection_url>?page[offset]=3&page[limit]=3",
"next": "<collection_url>?page[offset]=6&page[limit]=3",
"prev": "<collection_url>?page[offset]=0&page[limit]=3"
}
}
Observa lo siguiente:
- Hay 3 enlaces de paginación bajo la clave
links
:self
: URL de la página actual.next
: URL de la siguiente página.prev
: URL de la página anterior.
- Hay un
page[limit]
de 3, pero solo hay 2 recursos (!?)
La presencia o ausencia de los enlaces de paginación es significativa. Debes saber:
- Si el enlace
next
existe, hay más páginas. - Si el enlace
next
no existe, estás en la última página. - Si el enlace
prev
existe, no estás en la primera página. - Si no existe
next
niprev
, solo hay una página.
Aunque hay un límite de página de 3, solo hay 2 recursos. Esto se debe a que se eliminó una entidad por razones de seguridad. Podemos saber que no es porque no hay suficientes recursos para llenar la respuesta ya que vemos que hay un enlace next
. Si quieres saber más sobre esto, se explica en detalle aquí.
Ahora que hemos establecido algunos hechos importantes, pensemos cómo construir nuestro cliente. Veamos algo de pseudo-JavaScript para ayudar. 🧐
Imagina que quieres mostrar una lista del contenido más nuevo en tu sitio y tienes algún contenido "premium". Solo los suscriptores pagos deben ver el contenido premium. También decidiste que quieres un componente “top 5”, pero si hay más contenido, el usuario debe poder hacer clic en un enlace “siguiente página” para ver los siguientes 5 elementos más recientes.
Una implementación ingenua podría verse así:
const baseUrl = 'http://example.com';
const path = '/jsonapi/node/content';
const pager = 'page[limit]=5';
const filter = `filter[field_premium][value]=${user.isSubscriber()}`;
fetch(`${baseUrl}${path}?${pager}&${filter}`)
.then(resp => {
return resp.ok ? resp.json() : Promise.reject(resp.statusText);
})
.then(document => listComponent.setContent(document.data))
.catch(console.log);
Sin embargo, ignorando el manejo de errores, ya sabemos que esta no es una implementación robusta.
No podemos estar seguros de que una respuesta tendrá 5 elementos. Si 2 de esas entidades no son accesibles (quizá están sin publicar), entonces nuestro componente “top 5” solo tendrá 3 elementos.
Además, el filtro es innecesario. El servidor ya debería estar eliminando el contenido al que el usuario no puede acceder. Si no, podríamos tener una brecha de acceso porque un usuario malicioso podría alterar fácilmente la consulta para ver el contenido “premium”. Asegúrate siempre de que el control de acceso se aplique en el servidor; no confíes en tus consultas para ello.
Corrijámoslo:
const listQuota = 5;
const content = [];
const baseUrl = 'http://example.com';
const path = '/jsonapi/node/content';
const pager = `page[limit]=${listQuota}`;
const getAndSetContent = (link) => {
fetch(link)
.then(resp => {
return resp.ok ? resp.json() : Promise.reject(resp.statusText);
})
.then(document => {
content.push(...document.data);
listContent.setContent(content.slice(0, listQuota));
const hasNextPage = document.links.hasOwnProperty("next");
if (content.length <= listQuota && hasNextPage) {
getAndSetContent(document.links.next);
}
if (content.length > listQuota || hasNextPage) {
const nextPageLink = hasNextPage
? document.links.next
: null;
listComponent.showNextPageLink(nextPageLink);
}
})
.catch(console.log);
}
getAndSetContent(`${baseUrl}${path}?${pager}`)
Primero, observa que el filter
ha desaparecido. Esto se debe a que asumimos que los controles de acceso se realizan en el servidor en vez de confiar en un filtro. Esta es la única solución segura. Podríamos agregarlo como optimización de rendimiento, pero probablemente no sea necesario.
Luego, como sabemos que el servidor elimina recursos no accesibles al usuario, necesitamos verificar cuántos recursos realmente hay en la respuesta.
En la implementación ingenua, asumíamos que cada respuesta tendría 5 elementos. En este ejemplo, establecemos una "cuota" de 5 recursos. Tras realizar nuestras solicitudes, verificamos si hemos alcanzado nuestra cuota o no. También verificamos si el servidor todavía tiene más páginas (lo sabremos porque tendrá un enlace next
, ¿recuerdas?).
Si no hemos alcanzado la cuota y no estamos en la última página, hacemos otra solicitud utilizando el enlace next
que extraemos del documento. Es importante notar que no construimos una nueva URL para la siguiente página manualmente. ¡No hay necesidad de reinventar la rueda porque el servidor JSON:API ya lo hizo por nosotros!
Otra cosa interesante es que como fetch
es asíncrono, podemos agregar el contenido de la primera solicitud a nuestro componente incluso antes de que se hayan realizado todas las solicitudes. Cuando termina la segunda solicitud, simplemente actualizamos el componente nuevamente para incluir los resultados recién obtenidos.
Finalmente, nos aseguramos de que nuestro ficticio listComponent
sepa si debe mostrar un enlace de "siguiente página". Solo debe mostrar el enlace si ya tenemos contenido extra o si el servidor tiene páginas adicionales.
El primer caso puede ocurrir si recibimos solo 4 elementos en la primera solicitud y en la segunda recibimos 5 elementos pero no hay enlace next
. En ese caso, tendremos un total de 9 elementos pero nuestro listComponent
solo mostrará los primeros 5. Así que aún queremos mostrar un enlace de "siguiente página" en el componente, pero no queremos que el componente realice más solicitudes. Para indicar eso, establecemos nextPageLink
a null
.
En el segundo caso—cuando sí hay enlace next
—le pasamos ese enlace a nuestro componente para que pueda usarlo en una solicitud posterior. No queremos hacer esa solicitud si el usuario nunca hace clic en el enlace “siguiente página”, ¿verdad?
Los últimos párrafos ilustran un concepto muy importante… ¡Los enlaces de “siguiente página” en tu HTML no necesitan correlacionarse con las páginas de la API! De hecho, es una señal de que podrías estar haciéndolo “mal” si lo están.
¿Por qué ...?
... no puedo establecer un límite de página mayor a 50?
Primero, lee el ejemplo anterior. Entiende que JSON:API debe ejecutar verificaciones de acceso individuales para cada entidad en una respuesta. Segundo, comprende que el módulo JSON:API busca ser de "cero configuración". No deberías tener que instalar, modificar o configurar nada para usar el módulo.
La razón de esto es proteger tu aplicación de un ataque DDoS. Si un cliente malicioso establece un límite de página de 200,000 recursos, el módulo JSON:API tendría que ejecutar verificaciones de acceso para cada una de esas entidades, lo que llevaría rápidamente a errores de memoria y respuestas lentas. El servidor necesita establecer un máximo. El límite de 50 se eligió como un número redondo razonable.
Ten en cuenta que ha habido muchas discusiones sobre esta decisión y se llegó a un compromiso entre carga de soporte, valores predeterminados razonables y rendimiento en frontend. Si tu cliente sigue las recomendaciones de esta documentación, debería tener poco o ningún impacto en ti :)
Si aún deseas un límite superior, puedes usar el módulo JSON:API Page Limit.
... ¿por qué no hay X recursos en la respuesta?
El módulo JSON:API te permite especificar un límite de página, que suele malinterpretarse como una garantía de que habrá cierta cantidad de recursos en la respuesta. Por ejemplo, podrías saber que hay suficientes recursos disponibles para "llenar" una respuesta, pero la respuesta no tiene tantos recursos como esperabas.
Por muchas de las razones mencionadas arriba, JSON:API solo ejecuta una consulta a la base de datos por el número de elementos especificado en el parámetro page[limit]
. Es solo un máximo. Si el acceso a algunos recursos del resultado de la consulta no está permitido, esos recursos se eliminan de la respuesta. Cuando eso ocurre, verás menos recursos de los que podrías haber esperado.
Esto es común cuando solicitas entidades que podrían estar sin publicar (como nodos) y esas entidades no se han filtrado usando el parámetro filter
.
Artículo de la Documentación de Drupal.