Xlera8

Caching af data i SvelteKit

My forrige indlæg var en bred oversigt over SvelteKit, hvor vi så, hvilket fantastisk værktøj det er til webudvikling. Dette indlæg vil afsløre, hvad vi gjorde der, og dykke ned i enhver udviklers foretrukne emne: caching. Så sørg for at læse mit sidste indlæg, hvis du ikke allerede har gjort det. Koden til dette indlæg er tilgængelig på GitHub, såvel som en live demo.

Dette indlæg handler om datahåndtering. Vi tilføjer nogle rudimentære søgefunktioner, der vil ændre sidens forespørgselsstreng (ved hjælp af indbyggede SvelteKit-funktioner) og genaktivere sidens loader. Men i stedet for blot at genforespørge vores (imaginære) database, tilføjer vi noget caching, så gensøgning af tidligere søgninger (eller ved at bruge tilbage-knappen) vil vise tidligere hentede data hurtigt fra cachen. Vi vil se på, hvordan man kontrollerer, hvor lang tid de cachelagrede data forbliver gyldige, og endnu vigtigere, hvordan man manuelt ugyldiggør alle cachelagrede værdier. Som prikken over i'et vil vi se på, hvordan vi manuelt kan opdatere dataene på den aktuelle skærm, klientsiden, efter en mutation, mens vi stadig renser cachen.

Dette bliver et længere og sværere indlæg end det meste af det, jeg plejer at skrive, da vi dækker sværere emner. Dette indlæg vil i det væsentlige vise dig, hvordan du implementerer almindelige funktioner i populære dataværktøjer som reagere-forespørgsel; men i stedet for at trække et eksternt bibliotek ind, bruger vi kun webplatformen og SvelteKit-funktionerne.

Desværre er webplatformens funktioner på et lidt lavere niveau, så vi kommer til at lave lidt mere arbejde, end du måske er vant til. Fordelen er, at vi ikke behøver nogen eksterne biblioteker, hvilket vil hjælpe med at holde bundtstørrelserne pæne og små. Brug venligst ikke de metoder, jeg vil vise dig, medmindre du har en god grund til det. Caching er let at tage fejl, og som du vil se, er der en smule kompleksitet, der vil resultere i din ansøgningskode. Forhåbentlig er dit datalager hurtigt, og din brugergrænseflade er fin, så SvelteKit bare altid kan anmode om de data, det har brug for for en given side. Hvis det er, så lad det være. Nyd enkelheden. Men dette indlæg vil vise dig nogle tricks til, hvornår det holder op med at være tilfældet.

Apropos react-query, det blev netop løsladt for Svelte! Så hvis du læner dig op af manuelle caching-teknikker en masse, sørg for at tjekke det projekt ud, og se om det kan hjælpe.

Opsætning

Inden vi starter, lad os lave et par små ændringer til koden vi havde før. Dette vil give os en undskyldning for at se nogle andre SvelteKit-funktioner og, endnu vigtigere, sætte os op til succes.

Lad os først flytte vores dataindlæsning fra vores loader ind +page.server.js til en API rute. Vi laver en +server.js fil i routes/api/todos, og tilføj derefter en GET fungere. Dette betyder, at vi nu vil være i stand til at hente (ved at bruge standard GET verbum) til /api/todos sti. Vi tilføjer den samme dataindlæsningskode som før.

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);
}

Lad os derefter tage den sideindlæser, vi havde, og blot omdøbe filen fra +page.server.js til +page.js (eller .ts hvis du har stilladset dit projekt til at bruge TypeScript). Dette ændrer vores loader til at være en "universal" loader i stedet for en server loader. SvelteKit-dokumenterne forklar forskellen, men en universel loader kører på både serveren og også klienten. En fordel for os er, at fetch opkald til vores nye slutpunkt vil køre direkte fra vores browser (efter den første indlæsning) ved hjælp af browserens oprindelige fetch fungere. Vi tilføjer standard HTTP-cache om lidt, men indtil videre er alt, hvad vi skal gøre, at kalde slutpunktet.

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, };
}

Lad os nu tilføje en simpel formular til vores /list side:

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

Jep, formularer kan målrettes direkte til vores normale sideindlæsere. Nu kan vi tilføje et søgeord i søgefeltet, tryk Indtast, og et "søge"-udtryk vil blive tilføjet til URL'ens forespørgselsstreng, som vil genkøre vores loader og søge i vores to-do-emner.

Søgeformular

Lad os også øge forsinkelsen i vores todoData.js fil i /lib/data. Dette vil gøre det nemt at se, hvornår data er og ikke er cachelagret, mens vi arbejder gennem dette indlæg.

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

Husk, den fulde kode for dette indlæg er alt på GitHub, hvis du har brug for at henvise til det.

Grundlæggende caching

Lad os komme i gang ved at tilføje noget caching til vores /api/todos endepunkt. Vi går tilbage til vores +server.js fil og tilføje vores første cache-control header.

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

…hvilket vil lade hele funktionen se sådan ud:

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);
}

Vi vil snart se på manuel ugyldiggørelse, men alt, hvad denne funktion siger, er at cache disse API-kald i 60 sekunder. Indstil dette til hvad du vil, og afhængigt af din brugssituation, stale-while-revalidate kan også være værd at kigge nærmere på.

Og bare sådan cachelagres vores forespørgsler.

Cache i DevTools.

Bemærk Vær sikker på at du fjern markeringen afkrydsningsfeltet, der deaktiverer caching i udviklerværktøjer.

Husk, at hvis din første navigation på appen er listesiden, vil disse søgeresultater blive cachet internt til SvelteKit, så forvent ikke at se noget i DevTools, når du vender tilbage til den søgning.

Hvad er cachelagret, og hvor

Vores allerførste server-renderede belastning af vores app (forudsat at vi starter ved /list side) vil blive hentet på serveren. SvelteKit vil serialisere og sende disse data ned til vores klient. Hvad mere er, vil den observere Cache-Control header på svaret, og vil vide at bruge disse cachelagrede data til det endepunktkald inden for cachevinduet (som vi indstillede til 60 sekunder i put-eksemplet).

Efter den første indlæsning, når du begynder at søge på siden, bør du se netværksanmodninger fra din browser til /api/todos liste. Når du søger efter ting, du allerede har søgt efter (inden for de sidste 60 sekunder), bør svarene indlæses med det samme, da de er cachelagret.

Det, der er særligt fedt ved denne tilgang, er, at da dette er caching via browserens native caching, kan disse opkald (afhængigt af hvordan du administrerer den cache-busting, vi skal se på) fortsætte med at cache, selvom du genindlæser siden (i modsætning til indledende server-side load, som altid kalder slutpunktet frisk, selvom det gjorde det inden for de sidste 60 sekunder).

Data kan naturligvis ændres når som helst, så vi har brug for en måde at rense denne cache manuelt, som vi vil se på næste gang.

Cache-invalidering

Lige nu vil data blive cachelagret i 60 sekunder. Uanset hvad, efter et minut vil friske data blive trukket fra vores datalager. Du vil måske have en kortere eller længere tidsperiode, men hvad sker der, hvis du muterer nogle data og vil rydde din cache med det samme, så din næste forespørgsel vil være opdateret? Vi løser dette ved at tilføje en query-busting-værdi til den URL, vi sender til vores nye /todos slutpunkt.

Lad os gemme denne cache-busting-værdi i en cookie. Denne værdi kan indstilles på serveren, men stadig læses på klienten. Lad os se på nogle eksempler på kode.

Vi kan skabe en +layout.server.js fil i selve roden af ​​vores routes folder. Dette vil køre ved opstart af applikationen og er et perfekt sted at indstille en indledende cookieværdi.

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, };
}

Du har måske bemærket isDataRequest værdi. Husk, at layouts kører igen når som helst klientkodekald invalidate(), eller når som helst vi kører en serverhandling (forudsat at vi ikke slår standardadfærd fra). isDataRequest angiver disse genkørsler, og derfor indstiller vi kun cookien, hvis det er false; ellers sender vi det, der allerede er der.

httpOnly: false flag er også væsentligt. Dette gør det muligt for vores klientkode at læse disse cookieværdier ind document.cookie. Dette ville normalt være et sikkerhedsproblem, men i vores tilfælde er disse meningsløse tal, der tillader os at cache eller cache buste.

Læsning af cacheværdier

Vores universallæsser er det, der kalder vores /todos endepunkt. Dette kører på serveren eller klienten, og vi skal læse den cacheværdi, vi lige har sat op, uanset hvor vi er. Det er relativt nemt, hvis vi er på serveren: vi kan ringe await parent() for at hente dataene fra overordnede layouts. Men på klienten bliver vi nødt til at bruge noget bruttokode til at parse 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] ?? "";
};

Heldigvis har vi kun brug for det én gang.

Udsender cacheværdien

Men nu skal vi send denne værdi for vores /todos slutpunkt.

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') har et tjek i det for at se, om vi er på klienten (ved at kontrollere typen af ​​dokument), og returnerer intet, hvis vi er, på hvilket tidspunkt vi ved, at vi er på serveren. Så bruger den værdien fra vores layout.

Spræng cachen

Men hvordan opdaterer vi faktisk denne cache-busting-værdi, når vi har brug for det? Da det er gemt i en cookie, kan vi kalde det sådan fra enhver serverhandling:

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

Implementeringen

Det hele går ned ad bakke herfra; vi har gjort det hårde arbejde. Vi har dækket de forskellige webplatforms primitiver, vi har brug for, samt hvor de går hen. Lad os nu have det sjovt og skrive ansøgningskode for at binde det hele sammen.

Af årsager, der vil blive tydelige om lidt, lad os starte med at tilføje en redigeringsfunktion til vores /list side. Vi tilføjer denne anden tabelrække for hver opgave:

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>

Og selvfølgelig skal vi tilføje en formularhandling for vores /list side. Handlinger kan kun gå ind .server sider, så vi tilføjer en +page.server.js i vores /list folder. (Ja, a +page.server.js fil kan eksistere side om side ved siden af ​​en +page.js filer.)

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 }); },
};

Vi griber formulardataene, fremtvinger en forsinkelse, opdaterer vores todo, og så, vigtigst af alt, rydder vores cache-bust-cookie.

Lad os prøve det her. Genindlæs din side, og rediger derefter et af opgaverne. Du bør se tabelværdiopdateringen efter et øjeblik. Hvis du kigger på fanen Netværk i DevToold, vil du se en hentning til /todos slutpunkt, som returnerer dine nye data. Enkel og fungerer som standard.

Gem data

Øjeblikkelige opdateringer

Hvad hvis vi vil undgå den hentning, der sker, efter at vi har opdateret vores opgavevare, og i stedet opdaterer det ændrede element direkte på skærmen?

Dette er ikke kun et spørgsmål om ydeevne. Hvis du søger efter "indlæg" og derefter fjerner ordet "indlæg" fra et af opgavepunkterne på listen, forsvinder de fra listen efter redigeringen, da de ikke længere er i den pågældende sides søgeresultater. Du kunne gøre UX bedre med noget smagfuld animation til den spændende opgave, men lad os sige, at vi ville ikke kør sidens indlæsningsfunktion igen, men ryd stadig cachen og opdater den ændrede opgave, så brugeren kan se redigeringen. SvelteKit gør det muligt - lad os se hvordan!

Lad os først lave en lille ændring af vores læsser. I stedet for at returnere vores gøremål, lad os returnere en skrivevenlig butik indeholder vores gøremål.

return { todos: writable(todos),
};

Før havde vi adgang til vores gøremål på data prop, som vi ikke ejer og ikke kan opdatere. Men Svelte lader os returnere vores data i deres egen butik (forudsat at vi bruger en universel loader, hvilket vi er). Vi skal bare lave endnu en tweak til vores /list .

I stedet for dette:

{#each todos as t}

...vi er nødt til at gøre dette siden todos er nu selv en butik.:

{#each $todos as t}

Nu indlæses vores data som før. Men siden todos er en skrivbar butik, vi kan opdatere den.

Lad os først give en funktion til vores use:enhance attribut:

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

Dette vil køre før en indsendelse. Lad os skrive det næste:

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; } }) ); };
}

Denne funktion giver en data objekt med vores formulardata. Vi afkast en asynkron funktion, der kører efter vores redigering er færdig. Dokumenterne forklare alt dette, men ved at gøre dette lukker vi SvelteKits standardformularhåndtering, som ville have kørt vores indlæser igen. Det er præcis, hvad vi ønsker! (Vi kunne nemt få den standardadfærd tilbage, som dokumenterne forklarer.)

Vi ringer nu update på vores todos række, da det er en butik. Og det er det. Efter at have redigeret et opgaveelement, dukker vores ændringer op med det samme, og vores cache ryddes (som før, da vi indstiller en ny cookieværdi i vores editTodo form handling). Så hvis vi søger og derefter navigerer tilbage til denne side, får vi friske data fra vores loader, som korrekt vil udelukke alle opdaterede gøremål, der er blevet opdateret.

Koden til de øjeblikkelige opdateringer er tilgængelig på GitHub.

Grave dybere

Vi kan sætte cookies i enhver serverindlæsningsfunktion (eller serverhandling), ikke kun i rodlayoutet. Så hvis nogle data kun bruges under et enkelt layout, eller endda en enkelt side, kan du indstille denne cookieværdi der. Desuden, hvis du er ikke gør det trick, jeg lige har vist manuelt opdatering af data på skærmen, og i stedet vil have din loader til at køre igen efter en mutation, så kan du altid indstille en ny cookie-værdi lige i den load-funktion uden nogen kontrol mod isDataRequest. Det indstilles til at begynde med, og hver gang du kører en serverhandling, vil sidelayoutet automatisk ugyldiggøre og genkalde din loader, og nulstille cache-bust-strengen, før din universelle loader kaldes.

Skriver en genindlæsningsfunktion

Lad os afslutte med at bygge en sidste funktion: en genindlæsningsknap. Lad os give brugerne en knap, der vil rydde cachen og derefter genindlæse den aktuelle forespørgsel.

Vi tilføjer en simpel formhandling:

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

I et rigtigt projekt ville du sandsynligvis ikke kopiere/indsætte den samme kode for at sætte den samme cookie på samme måde flere steder, men til dette indlæg vil vi optimere for enkelhed og læsbarhed.

Lad os nu oprette en formular til at sende til den:

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

Det virker!

UI efter genindlæsning.

Vi kunne kalde dette gjort og gå videre, men lad os forbedre denne løsning en smule. Lad os specifikt give feedback på siden for at fortælle brugeren, at genindlæsningen sker. Som standard er SvelteKit-handlinger også ugyldige at alt. Hvert layout, side osv. i den aktuelle sides hierarki vil genindlæses. Der kan være nogle data, der er indlæst én gang i rodlayoutet, som vi ikke behøver at ugyldiggøre eller genindlæse.

Så lad os fokusere lidt på tingene og kun genindlæse vores gøremål, når vi kalder denne funktion.

Lad os først videregive en funktion til at forbedre:

<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; }); };
};

Vi sætter en ny reloading variabel til true ved starte af denne handling. Og så, for at tilsidesætte standardadfærden med at ugyldiggøre alt, returnerer vi en async fungere. Denne funktion vil køre, når vores serverhandling er afsluttet (som blot sætter en ny cookie).

Uden dette async funktion returnerede, ville SvelteKit ugyldiggøre alt. Da vi leverer denne funktion, vil den intet ugyldiggøre, så det er op til os at fortælle den, hvad den skal genindlæse. Det gør vi med invalidate fungere. Vi kalder det med en værdi på reload:todos. Denne funktion returnerer et løfte, som løses, når ugyldiggørelsen er fuldført, på hvilket tidspunkt vi indstiller reloading tilbage til false.

Til sidst skal vi synkronisere vores loader med denne nye reload:todos ugyldighedsværdi. Det gør vi i vores læsser med depends fungere:

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

Og det er det. depends , invalidate er utrolig nyttige funktioner. Det der er fedt er det invalidate tager ikke bare vilkårlige værdier, vi giver, som vi gjorde. Vi kan også give en URL, som SvelteKit vil spore, og ugyldiggøre eventuelle indlæsere, der afhænger af den URL. Til det formål, hvis du spekulerer på, om vi kunne springe opkaldet til depends og ugyldiggøre vores /api/todos endepunkt helt, du kan, men du skal levere eksakt URL, inklusive search term (og vores cacheværdi). Så du kan enten sammensætte URL'en til den aktuelle søgning eller matche på stinavnet, sådan her:

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

Personligt finder jeg den løsning, der bruger depends mere eksplicit og enkelt. Men se dokumenterne for mere info, selvfølgelig, og bestemme selv.

Hvis du gerne vil se genindlæsningsknappen i aktion, er koden til den i denne gren af ​​repoen.

Afskedige tanker

Det var et langt indlæg, men forhåbentlig ikke overvældende. Vi dykkede ind i forskellige måder, hvorpå vi kan cache data, når vi bruger SvelteKit. Meget af dette var blot et spørgsmål om at bruge webplatformens primitiver til at tilføje den korrekte cache og cookie-værdier, hvis viden vil tjene dig i webudvikling generelt, ud over kun SvelteKit.

Desuden er dette noget du absolut behøver ikke hele tiden. Nok, bør du kun række ud efter denne slags avancerede funktioner, når du faktisk har brug for dem. Hvis dit datalager serverer data hurtigt og effektivt, og du ikke har at gøre med nogen form for skaleringsproblemer, er der ingen mening i at svulme din applikationskode op med unødvendig kompleksitet ved at gøre de ting, vi talte om her.

Skriv som altid klar, ren, enkel kode og optimer, når det er nødvendigt. Formålet med dette indlæg var at give dig de optimeringsværktøjer, når du virkelig har brug for dem. Jeg håber du nød det!

Chat med os

Hej! Hvordan kan jeg hjælpe dig?