Cacha data i SvelteKit

Cacha data i SvelteKit

My tidigare inlägg var en bred översikt av SvelteKit där vi såg vilket bra verktyg det är för webbutveckling. Det här inlägget kommer att avslöja vad vi gjorde där och dyka in i varje utvecklares favoritämne: caching. Så se till att läsa mitt senaste inlägg om du inte redan har gjort det. Koden för detta inlägg finns på GitHub, såväl som en livedemo.

Det här inlägget handlar om datahantering. Vi kommer att lägga till lite rudimentär sökfunktion som kommer att ändra sidans frågesträng (med inbyggda SvelteKit-funktioner) och återaktivera sidans loader. Men i stället för att bara fråga om vår (imaginära) databas, kommer vi att lägga till lite cachning så att omsökning av tidigare sökningar (eller genom att använda bakåtknappen) visar tidigare hämtade data, snabbt, från cachen. Vi ska titta på hur man kontrollerar hur länge cachad data förblir giltig och, ännu viktigare, hur man manuellt ogiltigförklarar alla cachade värden. Som grädde på moset kommer vi att titta på hur vi manuellt kan uppdatera data på den aktuella skärmen, klientsidan, efter en mutation, samtidigt som vi rensar cachen.

Det här kommer att bli ett längre och svårare inlägg än det mesta jag brukar skriva eftersom vi tar upp svårare ämnen. Det här inlägget kommer i huvudsak att visa dig hur du implementerar vanliga funktioner i populära dataverktyg som reagera-förfrågan; men istället för att dra in ett externt bibliotek kommer vi bara att använda webbplattformen och SvelteKit-funktionerna.

Tyvärr är webbplattformens funktioner lite lägre, så vi kommer att göra lite mer arbete än du kanske är van vid. Fördelen är att vi inte behöver några externa bibliotek, vilket kommer att hjälpa till att hålla paketstorlekarna små och fina. Vänligen använd inte de metoder jag ska visa dig om du inte har en bra anledning till det. Cachning är lätt att få fel, och som du kommer att se är det lite komplexitet som kommer att resultera i din applikationskod. Förhoppningsvis är ditt datalager snabbt, och ditt användargränssnitt är bra, vilket gör att SvelteKit bara alltid kan begära den data den behöver för en given sida. Om det är det, lämna det ifred. Njut av enkelheten. Men det här inlägget kommer att visa dig några knep för när det slutar vara fallet.

På tal om react-query, det släpptes precis för Svelte! Så om du kommer på dig själv att luta dig mot manuella cachningstekniker mycket, se till att kolla upp det projektet och se om det kan hjälpa.

Inställning

Innan vi börjar, låt oss göra några små ändringar i koden vi hade innan. Detta kommer att ge oss en ursäkt för att se några andra SvelteKit-funktioner och, ännu viktigare, bereda oss för framgång.

Låt oss först flytta in vår dataladdning från vår laddare +page.server.js till en API -rutt. Vi skapar en +server.js fil i routes/api/todos, och lägg sedan till en GET fungera. Det betyder att vi nu kommer att kunna hämta (med standard GET-verbet) till /api/todos väg. Vi lägger till samma dataladdningskod som tidigare.

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

Låt oss sedan ta sidladdaren vi hade och helt enkelt byta namn på filen från +page.server.js till +page.js (eller .ts om du har byggt upp ditt projekt för att använda TypeScript). Detta ändrar vår loader till att vara en "universell" loader snarare än en server loader. SvelteKit-dokumenten förklara skillnaden, men en universell laddare körs på både servern och även klienten. En fördel för oss är att fetch anrop till vår nya slutpunkt kommer att köras direkt från vår webbläsare (efter den första laddningen), med webbläsarens ursprungliga fetch fungera. Vi kommer att lägga till standard HTTP-cache om ett tag, men för tillfället är allt vi gör är att anropa slutpunkten.

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

Låt oss nu lägga till ett enkelt formulär till vår /list sida:

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

Japp, formulär kan riktas direkt mot våra vanliga sidladdare. Nu kan vi lägga till en sökterm i sökrutan, tryck ange, och en "sökning"-term kommer att läggas till i webbadressens frågesträng, som kommer att köra vår loader igen och söka i våra att göra-objekt.

sökfunktionen
Cacha data i SvelteKit

Låt oss också öka förseningen i vår todoData.js fil i /lib/data. Detta kommer att göra det enkelt att se när data är och inte cachelagras när vi arbetar igenom detta inlägg.

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

Kom ihåg att hela koden för detta inlägg är allt på GitHub, om du behöver referera till det.

Grundläggande cachelagring

Låt oss komma igång genom att lägga till lite cachning i vår /api/todos slutpunkt. Vi går tillbaka till vår +server.js fil och lägg till vår första cache-kontrollhuvud.

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

...vilket kommer att låta hela funktionen se ut så här:

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 kommer att titta på manuell ogiltigförklaring inom kort, men allt den här funktionen säger är att cachelagra dessa API-anrop i 60 sekunder. Ställ in detta till vad du vill, och beroende på ditt användningsfall, stale-while-revalidate kan också vara värt att titta på.

Och precis som det, cachelagras våra frågor.

Cache i DevTools.
Cacha data i SvelteKit

Anmärkningar se till att du avmarkera kryssrutan som inaktiverar cachning i utvecklarverktyg.

Kom ihåg att om din första navigering på appen är listsidan, kommer dessa sökresultat att cachelagras internt till SvelteKit, så förvänta dig inte att se något i DevTools när du återvänder till den sökningen.

Vad är cachat och var

Vår allra första server-renderade laddning av vår app (förutsatt att vi börjar vid /list sida) kommer att hämtas på servern. SvelteKit kommer att serialisera och skicka dessa data till vår klient. Vad mer är, kommer det att observera Cache-Control rubrik på svaret, och kommer att veta att använda denna cachade data för det slutpunktsanropet inom cachefönstret (som vi satte till 60 sekunder i exemplet).

Efter den första laddningen, när du börjar söka på sidan, bör du se nätverksförfrågningar från din webbläsare till /api/todos lista. När du söker efter saker du redan har sökt efter (inom de senaste 60 sekunderna), bör svaren laddas omedelbart eftersom de är cachade.

Det som är speciellt coolt med det här tillvägagångssättet är att eftersom detta är cachelagring via webbläsarens inbyggda cachelagring, kan dessa anrop (beroende på hur du hanterar cache-bussningen vi ska titta på) fortsätta att cache även om du laddar om sidan (till skillnad från initial server-side load, som alltid kallar slutpunkten färsk, även om den gjorde det inom de senaste 60 sekunderna).

Uppenbarligen kan data ändras när som helst, så vi behöver ett sätt att rensa den här cachen manuellt, vilket vi ska titta på härnäst.

Cache-ogiltigförklaring

Just nu kommer data att cachelagras i 60 sekunder. Oavsett vad, efter en minut kommer färsk data att hämtas från vår databutik. Du kanske vill ha en kortare eller längre tidsperiod, men vad händer om du muterar vissa data och vill rensa din cache omedelbart så att din nästa fråga kommer att vara uppdaterad? Vi löser detta genom att lägga till ett query-busting-värde till webbadressen vi skickar till vår nya /todos slutpunkt.

Låt oss lagra detta cache-busting-värde i en cookie. Det värdet kan ställas in på servern men ändå läsas på klienten. Låt oss titta på några exempelkoder.

Vi kan skapa en +layout.server.js fil i själva roten av vår routes mapp. Detta kommer att köras vid start av applikationen och är ett perfekt ställe att ställa in ett initialt cookievärde.

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 kanske har lagt märke till isDataRequest värde. Kom ihåg att layouter kommer att köras igen när som helst klientkodanrop invalidate(), eller när som helst vi kör en serveråtgärd (förutsatt att vi inte stänger av standardbeteende). isDataRequest indikerar dessa omkörningar, så vi ställer bara in cookien om så är fallet false; annars skickar vi med det som redan finns.

Smakämnen httpOnly: false flaggan är också viktig. Detta gör att vår kundkod kan läsa in dessa cookievärden document.cookie. Detta skulle normalt vara ett säkerhetsproblem, men i vårt fall är dessa meningslösa siffror som tillåter oss att cache eller cache-bust.

Läser cache-värden

Vår universallastare är vad som kallas vår /todos slutpunkt. Detta körs på servern eller klienten, och vi måste läsa det cachevärdet som vi just ställt in oavsett var vi är. Det är relativt enkelt om vi är på servern: vi kan ringa await parent() för att hämta data från överordnade layouter. Men på klienten måste vi använda lite bruttokod för att analysera 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] ?? "";
};

Lyckligtvis behöver vi det bara en gång.

Skickar ut cachevärdet

Men nu måste vi sända detta värde för vår /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 en check i den för att se om vi är på klienten (genom att kontrollera typen av dokument), och returnerar ingenting om vi är det, då vet vi att vi är på servern. Sedan använder den värdet från vår layout.

Bryter cachen

Men hur uppdaterar vi faktiskt det cache-busting-värdet när vi behöver? Eftersom det är lagrat i en cookie kan vi kalla det så här från vilken serveråtgärd som helst:

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

Genomförandet

Det är allt utför härifrån; vi har gjort det hårda arbetet. Vi har täckt de olika primitiva webbplattformarna vi behöver, samt var de tar vägen. Nu ska vi ha lite kul och skriva ansökningskod för att knyta ihop allt.

Av skäl som kommer att bli tydliga om lite, låt oss börja med att lägga till en redigeringsfunktion till vår /list sida. Vi lägger till den här andra tabellraden för varje uppgift:

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>

Och, naturligtvis, måste vi lägga till en formuläråtgärd för vår /list sida. Åtgärder kan bara gå in .server sidor, så vi lägger till en +page.server.js i vår /list mapp. (Ja, a +page.server.js fil kan samexistera bredvid en +page.js fil.)

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 tar tag i formulärdata, tvingar fram en fördröjning, uppdaterar vår uppgift och rensar sedan, viktigast av allt, vår cache-bust-cookie.

Låt oss ge detta ett försök. Ladda om din sida och redigera sedan ett av att göra-objekten. Du bör se uppdateringen av tabellvärdet efter ett ögonblick. Om du tittar på fliken Nätverk i DevToold ser du en hämtning till /todos slutpunkt, som returnerar dina nya data. Enkelt och fungerar som standard.

Sparar data
Cacha data i SvelteKit

Omedelbara uppdateringar

Vad händer om vi vill undvika den hämtningen som sker efter att vi har uppdaterat vårt att göra-objekt, och istället uppdatera det ändrade objektet direkt på skärmen?

Det här är inte bara en fråga om prestanda. Om du söker efter "inlägg" och sedan tar bort ordet "inlägg" från något av att göra-objekten i listan, försvinner de från listan efter redigeringen eftersom de inte längre finns i den sidans sökresultat. Du kan göra UX bättre med lite smakfull animation för den spännande att göra, men låt oss säga att vi ville inte kör om sidans laddningsfunktion men rensa fortfarande cacheminnet och uppdatera den ändrade att göra så att användaren kan se redigeringen. SvelteKit gör det möjligt — låt oss se hur!

Låt oss först göra en liten förändring av vår lastare. Istället för att returnera våra att göra-föremål, låt oss returnera en skrivbar butik som innehåller våra att göra.

return { todos: writable(todos),
};

Förut hade vi tillgång till våra att göra på data prop, som vi inte äger och inte kan uppdatera. Men Svelte låter oss returnera vår data i sin egen butik (förutsatt att vi använder en universell lastare, vilket vi är). Vi behöver bara göra ytterligare en justering till vår /list sida.

Istället för det här:

{#each todos as t}

...vi måste göra detta sedan todos är själv nu en butik.:

{#each $todos as t}

Nu laddas vår data som tidigare. Men eftersom todos är en skrivbar butik, vi kan uppdatera den.

Låt oss först ge en funktion till vår use:enhance attribut:

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

Detta kommer att köras före en inlämning. Låt oss skriva det härnäst:

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

Denna funktion ger en data objekt med våra formulärdata. Vi avkastning en asynkronfunktion som körs efter vår redigering är klar. Dokumenten förklara allt detta, men genom att göra detta stänger vi av SvelteKits standardformulärhantering som skulle ha kört vår laddare igen. Det är precis vad vi vill! (Vi skulle lätt kunna få tillbaka det standardbeteendet, som dokumenten förklarar.)

Vi ringer nu update på vår todos array eftersom det är en butik. Och det är det. Efter att ha redigerat ett att göra-objekt visas våra ändringar omedelbart och vår cache rensas (som tidigare, eftersom vi ställer in ett nytt cookie-värde i vår editTodo form åtgärd). Så om vi söker och sedan navigerar tillbaka till den här sidan kommer vi att få färsk data från vår laddare, som korrekt kommer att utesluta alla uppdaterade att göra-objekt som har uppdaterats.

Koden för de omedelbara uppdateringarna finns på GitHub.

Gräver djupare

Vi kan sätta cookies i vilken serverladdningsfunktion som helst (eller serveråtgärd), inte bara rotlayouten. Så om vissa data bara används under en enda layout, eller till och med en enda sida, kan du ställa in det cookievärdet där. Dessutom, om du är det inte gör tricket jag visade just manuell uppdatering av data på skärmen, och istället vill att din laddare ska köras igen efter en mutation, då kan du alltid ställa in ett nytt cookie-värde direkt i den laddningsfunktionen utan någon kontroll mot isDataRequest. Den kommer att ställas in initialt, och när du sedan kör en serveråtgärd kommer sidlayouten automatiskt att ogiltigförklara och anropa din laddare igen, vilket återställer cache-bust-strängen innan din universella laddare anropas.

Skriver en omladdningsfunktion

Låt oss avsluta med att bygga en sista funktion: en omladdningsknapp. Låt oss ge användarna en knapp som rensar cacheminnet och sedan laddar om den aktuella frågan.

Vi lägger till en enkel formåtgärd:

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

I ett riktigt projekt skulle du förmodligen inte kopiera/klistra in samma kod för att ställa in samma cookie på samma sätt på flera ställen, men för det här inlägget kommer vi att optimera för enkelhet och läsbarhet.

Låt oss nu skapa ett formulär för att skicka till det:

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

Det fungerar!

UI efter omladdning.
Cacha data i SvelteKit

Vi skulle kunna kalla detta gjort och gå vidare, men låt oss förbättra den här lösningen lite. Specifikt, låt oss ge feedback på sidan för att berätta för användaren att omladdningen sker. Som standard ogiltigförklaras SvelteKit-åtgärder allt. Varje layout, sida, etc. i den aktuella sidans hierarki skulle laddas om. Det kan finnas vissa data som har laddats en gång i rotlayouten som vi inte behöver ogiltigförklara eller ladda om.

Så låt oss fokusera lite på saker och ting och bara ladda om våra uppgifter när vi anropar den här funktionen.

Låt oss först skicka en funktion för att förbättra:

<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 till true vid starta av denna åtgärd. Och sedan, för att åsidosätta standardbeteendet att ogiltigförklara allt, returnerar vi en async fungera. Denna funktion kommer att köras när vår serveråtgärd är klar (som bara sätter en ny cookie).

Utan detta async funktionen returnerade, skulle SvelteKit ogiltigförklara allt. Eftersom vi tillhandahåller den här funktionen kommer den att ogiltigförklara ingenting, så det är upp till oss att berätta vad den ska ladda om. Vi gör detta med invalidate fungera. Vi kallar det med ett värde på reload:todos. Denna funktion returnerar ett löfte, som löser sig när ogiltigförklaringen är klar, vid vilken tidpunkt vi ställer in reloading tillbaka till false.

Slutligen måste vi synkronisera vår laddare med denna nya reload:todos ogiltighetsvärde. Det gör vi i vår lastare med depends fungera:

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

Och det är det. depends och invalidate är otroligt användbara funktioner. Det som är coolt är det invalidate tar inte bara godtyckliga värderingar vi tillhandahåller som vi gjorde. Vi kan också tillhandahålla en URL, som SvelteKit kommer att spåra, och ogiltigförklara alla laddare som är beroende av den URL:en. För det ändamålet, om du undrar om vi skulle kunna hoppa över samtalet till depends och ogiltigförklara vår /api/todos endpoint helt och hållet, du kan, men du måste tillhandahålla exakt URL, inklusive search term (och vårt cachevärde). Så du kan antingen sätta ihop webbadressen för den aktuella sökningen eller matcha sökvägens namn, så här:

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

Personligen hittar jag lösningen som använder depends mer tydligt och enkelt. Men se dokumenten för mer info, naturligtvis, och bestäm själv.

Om du vill se omladdningsknappen i funktion finns koden för den denna gren av repan.

Avskiljande tankar

Det här var ett långt inlägg, men förhoppningsvis inte överväldigande. Vi gick in på olika sätt vi kan cache-data när vi använder SvelteKit. Mycket av detta var bara en fråga om att använda webbplattformsprimitiver för att lägga till rätt cache och cookie-värden, vars kunskap kommer att tjäna dig i webbutveckling i allmänhet, utöver bara SvelteKit.

Dessutom är detta något du absolut behöver inte hela tiden. Förmodligen bör du bara sträcka dig efter den här typen av avancerade funktioner när du faktiskt behöver dem. Om din databutik tillhandahåller data snabbt och effektivt, och du inte har att göra med någon form av skalningsproblem, är det ingen mening med att svälla din applikationskod med onödig komplexitet och göra de saker vi pratade om här.

Som alltid, skriv tydlig, ren, enkel kod och optimera när det behövs. Syftet med det här inlägget var att ge dig dessa optimeringsverktyg när du verkligen behöver dem. Jag hoppas att du tyckte om det!

Tidsstämpel:

Mer från CSS-tricks