Paginazione
La paginazione può essere un argomento ingannevolmente complesso. È facile cadere in trappole e non seguire le buone pratiche. Questa pagina ti aiuterà a fare la paginazione "nel modo giusto". Cioè, se leggi e comprendi questa pagina, pensiamo che il tuo client sarà più robusto, a prova di futuro e renderà la tua vita più semplice in seguito.
Se c’è solo una cosa che devi portare con te da questa guida, è che non dovresti costruire manualmente i tuoi URL di paginazione.
Ogni risposta paginata dal modulo JSON:API contiene già un link alla pagina successiva di una collezione, pronto da usare. Devi seguire quel link.
All’inizio di questo documento, esamineremo alcune caratteristiche importanti dell’API e come implementare la paginazione "correttamente". Alla fine di questo documento troverai alcune risposte a domande comuni e insidie.
Come?
Ogni risposta paginata dal modulo JSON:API contiene link di paginazione già integrati. Guardiamo un piccolo esempio:
{
"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"
}
}
Annotiamo alcune cose:
- Ci sono 3 link di paginazione sotto la chiave
links
:self
: questo è l’URL della pagina corrente.next
: questo è l’URL della pagina successiva.prev
: questo è l’URL della pagina precedente.
- C’è un
page[limit]
di 3, ma ci sono solo 2 risorse (?!)
La presenza o assenza dei link di paginazione è significativa. Devi sapere:
- Se il link
next
esiste, ci sono altre pagine. - Se il link
next
non esiste, sei all’ultima pagina. - Se il link
prev
esiste, non sei alla prima pagina. - Se non esiste né un link
next
néprev
, c’è solo una pagina.
Anche se c’è un limite di pagina di 3, ci sono solo 2 risorse! Questo perché un’entità è stata rimossa per motivi di sicurezza. Possiamo dire che non è dovuto al fatto che non ci siano abbastanza risorse per riempire la risposta, perché possiamo vedere che c’è un link next
. Se vuoi saperne di più su questo, è spiegato più in dettaglio qui sotto.
Ok, ora che abbiamo stabilito alcuni fatti importanti. Pensiamo a come dovremmo costruire il nostro client. Vedremo un po’ di pseudo-JavaScript come aiuto. 🧐
Immagina di voler mostrare un elenco dei contenuti più recenti sul nostro sito e che abbiamo alcuni contenuti "premium". Solo gli abbonati paganti dovrebbero poter vedere i contenuti premium. Abbiamo anche deciso di voler un componente "top 5", tuttavia, se esistono più contenuti, l’utente dovrebbe poter cliccare un link "pagina successiva" per vedere i 5 contenuti più recenti successivi.
Una implementazione ingenua potrebbe sembrare qualcosa del genere:
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);
Tuttavia, anche ignorando la pessima gestione degli errori, sappiamo già che questa non è un’implementazione molto robusta.
Abbiamo visto sopra che non possiamo essere sicuri che una risposta conterrà 5 elementi. Se 2 di queste entità non sono accessibili (magari sono non pubblicate), allora il nostro componente "top 5" avrà solo 3 elementi!
Abbiamo anche un filtro non necessario. Il server dovrebbe già rimuovere i contenuti che l’utente non è autorizzato a vedere. In caso contrario, avremmo un potenziale bypass di accesso nella nostra applicazione, perché un utente malintenzionato potrebbe facilmente modificare la query per vedere i contenuti "premium". Assicurati sempre di applicare i controlli di accesso sul server; non fidarti delle tue query per farlo al posto tuo.
Sistemiamolo:
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}`)
Per prima cosa, puoi vedere che il filter
è sparito. Questo perché stiamo assumendo che i controlli di accesso vengano eseguiti sul server invece di fare affidamento su un filtro. Questa è l’unica soluzione sicura. Potremmo aggiungerlo di nuovo come ottimizzazione delle prestazioni, ma probabilmente non è necessario.
In seguito, poiché sappiamo che il server rimuove semplicemente le risorse non accessibili all’utente, dobbiamo davvero controllare quante risorse sono effettivamente presenti nella risposta.
Nell’implementazione "ingenua", presumevamo che ogni risposta avrebbe avuto 5 elementi. In questo esempio, ora impostiamo una "quota" di 5 risorse. Dopo aver effettuato le richieste, verifichiamo se abbiamo raggiunto la quota o meno. Controlliamo anche se il server ha ancora altre pagine (lo sapremo perché avrà un link next
, ricordi?).
Se non abbiamo raggiunto la quota e non siamo all’ultima pagina, effettuiamo un’altra richiesta utilizzando il link next
che abbiamo estratto dal documento. È importante notare che non abbiamo costruito manualmente un nuovo URL per la pagina successiva. Non c’è bisogno di reinventare la ruota perché il server JSON:API lo ha già fatto per noi!
Un’altra cosa interessante da notare è che, poiché fetch
è asincrono, possiamo aggiungere il contenuto della prima richiesta al nostro componente anche prima che tutte le richieste siano state completate. Quando la seconda richiesta termina, aggiorniamo semplicemente di nuovo il componente in modo che includa i nuovi risultati recuperati.
Infine, ci assicuriamo che il nostro fittizio listComponent
sappia se deve mostrare o meno un link "pagina successiva". Dovrebbe mostrarlo solo se abbiamo già contenuti extra oppure se il server ha altre pagine.
Il primo caso può verificarsi se riceviamo solo 4 elementi nella prima richiesta e nella seconda ne otteniamo 5 ma nessun link next
. In quel caso, avremo un totale di 9 elementi ma il nostro listComponent
mostrerà solo i primi 5. Quindi vogliamo comunque mostrare un link "pagina successiva" sul componente ma non vogliamo che il nostro componente invii effettivamente altre richieste. Per indicarlo, impostiamo nextPageLink
a null
.
Nel secondo caso—quando abbiamo un link next
—passiamo quel link della pagina successiva al nostro componente affinché possa usarlo per fare una richiesta successiva. Non vogliamo fare quella richiesta se l’utente non clicca mai il link "pagina successiva", giusto?
Gli ultimi paragrafi illustrano un concetto davvero importante... i link "pagina successiva" nel tuo HTML non devono essere correlati alle pagine dell’API! Anzi, è un’indicazione che potresti star facendo qualcosa "sbagliato" se lo sono.
Perché ... ?
... non posso impostare un limite di pagina superiore a 50?
Per prima cosa, leggi l’esempio fornito sopra. Comprendi che JSON:API deve eseguire controlli di accesso individuali per ogni entità in una risposta. In secondo luogo, comprendi che il modulo JSON:API mira a essere "zero configurazione". Non dovresti dover installare, modificare o configurare nulla per usare il modulo.
Il motivo è proteggere la tua applicazione da un attacco DDoS. Se un client API malevolo impostasse un limite di pagina di 200.000 risorse, il modulo JSON:API dovrebbe eseguire controlli di accesso per ognuna di quelle entità. Questo porterebbe rapidamente a errori di memoria insufficiente e risposte lente. Il server ha bisogno di impostare un massimo. Il limite di 50 è stato scelto in modo un po’ arbitrario come un numero rotondo ragionevole.
Per favore, comprendi che ci sono state molte lunghe conversazioni su questa decisione e si è dovuto trovare un compromesso tra carico di supporto, valori predefiniti sensati e prestazioni del frontend. Anche se i manutentori del modulo JSON:API capiscono che questo potrebbe non essere ideale per ogni caso d’uso, sono fiduciosi che se il tuo client segue le raccomandazioni in questa documentazione, non dovrebbe avere alcun impatto su di te :)
Se vuoi comunque un limite più alto, puoi usare il modulo JSON:API Page Limit.
... non ci sono X risorse nella risposta?
Il modulo JSON:API ti permette di specificare un limite di pagina, che spesso viene frainteso come una garanzia che un certo numero di risorse sarà incluso in una risposta. Per esempio, potresti sapere che ci sono risorse sufficienti disponibili per "riempire" una risposta, ma la risposta non ha tante risorse quante ti aspettavi.
Per molti degli stessi motivi indicati sopra, JSON:API esegue solo una query sul database per il numero di elementi specificato dal parametro di query page[limit]
. È solo un massimo. Se l’accesso a alcune delle risorse nel risultato della query non è consentito, quelle risorse verranno rimosse dalla risposta. In quel caso, vedrai meno risorse di quante ti aspettavi.
Questo è abbastanza comune quando si effettua una richiesta per entità che potrebbero non essere pubblicate (come i nodi) e tali entità non sono già state filtrate utilizzando il parametro di query filter
.
Articolo da Documentazione di Drupal.