Pagination
La pagination peut être un sujet trompeusement complexe. Il est facile de tomber dans des pièges et de ne pas suivre les meilleures pratiques. Cette page vous aidera à faire la pagination "correctement". Autrement dit, si vous lisez et comprenez cette page, nous pensons que votre client sera plus robuste et pérenne, et que cela facilitera votre travail à l’avenir.
Si vous ne retenez qu’une seule chose de ce guide, c’est que vous ne devez pas construire vos propres URLs de pagination.
Toutes les réponses paginées du module JSON:API contiennent déjà un lien vers la page suivante d’une collection prêt à être utilisé. Vous devez suivre ce lien.
Au début de ce document, nous allons examiner quelques fonctionnalités importantes de l’API et comment implémenter la pagination "correctement". À la fin, vous trouverez des réponses aux questions courantes et pièges.
Comment ?
Chaque réponse paginée du module JSON:API contient des liens de pagination intégrés. Voici un petit exemple :
{
"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"
}
}
Remarquons quelques points :
- Il y a 3 liens de pagination sous la clé
links
:self
: URL de la page courante.next
: URL de la page suivante.prev
: URL de la page précédente.
- La limite de page
page[limit]
est de 3, mais il n’y a que 2 ressources (?!)
La présence ou l’absence des liens de pagination est importante. Il faut comprendre :
- Si le lien
next
existe, il y a plus de pages. - Si le lien
next
n’existe pas, vous êtes sur la dernière page. - Si le lien
prev
existe, vous n’êtes pas sur la première page. - Si aucun lien
next
niprev
n’existe, il n’y a qu’une seule page.
Même si la limite de page est de 3, il n’y a que 2 ressources ! C’est parce qu’une entité a été retirée pour des raisons de sécurité. On sait que ce n’est pas parce qu’il n’y a pas assez de ressources pour remplir la réponse, car il y a un lien next
. Pour en savoir plus, voyez la section explicative ci-dessous.
Maintenant que nous avons établi quelques faits importants, réfléchissons à la façon de construire notre client. Voici un exemple en pseudo-JavaScript pour vous aider. 🧐
Imaginons que vous voulez afficher une liste des contenus les plus récents sur notre site, avec du contenu "premium". Seuls les abonnés payants doivent voir ce contenu premium. Nous voulons aussi un composant "top 5", mais si plus de contenus existent, l’utilisateur doit pouvoir cliquer sur un lien "page suivante" pour voir les 5 contenus suivants.
Une implémentation naïve pourrait ressembler à ceci :
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);
Mais même en ignorant la mauvaise gestion des erreurs, ce n’est pas une implémentation très robuste.
Nous avons vu qu’on ne peut pas être sûr qu’une réponse contiendra 5 éléments. Si 2 entités ne sont pas accessibles (peut-être non publiées), notre composant "top 5" n’aura que 3 éléments !
Nous avons aussi un filtre inutile. Le serveur devrait déjà exclure le contenu que l’utilisateur n’a pas le droit de voir. Sinon, il y aurait une faille de sécurité car un utilisateur malveillant pourrait modifier la requête pour voir le contenu "premium". Assurez-vous toujours que le contrôle d’accès est effectué côté serveur ; ne vous fiez pas à vos requêtes pour cela.
Voici une version corrigée :
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}`)
Tout d’abord, vous voyez que le filter
a disparu. C’est parce que l’on suppose que les contrôles d’accès sont effectués côté serveur et non via un filtre. C’est la seule solution sécurisée. On pourrait le réintroduire comme optimisation, mais ce n’est probablement pas nécessaire.
Ensuite, puisque le serveur supprime les ressources non accessibles à l’utilisateur, on doit vraiment vérifier combien de ressources se trouvent réellement dans la réponse.
Dans l’implémentation naïve, on supposait que chaque réponse avait 5 éléments. Ici, on définit un quota de 5 ressources. Après chaque requête, on vérifie si le quota est atteint et si le serveur propose encore des pages suivantes (on le sait grâce au lien next
).
Si le quota n’est pas atteint et que ce n’est pas la dernière page, on effectue une autre requête en utilisant le lien next
extrait du document. Il est important de noter que nous ne construisons pas manuellement une URL pour la page suivante. Le serveur JSON:API l’a déjà fait pour nous !
Autre point intéressant : puisque fetch
est asynchrone, on peut ajouter le contenu de la première requête au composant avant que toutes les requêtes soient terminées. Quand la deuxième requête se termine, on met à jour le composant pour inclure les nouveaux résultats.
Enfin, on s’assure que notre composant fictif listComponent
sait s’il doit afficher un lien "page suivante". Il ne doit l’afficher que si on a déjà du contenu supplémentaire ou si le serveur propose d’autres pages.
Le premier cas arrive si la première requête renvoie 4 éléments, et la deuxième 5 éléments mais pas de lien next
. Dans ce cas, on a 9 éléments au total mais le composant n’en affiche que 5. On veut donc afficher un lien "page suivante" mais sans que le composant effectue de nouvelle requête. Pour cela, on met nextPageLink
à null
.
Dans le second cas — lorsqu’il y a un lien next
— on transmet ce lien au composant pour qu’il puisse effectuer une requête ultérieure. Pas besoin de faire cette requête si l’utilisateur ne clique jamais sur "page suivante".
Ces derniers paragraphes illustrent un concept très important... les liens "page suivante" dans votre HTML ne doivent pas nécessairement correspondre aux pages API ! En fait, c’est souvent un signe que vous faites les choses "mal" s’ils correspondent.
Pourquoi ... ?
... ne puis-je pas définir une limite de page supérieure à 50 ?
Premièrement, relisez l’exemple ci-dessus. Comprenez que JSON:API doit exécuter des contrôles d’accès individuels pour chaque entité dans une réponse. Deuxièmement, sachez que le module JSON:API vise à être « zéro configuration ». Vous ne devriez pas avoir à installer, modifier ou configurer quoi que ce soit pour utiliser le module.
La raison est de protéger votre application contre les attaques DDoS. Si un client API malveillant fixe une limite de page à 200 000 ressources, JSON:API devrait vérifier l’accès pour chacune de ces entités, ce qui entraînerait rapidement des erreurs de mémoire et des réponses lentes. Le serveur doit fixer un maximum. La limite de 50 a été choisie arbitrairement car c’est un nombre rond pratique.
Comprenez que ce choix a fait l’objet de nombreuses discussions et d’un compromis entre la charge de support, les valeurs par défaut raisonnables et les performances front-end. Même si les mainteneurs du module JSON:API savent que ce n’est pas idéal pour tous les cas d’usage, ils sont confiants que si votre client suit ces recommandations, l’impact sera minime voire nul :)
Si vous souhaitez une limite plus élevée, vous pouvez utiliser le module JSON:API Page Limit.
... pourquoi n’y a-t-il pas X ressources dans la réponse ?
Le module JSON:API vous permet de spécifier une limite de page, mais cela ne garantit pas qu’un certain nombre de ressources sera inclus dans la réponse. Par exemple, vous savez peut-être qu’il y a suffisamment de ressources pour « remplir » une réponse, mais la réponse n’en contient pas autant que prévu.
Pour les mêmes raisons que ci-dessus, JSON:API exécute une requête en base de données pour le nombre d’éléments indiqué par le paramètre page[limit]
. C’est une limite maximale. Si l’accès à certaines ressources dans le résultat n’est pas autorisé, ces ressources seront retirées de la réponse. Vous verrez donc moins de ressources que prévu.
Cela est fréquent quand vous demandez des entités pouvant être non publiées (comme des nœuds) et que ces entités n’ont pas été filtrées via le paramètre filter
.
Article extrait de la Documentation Drupal.