Paginacija
Paginacija može biti iznenađujuće kompleksna tema. Lako je upasti u zamke i ne pratiti najbolje prakse. Ova stranica će vam pomoći da radite paginaciju „kako treba“. To znači, ako pročitate i razumete ovu stranicu, mislimo da će vaš klijent biti robusniji, spremniji za budućnost i olakšaće vam život kasnije.
Ako iz ovog vodiča zapamtite samo jednu stvar, neka to bude da ne treba da pravite svoje paginacione URL-ove.
Svaki paginirani odgovor iz JSON:API modula već sadrži link ka sledećoj strani kolekcije koji treba da koristite. Treba da pratite taj link.
Na početku ovog dokumenta pogledaćemo neke važne karakteristike API-ja i kako da implementirate paginaciju „kako treba“. Na kraju dokumenta naći ćete i neka odgovore na česta pitanja i potencijalne probleme.
Kako?
Svaki paginirani odgovor iz JSON:API modula ima paginacione linkove ugrađene u sebe. Pogledajmo jedan mali primer:
{
"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"
}
}
Obratimo pažnju na nekoliko stvari:
- Postoje 3 paginaciona linka pod ključem
links
:self
: ovo je URL za trenutnu stranicu.next
: ovo je URL za sledeću stranicu.prev
: ovo je URL za prethodnu stranicu.
- Postoji
page[limit]
od 3, ali postoje samo 2 resursa (?!)
Prisustvo ili odsustvo paginacionih linkova je značajno. Treba da znate:
- Ako postoji
next
link, postoje još stranica. - Ako
next
link ne postoji, na poslednjoj ste stranici. - Ako postoji
prev
link, niste na prvoj stranici. - Ako ne postoji ni
next
niprev
link, postoji samo jedna stranica.
Čak iako postoji limit stranice od 3, ima samo 2 resursa! To je zato što je jedan entitet uklonjen iz bezbednosnih razloga. Možemo zaključiti da nije zato što nema dovoljno resursa da popune odgovor, jer možemo da vidimo da postoji next
link. Ako želite više informacija o ovome, detaljnije je objašnjeno ispod.
Dobro, sada kada smo utvrdili neke važne činjenice, hajde da razmislimo kako treba izgraditi klijenta. Pogledaćemo malo pseudo-JavaScript koda da pomogne. 🧐
Zamislite da želite da prikažete listu najnovijeg sadržaja na našem sajtu i imamo neki „premium“ sadržaj. Samo pretplatnici bi trebalo da vide premium sadržaj. Takođe, odlučili smo da želimo komponentu „top 5“, ali ako postoji više sadržaja, korisnik bi trebalo da može da klikne na link „sledeća stranica“ da vidi narednih 5 najnovijih sadržaja.
Naivna implementacija može izgledati ovako:
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);
Ipak, čak i ako zanemarimo loše rukovanje greškama, već znamo da ovo nije robusna implementacija.
Videli smo gore da ne možemo biti sigurni da će odgovor imati 5 stavki. Ako 2 od tih entiteta nisu dostupna (možda su neobjavljeni), naša „top 5“ komponenta će imati samo 3 stavke!
Takođe imamo nepotreban filter. Server bi već trebalo da uklanja sadržaj koji korisnik ne sme da vidi. Ako to nije slučaj, postoji potencijalna rupa u pristupu jer bi zlonamerni korisnik lako mogao da izmeni upit da vidi „premium“ sadržaj. Uvek osigurajte da se kontrola pristupa sprovodi na serveru; ne verujte da vaši upiti to rade za vas.
Hajde da to popravimo:
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}`)
Prvo, možete videti da je filter
nestao. To je zato što pretpostavljamo da se provera pristupa vrši na serveru umesto da se oslanjamo na filter. Ovo je jedino sigurno rešenje. Mogli bismo da ga vratimo kao optimizaciju performansi, ali verovatno nije potrebno.
Zatim, pošto znamo da server jednostavno uklanja resurse koji nisu dostupni korisniku, zaista treba da proverimo koliko resursa zaista ima u odgovoru.
U „naivnoj“ implementaciji pretpostavljali smo da će svaki odgovor imati 5 stavki. U ovom primeru sada postavljamo „kvotu“ od 5 resursa. Nakon što napravimo zahteve, proveravamo da li smo ispunili kvotu ili ne. Takođe proveravamo da li server ima još stranica (to znamo ako postoji next
link, sećate se?).
Ako nismo ispunili kvotu i nismo na poslednjoj stranici, pravimo još jedan zahtev koristeći next
link koji smo izvukli iz dokumenta. Važno je napomenuti da nismo sami konstruisali novi URL za sledeću stranicu. Nema potrebe da izmišljate točak iznova jer je JSON:API server to već uradio za nas!
Još jedna zanimljiva stvar je što, pošto je fetch
asinhron, možemo dodati sadržaj prvog zahteva u našu komponentu čak i pre nego što su svi zahtevi izvršeni. Kada drugi zahtev završi, samo ažuriramo komponentu da uključi i novo preuzete rezultate.
Na kraju, osiguravamo da naša fiktivna listComponent
zna da li treba ili ne treba da prikaže link „sledeća stranica“. Treba da prikaže link samo ako već imamo dodatni sadržaj ili ako server ima još stranica.
Prvi slučaj može se desiti ako dobijemo samo 4 stavke u prvom zahtevu, a u drugom dobijemo 5 stavki ali nema next
linka. U tom slučaju imaćemo ukupno 9 stavki, ali naša listComponent
će prikazivati samo prvih 5. Tako da i dalje želimo da prikažemo link „sledeća stranica“ na komponenti ali ne želimo da naša komponenta zapravo šalje dodatne zahteve. Da bismo to naznačili, postavljamo nextPageLink
na null
.
U drugom slučaju — kada imamo next
link — prosleđujemo taj link za sledeću stranicu našoj komponenti kako bi mogla da ga koristi za sledeći zahtev. Ne želimo da pravimo taj zahtev ako korisnik nikad ne klikne na „sledeća stranica“, zar ne?
Poslednjih nekoliko pasusa ilustruje veoma važan koncept... linkovi „sledeća stranica“ u vašem HTML-u ne moraju da budu povezani sa API stranicama! U stvari, to je znak da možda radite nešto „pogrešno“ ako jesu.
Zašto ... ?
... ne mogu da postavim limit stranice veći od 50?
Prvo, pročitajte primer iznad. Razumite da JSON:API mora da izvrši pojedinačne provere pristupa za svaki entitet u odgovoru. Drugo, JSON:API modul je zamišljen da bude „bez konfiguracije“. Ne bi trebalo da instalirate, menjate ili podešavate bilo šta da biste koristili modul.
Razlog za ovo je zaštita vaše aplikacije od DDoS napada. Ako bi zlonamerni API klijent postavio limit stranice od 200.000 resursa, JSON:API modul bi morao da izvrši proveru pristupa za svaki od tih entiteta. To bi brzo dovelo do grešaka sa memorijom i sporih odgovora. Server mora da postavi maksimum. Limit od 50 je prilično proizvoljno izabran kao lep okrugao broj.
Razumite da se oko ove odluke vodilo mnogo dugih razgovora i da je morao biti napravljen kompromis između opterećenja podrške, razumnih podrazumevanih vrednosti i performansi frontenda. Iako održavači JSON:API modula razumeju da ovo možda nije idealno za svaki slučaj, sigurni su da ako vaš klijent sledi preporuke iz ove dokumentacije, ovo neće imati nikakav ili vrlo mali uticaj na vas :)
Ako i dalje želite veći limit, možete koristiti JSON:API Page Limit modul.
... zar nema X resursa u odgovoru?
JSON:API modul vam omogućava da navedete limit stranice, ali to se često pogrešno tumači kao garancija da će određeni broj resursa biti uključen u odgovor. Na primer, možda znate da ima dovoljno resursa da „popune“ odgovor, ali odgovor nema onoliko resursa koliko ste očekivali.
Iz mnogih gore navedenih razloga, JSON:API pokreće upit ka bazi samo za onoliko stavki koliko je navedeno u page[limit]
parametru upita. To je samo maksimum. Ako pristup nekim resursima iz rezultata upita nije dozvoljen, ti resursi će biti uklonjeni iz odgovora. U tom slučaju ćete videti manje resursa nego što ste očekivali.
Ovo je veoma uobičajeno kada šaljete zahtev za entitete koji možda nisu objavljeni (kao što su čvorovi) i ti entiteti nisu već filtrirani putem filter
parametra upita.
Članak sa Drupal Dokumentacije.