Xlera8

Memorarea în cache a datelor în SvelteKit

My anterior mesaj a fost o prezentare generală a SvelteKit, unde am văzut ce instrument grozav este pentru dezvoltarea web. Această postare va dezvălui ceea ce am făcut acolo și va aborda subiectul preferat al fiecărui dezvoltator: cache. Deci, asigurați-vă că citiți ultimul meu post dacă nu ați făcut-o deja. Codul pentru această postare este disponibil pe GitHub, precum și un demo live.

Această postare se referă la manipularea datelor. Vom adăuga o funcționalitate rudimentară de căutare care va modifica șirul de interogare a paginii (folosind caracteristicile SvelteKit încorporate) și vom reactiva încărcătorul paginii. Dar, mai degrabă decât să reinterogăm baza noastră de date (imaginară), vom adăuga ceva memorare în cache, astfel încât recăutarea căutărilor anterioare (sau folosind butonul Înapoi) va afișa rapid datele preluate anterior din cache. Vom analiza cum să controlăm perioada de timp în care datele din cache rămân valabile și, mai important, cum să invalidăm manual toate valorile din cache. Ca cireașă de pe tort, ne vom uita la modul în care putem actualiza manual datele de pe ecranul curent, din partea clientului, după o mutație, în timp ce purificăm memoria cache.

Aceasta va fi o postare mai lungă și mai dificilă decât majoritatea a ceea ce scriu de obicei, deoarece acoperim subiecte mai dificile. Această postare vă va arăta, în esență, cum să implementați caracteristicile comune ale utilităților de date populare, cum ar fi reacție-interogare; dar în loc să folosim o bibliotecă externă, vom folosi doar platforma web și funcțiile SvelteKit.

Din păcate, caracteristicile platformei web sunt un nivel puțin mai scăzut, așa că vom lucra mai mult decât ați fi obișnuit. Avantajul este că nu vom avea nevoie de biblioteci externe, ceea ce va ajuta să menținem dimensiunile pachetelor frumoase și mici. Vă rugăm să nu folosiți abordările pe care vi le voi arăta decât dacă aveți un motiv întemeiat. Memorarea în cache este ușor de greșit și, după cum veți vedea, există un pic de complexitate care va avea ca rezultat codul aplicației dvs. Sperăm că depozitul dvs. de date este rapid, iar interfața dvs. de utilizare este în regulă, permițând lui SvelteKit să solicite întotdeauna datele de care are nevoie pentru orice pagină dată. Dacă este, lăsați-o în pace. Bucurați-vă de simplitate. Dar această postare vă va arăta câteva trucuri pentru când nu mai este cazul.

Vorbind de interogare de reacție, asta tocmai a fost eliberat pentru Svelte! Deci, dacă vă aflați că vă sprijiniți pe tehnicile manuale de stocare în cache mult, asigurați-vă că verificați proiectul respectiv și vedeți dacă vă poate ajuta.

Configurare

Înainte de a începe, să facem câteva mici modificări codul pe care îl aveam înainte. Acest lucru ne va oferi o scuză pentru a vedea alte câteva funcții SvelteKit și, mai important, ne va pregăti pentru succes.

Mai întâi, să mutăm încărcarea datelor din încărcătorul nostru în +page.server.js la un Ruta API. Vom crea un +server.js fișier în routes/api/todos, apoi adăugați a GET funcţie. Aceasta înseamnă că acum vom putea prelua (folosind verbul implicit GET) la /api/todos cale. Vom adăuga același cod de încărcare a datelor ca înainte.

import { json } from "@sveltejs/kit";
import { getTodos } from "$lib/data/todoData"; export async function GET({ url, setHeaders, request }) { const search = url.searchParams.get("search") || ""; const todos = await getTodos(search); return json(todos);
}

Apoi, să luăm încărcătorul de pagini pe care îl aveam și să redenumim pur și simplu fișierul din +page.server.js la +page.js (Sau .ts dacă v-ați configurat proiectul pentru a utiliza TypeScript). Acest lucru schimbă încărcătorul nostru să fie un încărcător „universal”, mai degrabă decât un încărcător de server. Documentele SvelteKit explica diferența, dar un încărcător universal rulează atât pe server, cât și pe client. Un avantaj pentru noi este că fetch apelul către noul nostru punct final va rula direct din browserul nostru (după încărcarea inițială), folosind browserul nativ fetch funcţie. Vom adăuga puțin timp cache HTTP standard, dar pentru moment, tot ce vom face este să apelăm punctul final.

export async function load({ fetch, url, setHeaders }) { const search = url.searchParams.get("search") || ""; const resp = await fetch(`/api/todos?search=${encodeURIComponent(search)}`); const todos = await resp.json(); return { todos, };
}

Acum să adăugăm un formular simplu în formularul nostru /list Pagina:

<div class="search-form"> <form action="/ro/list"> <label>Search</label> <input autofocus name="search" /> </form>
</div>

Da, formularele pot viza direct dispozitivele noastre normale de încărcare a paginilor. Acum putem adăuga un termen de căutare în caseta de căutare, apăsați Intrați, iar un termen de „căutare” va fi atașat la șirul de interogare al adresei URL, care va rula din nou încărcătorul și va căuta elementele noastre de făcut.

formular de căutare

Să creștem și întârzierea în a noastră todoData.js fișier în /lib/data. Acest lucru va face ușor să vedeți când datele sunt și nu sunt stocate în cache pe măsură ce lucrăm prin această postare.

export const wait = async amount => new Promise(res => setTimeout(res, amount ?? 500));

Amintiți-vă, codul complet pentru această postare este totul pe GitHub, dacă trebuie să-l referiți.

Cache de bază

Să începem prin adăugarea unor stocări în cache la sistemul nostru /api/todos punct final. Ne vom întoarce la noi +server.js fișier și adăugați primul nostru antet de control cache.

setHeaders({ "cache-control": "max-age=60",
});

… ceea ce va lăsa întreaga funcție să arate astfel:

export async function GET({ url, setHeaders, request }) { const search = url.searchParams.get("search") || ""; setHeaders({ "cache-control": "max-age=60", }); const todos = await getTodos(search); return json(todos);
}

Ne vom uita la invalidarea manuală în scurt timp, dar tot ceea ce spune această funcție este să memorăm în cache aceste apeluri API timp de 60 de secunde. Setați asta la orice dorițiși, în funcție de cazul dvs. de utilizare, stale-while-revalidate ar putea, de asemenea, să merite să cercetăm.

Și chiar așa, interogările noastre sunt stocate în cache.

Cache în DevTools.

notițe asigură-te că debifați caseta de selectare care dezactivează stocarea în cache în instrumentele de dezvoltare.

Amintiți-vă, dacă navigarea inițială în aplicație este pagina cu listă, acele rezultate ale căutării vor fi stocate în cache intern în SvelteKit, așa că nu vă așteptați să vedeți nimic în DevTools când reveniți la căutarea respectivă.

Ce este stocat în cache și unde

Prima noastră încărcare, redată de server, a aplicației noastre (presupunând că începem de la /list pagina) va fi preluat de pe server. SvelteKit va serializa și va trimite aceste date către clientul nostru. Mai mult, se va observa Cache-Control antet pe răspuns și va ști să folosească aceste date stocate în cache pentru acel apel la punctul final în fereastra de cache (pe care am setat-o ​​la 60 de secunde în exemplul pus).

După această încărcare inițială, când începeți să căutați pe pagină, ar trebui să vedeți solicitările de rețea de la browser la /api/todos listă. Pe măsură ce căutați lucruri pe care le-ați căutat deja (în ultimele 60 de secunde), răspunsurile ar trebui să se încarce imediat, deoarece sunt stocate în cache.

Ceea ce este deosebit de interesant cu această abordare este că, deoarece aceasta este stocarea în cache prin memoria cache nativă a browserului, aceste apeluri ar putea (în funcție de modul în care gestionați distrugerea cache-ului pe care o vom analiza) să continue să fie stocată în cache chiar dacă reîncărcați pagina (spre deosebire de încărcare inițială pe partea serverului, care apelează întotdeauna punctul final proaspăt, chiar dacă a făcut-o în ultimele 60 de secunde).

Evident, datele se pot schimba oricând, așa că avem nevoie de o modalitate de a curăța manual acest cache, pe care o vom analiza în continuare.

Invalidarea memoriei cache

În acest moment, datele vor fi stocate în cache timp de 60 de secunde. Indiferent de ce, după un minut, date noi vor fi extrase din magazinul nostru de date. Poate doriți o perioadă de timp mai scurtă sau mai lungă, dar ce se întâmplă dacă modificați unele date și doriți să ștergeți imediat memoria cache, astfel încât următoarea interogare să fie actualizată? Vom rezolva acest lucru adăugând o valoare de eliminare a interogărilor la adresa URL pe care o trimitem către noua noastră /todos punctul final.

Să stocăm această valoare de distrugere a memoriei cache într-un cookie. Această valoare poate fi setată pe server, dar totuși poate fi citită pe client. Să ne uităm la un exemplu de cod.

Putem crea un +layout.server.js dosar la rădăcina noastră routes pliant. Acesta va rula la pornirea aplicației și este un loc perfect pentru a seta o valoare inițială a cookie-ului.

export function load({ cookies, isDataRequest }) { const initialRequest = !isDataRequest; const cacheValue = initialRequest ? +new Date() : cookies.get("todos-cache"); if (initialRequest) { cookies.set("todos-cache", cacheValue, { path: "/", httpOnly: false }); } return { todosCacheBust: cacheValue, };
}

Este posibil să fi observat isDataRequest valoare. Amintiți-vă, aspectele se vor relua oricând apelurile de cod de client invalidate(), sau oricând rulăm o acțiune de server (presupunând că nu dezactivăm comportamentul implicit). isDataRequest indică acele reluări și, prin urmare, setăm cookie-ul numai dacă este așa false; în caz contrar, trimitem ceea ce este deja acolo.

httpOnly: false steagul este de asemenea semnificativ. Acest lucru permite codului nostru client să citească aceste valori cookie în document.cookie. În mod normal, aceasta ar fi o problemă de securitate, dar în cazul nostru, acestea sunt numere lipsite de sens care ne permit să stocăm în cache sau să blocăm cache.

Citirea valorilor din cache

Încărcătorul nostru universal este ceea ce se numește nostru /todos punct final. Acesta rulează pe server sau pe client și trebuie să citim acea valoare cache pe care tocmai am configurat-o, indiferent unde ne aflăm. Este relativ ușor dacă suntem pe server: putem suna await parent() pentru a obține datele din machetele părinte. Dar pe client, va trebui să folosim un cod brut pentru a analiza document.cookie:

export function getCookieLookup() { if (typeof document !== "object") { return {}; } return document.cookie.split("; ").reduce((lookup, v) => { const parts = v.split("="); lookup[parts[0]] = parts[1]; return lookup; }, {});
} const getCurrentCookieValue = name => { const cookies = getCookieLookup(); return cookies[name] ?? "";
};

Din fericire, avem nevoie doar o dată.

Se trimite valoarea cache

Dar acum trebuie trimite această valoare pentru noi /todos punctul final.

import { getCurrentCookieValue } from "$lib/util/cookieUtils"; export async function load({ fetch, parent, url, setHeaders }) { const parentData = await parent(); const cacheBust = getCurrentCookieValue("todos-cache") || parentData.todosCacheBust; const search = url.searchParams.get("search") || ""; const resp = await fetch(`/api/todos?search=${encodeURIComponent(search)}&cache=${cacheBust}`); const todos = await resp.json(); return { todos, };
}

getCurrentCookieValue('todos-cache') are o verificare pentru a vedea dacă suntem pe client (prin verificarea tipului de document) și nu returnează nimic dacă suntem, moment în care știm că suntem pe server. Apoi folosește valoarea din aspectul nostru.

Distrugerea memoriei cache

dar cum actualizăm de fapt acea valoare de distrugere a memoriei cache atunci când avem nevoie? Deoarece este stocat într-un cookie, îl putem numi astfel din orice acțiune de server:

cookies.set("todos-cache", cacheValue, { path: "/", httpOnly: false });

Implementarea

Totul e la vale de aici; am făcut munca grea. Am acoperit diferitele primitive ale platformei web de care avem nevoie, precum și unde ajung. Acum să ne distrăm puțin și să scriem codul aplicației pentru a le lega pe toate.

Din motive care vor deveni clare într-un pic, să începem prin a adăuga o funcționalitate de editare la /list pagină. Vom adăuga acest al doilea rând de tabel pentru fiecare tot:

import { enhance } from "$app/forms";
<tr> <td colspan="4"> <form use:enhance method="post" action="?/editTodo"> <input name="id" value="{t.id}" type="hidden" /> <input name="title" value="{t.title}" /> <button>Save</button> </form> </td>
</tr>

Și, desigur, va trebui să adăugăm o acțiune de formular pentru a noastră /list pagină. Acțiunile pot intra doar .server pagini, așa că vom adăuga un +page.server.js în a noastră /list pliant. (Da o +page.server.js fișierul poate coexista lângă a +page.js fişier.)

import { getTodo, updateTodo, wait } from "$lib/data/todoData"; export const actions = { async editTodo({ request, cookies }) { const formData = await request.formData(); const id = formData.get("id"); const newTitle = formData.get("title"); await wait(250); updateTodo(id, newTitle); cookies.set("todos-cache", +new Date(), { path: "/", httpOnly: false }); },
};

Preluăm datele din formular, forțăm o întârziere, ne actualizăm tot și apoi, cel mai important, ștergem cookie-ul nostru cache-bust.

Să încercăm asta. Reîncărcați pagina, apoi editați unul dintre elementele de făcut. Ar trebui să vedeți actualizarea valorii tabelului după un moment. Dacă te uiți în fila Rețea din DevToold, vei vedea o preluare la /todos endpoint, care vă returnează noile date. Simplu și funcționează implicit.

Salvarea datelor

Actualizări imediate

Ce se întâmplă dacă dorim să evităm preluarea care se întâmplă după ce ne actualizăm elementul de făcut și, în schimb, să actualizăm elementul modificat chiar pe ecran?

Aceasta nu este doar o chestiune de performanță. Dacă căutați „post” și apoi eliminați cuvântul „post” din oricare dintre elementele de făcut din listă, acestea vor dispărea din listă după modificare, deoarece nu se mai află în rezultatele căutării acelei pagini. Ai putea îmbunătăți UX-ul cu niște animații de bun gust pentru treaba ieșitoare, dar să presupunem că am vrut să nu rulați din nou funcția de încărcare a paginii, dar ștergeți memoria cache și actualizați operațiunea modificată, astfel încât utilizatorul să poată vedea editarea. SvelteKit face acest lucru posibil - să vedem cum!

În primul rând, să facem o mică schimbare în încărcătorul nostru. În loc să ne returnăm articolele de făcut, să returnăm a magazin care poate fi scris care conțin sarcinile noastre de făcut.

return { todos: writable(todos),
};

Înainte, accesam sarcinile noastre de făcut pe data prop, pe care nu o deținem și nu putem actualiza. Dar Svelte ne permite să ne returnăm datele în propriul lor magazin (presupunând că folosim un încărcător universal, ceea ce suntem). Trebuie doar să facem încă o modificare /list .

In loc de asta:

{#each todos as t}

… trebuie să facem asta de atunci todos este acum un magazin.:

{#each $todos as t}

Acum datele noastre se încarcă ca înainte. Dar de atunci todos este un magazin care poate fi scris, îl putem actualiza.

În primul rând, să oferim o funcție pentru a noastră use:enhance atribut:

<form use:enhance={executeSave} on:submit={runInvalidate} method="post" action="?/editTodo"
>

Aceasta va rula înainte de trimitere. Să scriem asta în continuare:

function executeSave({ data }) { const id = data.get("id"); const title = data.get("title"); return async () => { todos.update(list => list.map(todo => { if (todo.id == id) { return Object.assign({}, todo, { title }); } else { return todo; } }) ); };
}

Această funcție oferă a data obiect cu datele formularului nostru. Noi reveni o funcție asincronă care va rula după editarea noastră este terminată. Docs explicați toate acestea, dar făcând acest lucru, am oprit gestionarea implicită a formularelor SvelteKit, care ar fi reluat încărcătorul nostru. Este exact ceea ce ne dorim! (Am putea recupera cu ușurință acel comportament implicit, așa cum explică documentele.)

Sunăm acum update pe noastre todos matrice deoarece este un magazin. Și asta este. După editarea unui element de făcut, modificările noastre apar imediat și memoria cache este șters (ca și înainte, deoarece am setat o nouă valoare cookie în editTodo acţiune de formă). Deci, dacă căutăm și apoi navigăm înapoi la această pagină, vom obține date proaspete din încărcătorul nostru, care va exclude în mod corect orice elemente de făcut actualizate care au fost actualizate.

Codul pentru actualizările imediate este disponibil pe GitHub.

Săpând mai adânc

Putem seta cookie-uri în orice funcție de încărcare a serverului (sau acțiune de server), nu doar aspectul rădăcină. Deci, dacă unele date sunt folosite doar sub un singur aspect, sau chiar sub o singură pagină, puteți seta acea valoare cookie acolo. Mai mult, dacă ești nu făcând trucul pe care tocmai l-am arătat, actualizarea manuală a datelor de pe ecran și, în schimb, doriți ca încărcătorul dvs. să ruleze din nou după o mutație, atunci puteți seta oricând o nouă valoare cookie chiar în acea funcție de încărcare fără nicio verificare împotriva isDataRequest. Se va seta inițial, iar apoi, oricând executați o acțiune de server, aspectul paginii va invalida și va reapela automat încărcătorul, resetând șirul de memorie cache înainte ca încărcătorul universal să fie apelat.

Scrierea unei funcții de reîncărcare

Să încheiem construind o ultimă caracteristică: un buton de reîncărcare. Să oferim utilizatorilor un buton care va șterge memoria cache și apoi va reîncărca interogarea curentă.

Vom adăuga o acțiune de formă dirt simplă:

async reloadTodos({ cookies }) { cookies.set('todos-cache', +new Date(), { path: '/', httpOnly: false });
},

Într-un proiect real, probabil că nu ați copia/lipi același cod pentru a seta același cookie în același mod în mai multe locuri, dar pentru această postare vom optimiza pentru simplitate și lizibilitate.

Acum să creăm un formular pe care să îl postăm:

<form method="POST" action="?/reloadTodos" use:enhance> <button>Reload todos</button>
</form>

Asta merge!

UI după reîncărcare.

Am putea spune asta gata și să mergem mai departe, dar să îmbunătățim puțin această soluție. Mai exact, să oferim feedback pe pagină pentru a-i spune utilizatorului că are loc reîncărcarea. De asemenea, în mod implicit, acțiunile SvelteKit sunt invalidate tot. Fiecare aspect, pagină etc. din ierarhia paginii curente se va reîncărca. Este posibil să existe unele date care sunt încărcate o dată în aspectul rădăcină pe care nu trebuie să le invalidăm sau să le reîncărcăm.

Deci, să ne concentrăm puțin asupra lucrurilor și să ne reîncărcăm sarcinile de făcut numai atunci când apelăm această funcție.

Mai întâi, să transmitem o funcție pentru a îmbunătăți:

<form method="POST" action="?/reloadTodos" use:enhance={reloadTodos}>
import { enhance } from "$app/forms";
import { invalidate } from "$app/navigation"; let reloading = false;
const reloadTodos = () => { reloading = true; return async () => { invalidate("reload:todos").then(() => { reloading = false; }); };
};

Stabilim un nou reloading variabilă la true de la Începe a acestei actiuni. Și apoi, pentru a suprascrie comportamentul implicit de a invalida totul, returnăm un async funcţie. Această funcție va rula când acțiunea serverului nostru este terminată (care doar setează un cookie nou).

Fără asta async funcția returnată, SvelteKit ar invalida totul. Deoarece oferim această funcție, nu va invalida nimic, așa că depinde de noi să-i spunem ce să reîncarce. Facem asta cu invalidate funcţie. O numim cu o valoare de reload:todos. Această funcție returnează o promisiune, care se rezolvă când invalidarea este completă, moment în care setăm reloading înapoi la false.

În cele din urmă, trebuie să ne sincronizăm încărcătorul cu acest nou reload:todos valoarea de invalidare. Facem asta în încărcătorul nostru cu depends funcţie:

export async function load({ fetch, url, setHeaders, depends }) { depends('reload:todos'); // rest is the same

Și atât. depends și invalidate sunt funcții incredibil de utile. Ce e tare este asta invalidate nu ia doar valori arbitrare pe care le oferim, așa cum am făcut-o. De asemenea, putem furniza o adresă URL, pe care SvelteKit o va urmări și să invalidăm orice încărcătoare care depind de acea adresă URL. În acest scop, dacă vă întrebați dacă am putea sări peste apelul la depends și ne invalidează /api/todos endpoint cu totul, puteți, dar trebuie să furnizați exact URL, inclusiv search termenul (și valoarea noastră cache). Deci, puteți fie să puneți împreună adresa URL pentru căutarea curentă, fie să potriviți numele căii, astfel:

invalidate(url => url.pathname == "/api/todos");

Personal, gasesc solutia care foloseste depends mai explicit și mai simplu. Dar vezi documentele pentru mai multe informații, desigur, și decideți singur.

Dacă doriți să vedeți butonul de reîncărcare în acțiune, codul pentru acesta este introdus această ramură a repo.

Gânduri de despărțire

Aceasta a fost o postare lungă, dar sperăm că nu copleșitoare. Ne-am aruncat în diferite moduri în care putem stoca datele în cache atunci când folosim SvelteKit. O mare parte din aceasta a fost doar o chestiune de utilizare a primitivelor platformei web pentru a adăuga cache-ul corect și valorile cookie, cunoașterea cărora vă va ajuta în dezvoltarea web în general, dincolo de doar SvelteKit.

În plus, acesta este ceva absolut nu au nevoie tot timpul. Probabil, ar trebui să apelați la acest tip de funcții avansate doar atunci când aveți chiar nevoie de ele. Dacă depozitul dvs. de date servește datele rapid și eficient și nu aveți de-a face cu niciun fel de probleme de scalare, nu are sens să vă umflați codul aplicației cu o complexitate inutilă în a face lucrurile despre care am vorbit aici.

Ca întotdeauna, scrieți cod clar, curat, simplu și optimizați-l atunci când este necesar. Scopul acestei postări a fost să vă ofere acele instrumente de optimizare atunci când aveți cu adevărat nevoie de ele. Sper ca ti-a placut!

Chat cu noi

Bună! Cu ce ​​​​vă pot ajuta?