Paginering
Paginering kan een bedrieglijk complex onderwerp zijn. Het is makkelijk om in valkuilen te trappen en best practices niet te volgen. Deze pagina helpt je om paginering op de “juiste” manier te doen. Dat wil zeggen: als je deze pagina leest en begrijpt, zal je client robuuster en toekomstbestendiger zijn en maak je je eigen leven later makkelijker.
Als je slechts één ding onthoudt van deze gids, laat het dan dit zijn: je moet je eigen paginerings-URL’s niet zelf construeren.
Elke gepagineerde response van de JSON:API-module heeft al een link naar de volgende pagina van een collectie ingebouwd die je kunt gebruiken. Je moet die link volgen.
Aan het begin van dit document bekijken we enkele belangrijke functies van de API en hoe je paginering op de “juiste” manier implementeert. Aan het einde van dit document vind je enkele antwoorden op veelgestelde vragen en valkuilen.
Hoe?
Elke gepagineerde response van de JSON:API-module heeft pagineringslinks ingebouwd. Laten we naar een klein voorbeeld kijken:
{
"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"
}
}
Laten we enkele zaken noteren:
- Er zijn 3 pagineringslinks onder de
links
-sleutel:self
: dit is de URL voor de huidige pagina.next
: dit is de URL voor de volgende pagina.prev
: dit is de URL voor de vorige pagina.
- Er is een
page[limit]
van 3, maar er zijn slechts 2 resources (?!)
De aanwezigheid of afwezigheid van de pagineringslinks is belangrijk. Je moet weten:
- Als de
next
-link bestaat, zijn er meer pagina’s. - Als de
next
-link niet bestaat, bevindt je je op de laatste pagina. - Als de
prev
-link bestaat, ben je niet op de eerste pagina. - Als er noch een
next
noch eenprev
-link bestaat, is er slechts één pagina.
Ondanks dat er een paginalimiet van 3 is, zijn er slechts 2 resources! Dit komt doordat een entiteit om veiligheidsredenen is verwijderd. We kunnen zien dat dit niet komt doordat er onvoldoende resources zijn om de response te vullen, want we zien dat er een next
-link aanwezig is. Wil je hier meer over weten, dit wordt hieronder uitgebreider uitgelegd.
Oké, nu we enkele belangrijke feiten hebben vastgesteld. Laten we nadenken over hoe we onze client zouden moeten bouwen. We bekijken wat pseudo-JavaScript als voorbeeld. 🧐
Stel dat je een overzicht wilt tonen van de nieuwste content op onze site en dat we enkele “premium”-content hebben. Alleen betalende abonnees mogen premium-content zien. We hebben ook besloten dat we een “top 5”-component willen, maar als er meer content is, moet de gebruiker op een “volgende pagina”-link kunnen klikken om de volgende 5 nieuwste items te zien.
Een naïeve implementatie zou er ongeveer zo uitzien:
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);
Maar zelfs zonder rekening te houden met de slechte foutafhandeling, weten we al dat dit geen robuuste implementatie is.
We hebben hierboven gezien dat we er niet zeker van kunnen zijn dat een response 5 items zal hebben. Als 2 van die entiteiten niet toegankelijk zijn (bijvoorbeeld omdat ze niet gepubliceerd zijn), zal onze “top 5”-component slechts 3 items hebben!
We hebben ook een onnodige filter. De server zou al content moeten verwijderen waar de gebruiker geen toegang tot heeft. Zo niet, dan zouden we een potentiële toegangs-omzeiling hebben in onze applicatie, omdat een kwaadwillende gebruiker eenvoudig de query zou kunnen aanpassen om de “premium”-content te zien. Zorg er altijd voor dat je toegangscontrole afdwingt op de server; vertrouw niet op je queries om dat voor je te doen.
Laten we dit oplossen:
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}`)
Ten eerste zie je dat de filter
verdwenen is. Dat komt omdat we ervan uitgaan dat toegangscontroles op de server worden uitgevoerd in plaats van te vertrouwen op een filter. Dit is de enige veilige oplossing. We zouden het kunnen toevoegen als prestatie-optimalisatie, maar dat is waarschijnlijk niet nodig.
Verder, aangezien we weten dat de server resources verwijdert die niet toegankelijk zijn voor de gebruiker, moeten we echt controleren hoeveel resources er daadwerkelijk in de response zitten.
In de “naïeve” implementatie gingen we ervan uit dat elke response 5 items zou hebben. In dit voorbeeld stellen we nu een “quota” van 5 resources in. Nadat we onze requests hebben uitgevoerd, controleren we of we ons quota hebben bereikt of niet. We controleren ook of de server nog meer pagina’s heeft (dat weten we omdat er een next
-link is, weet je nog?).
Als we het quota niet hebben bereikt en we zijn niet op de laatste pagina, doen we een nieuw verzoek met de next
-link die we uit het document hebben gehaald. Het is belangrijk om te zien dat we de URL voor de volgende pagina niet handmatig hebben geconstrueerd. Dat hoeft niet, omdat de JSON:API-server dit al voor ons gedaan heeft!
Een ander interessant punt is dat omdat fetch
asynchroon is, we de content van het eerste verzoek al aan onze component kunnen toevoegen, nog voordat alle verzoeken voltooid zijn. Wanneer het tweede verzoek klaar is, werken we de component opnieuw bij zodat de nieuwe resultaten ook worden weergegeven.
Tot slot zorgen we ervoor dat onze fictieve listComponent
weet of er een “volgende pagina”-link getoond moet worden of niet. Die mag alleen getoond worden als we al extra content hebben of als de server extra pagina’s heeft.
Het eerste geval kan zich voordoen als we in het eerste verzoek slechts 4 items ontvangen en in het tweede verzoek 5 items maar geen next
-link. In dat geval hebben we in totaal 9 items, maar onze listComponent
zal er slechts 5 tonen. Dus we willen nog steeds een “volgende pagina”-link tonen, maar we willen niet dat onze component nog meer requests uitvoert. Om dat aan te geven, stellen we nextPageLink
in op null
.
In het tweede geval—wanneer we wel een next
-link hebben—geven we die volgende paginalink door aan onze component zodat die een volgend verzoek kan doen. We willen dat verzoek niet uitvoeren als de gebruiker nooit op de “volgende pagina”-link klikt, toch?
De laatste paragrafen illustreren een heel belangrijk concept... “volgende pagina”-links in je HTML hoeven niet overeen te komen met API-pagina’s! Sterker nog, het kan erop wijzen dat je het “verkeerd” doet als ze dat wel doen.
Waarom ... ?
... kan ik geen paginalimiet hoger dan 50 instellen?
Lees eerst het bovenstaande voorbeeld. Begrijp dat JSON:API voor elke entiteit in een response toegangscontroles moet uitvoeren. Begrijp ook dat de JSON:API-module “zero configuration” wil zijn. Je zou niets moeten hoeven installeren, aanpassen of configureren om de module te gebruiken.
De reden hiervoor is om je applicatie te beschermen tegen een DDoS-aanval. Als een kwaadwillende API-client een paginalimiet van 200.000 resources zou instellen, zou de JSON:API-module toegangscontroles moeten uitvoeren voor al die entiteiten. Dit zou snel leiden tot geheugenfouten en trage responses. De server moet dus een maximum instellen. De limiet van 50 is enigszins arbitrair gekozen als een mooi rond getal.
Begrijp alsjeblieft dat er veel lange gesprekken zijn gevoerd over deze beslissing en dat er een compromis moest worden gemaakt tussen beheerslast, verstandige standaarden en frontend-prestaties. Hoewel de JSON:API-modulebeheerders weten dat dit misschien niet ideaal is voor elk gebruiksscenario, zijn ze ervan overtuigd dat als je client de aanbevelingen in deze documentatie volgt, dit weinig tot geen impact op je zal hebben :)
Wil je toch een hogere limiet, dan kun je de JSON:API Page Limit-module gebruiken.
... staan er niet X aantal resources in de response?
De JSON:API-module laat je een paginalimiet (limit) opgeven. Dit wordt vaak verkeerd begrepen als een garantie dat een bepaald aantal resources in een response zal zitten. Bijvoorbeeld: je weet misschien dat er voldoende resources beschikbaar zijn om een response te “vullen”, maar de response bevat toch minder resources dan je had verwacht.
Om veel van dezelfde redenen die hierboven zijn beschreven, voert JSON:API slechts een databasequery uit voor het aantal items dat door de page[limit]
-queryparameter is opgegeven. Dit is slechts een maximum. Als toegang tot sommige resources in het queryresultaat niet is toegestaan, worden die resources verwijderd uit de response. In dat geval zie je minder resources dan je misschien had verwacht.
Dit komt vaak voor wanneer je een verzoek doet voor entiteiten die ongepubliceerd kunnen zijn (zoals nodes) en die entiteiten nog niet gefilterd zijn met de filter
-queryparameter.
Artikel van Drupal Documentatie.