Xlera8

Gegevens cachen in SvelteKit

My vorige post was een breed overzicht van SvelteKit waar we zagen wat een geweldige tool het is voor webontwikkeling. Dit bericht splitst af wat we daar hebben gedaan en duikt in het favoriete onderwerp van elke ontwikkelaar: caching. Dus, zorg ervoor dat je mijn laatste bericht leest als je dat nog niet hebt gedaan. De code voor dit bericht is beschikbaar op GitHubevenals een live demo.

Dit bericht gaat helemaal over gegevensverwerking. We zullen wat rudimentaire zoekfunctionaliteit toevoegen die de zoekreeks van de pagina wijzigt (met behulp van ingebouwde SvelteKit-functies) en de lader van de pagina opnieuw activeert. Maar in plaats van gewoon onze (denkbeeldige) database opnieuw te bevragen, voegen we wat caching toe, zodat het opnieuw doorzoeken van eerdere zoekopdrachten (of het gebruik van de terugknop) eerder opgehaalde gegevens snel uit de cache zal tonen. We zullen bekijken hoe u kunt bepalen hoe lang de gegevens in de cache geldig blijven en, nog belangrijker, hoe u alle waarden in de cache handmatig ongeldig kunt maken. Als kers op de taart bekijken we hoe we de gegevens op het huidige scherm, client-side, handmatig kunnen bijwerken na een mutatie, terwijl we nog steeds de cache opschonen.

Dit wordt een langer, moeilijker bericht dan het meeste van wat ik gewoonlijk schrijf, aangezien we moeilijkere onderwerpen behandelen. Dit bericht laat je in wezen zien hoe je gemeenschappelijke functies van populaire gegevenshulpprogramma's kunt implementeren, zoals reageer-query; maar in plaats van een externe bibliotheek binnen te halen, gebruiken we alleen het webplatform en de SvelteKit-functies.

Helaas zijn de functies van het webplatform van een wat lager niveau, dus we zullen wat meer werk verzetten dan je misschien gewend bent. Het voordeel is dat we geen externe bibliotheken nodig hebben, waardoor bundels mooi en klein blijven. Gebruik alsjeblieft niet de benaderingen die ik je ga laten zien, tenzij je daar een goede reden voor hebt. Caching is gemakkelijk verkeerd te doen, en zoals u zult zien, is er een beetje complexiteit die zal resulteren in uw applicatiecode. Hopelijk is uw gegevensopslag snel en is uw gebruikersinterface in orde, zodat SvelteKit altijd de gegevens kan opvragen die het nodig heeft voor een bepaalde pagina. Als dat zo is, laat het dan met rust. Geniet van de eenvoud. Maar dit bericht zal je enkele trucs laten zien voor wanneer dat niet meer het geval is.

Over reactie-query gesproken, het is net vrijgelaten voor Slim! Dus als u merkt dat u leunt op handmatige cachingtechnieken veel, zorg ervoor dat je dat project bekijkt en kijk of het kan helpen.

Opzetten

Laten we, voordat we beginnen, een paar kleine wijzigingen aanbrengen in de code die we eerder hadden. Dit geeft ons een excuus om enkele andere SvelteKit-functies te zien en, nog belangrijker, ons klaar te stomen voor succes.

Laten we eerst het laden van onze gegevens van onze lader naar binnen verplaatsen +page.server.js een API-route. We maken een +server.js bestand in routes/api/todos, en voeg dan a . toe GET functie. Dit betekent dat we nu kunnen ophalen (met behulp van het standaard GET-werkwoord) naar de /api/todos pad. We voegen dezelfde code voor het laden van gegevens toe als voorheen.

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

Laten we vervolgens de paginalader nemen die we hadden en het bestand eenvoudig hernoemen van +page.server.js naar +page.js (of .ts als je je project hebt opgezet om TypeScript te gebruiken). Dit verandert onze lader in een "universele" lader in plaats van een serverlader. De SvelteKit-documenten leg het verschil uit, maar een universele lader draait zowel op de server als op de client. Een voordeel voor ons is dat de fetch de oproep naar ons nieuwe eindpunt wordt rechtstreeks vanuit onze browser uitgevoerd (na de eerste keer laden), met behulp van de native browser fetch functie. We zullen binnenkort standaard HTTP-caching toevoegen, maar voor nu hoeven we alleen maar het eindpunt aan te roepen.

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

Laten we nu een eenvoudig formulier toevoegen aan onze /list pagina:

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

Ja, formulieren kunnen rechtstreeks worden getarget op onze normale paginaladers. Nu kunnen we een zoekterm in het zoekvak toevoegen, hit Enter, en een "zoek"-term zal worden toegevoegd aan de queryreeks van de URL, die onze lader opnieuw zal starten en onze actiepunten zal doorzoeken.

zoekformulier

Laten we ook de vertraging in onze vergroten todoData.js bestand in /lib/data. Dit maakt het gemakkelijk om te zien wanneer gegevens wel en niet in de cache worden opgeslagen terwijl we aan dit bericht werken.

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

Onthoud dat de volledige code voor dit bericht is allemaal op GitHub, als u ernaar moet verwijzen.

Basis caching

Laten we beginnen door wat caching toe te voegen aan ons /api/todos eindpunt. We gaan terug naar onze +server.js bestand en voeg onze eerste cache-control header toe.

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

...waardoor de hele functie er zo uitziet:

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

We zullen binnenkort kijken naar handmatige ongeldigverklaring, maar het enige dat deze functie zegt, is om deze API-aanroepen gedurende 60 seconden in de cache op te slaan. Stel dit in op wat je maar wilt, en afhankelijk van uw gebruikssituatie, stale-while-revalidate misschien ook de moeite waard om naar te kijken.

En zomaar worden onze vragen in de cache opgeslagen.

Cache in DevTools.

Note zorg ervoor dat je vinkje uit het selectievakje dat caching in ontwikkelaarstools uitschakelt.

Onthoud dat als uw eerste navigatie op de app de lijstpagina is, die zoekresultaten intern in de cache van SvelteKit worden opgeslagen, dus verwacht niet dat u iets in DevTools ziet wanneer u terugkeert naar die zoekopdracht.

Wat wordt in de cache opgeslagen en waar

Onze allereerste, door de server gegenereerde belasting van onze app (ervan uitgaande dat we beginnen bij het /list pagina) wordt opgehaald op de server. SvelteKit zal deze gegevens serialiseren en naar onze klant sturen. Bovendien zal het de Cache-Control hoofd op het antwoord, en zal weten om deze gegevens in de cache te gebruiken voor die eindpuntaanroep binnen het cachevenster (dat we in het voorbeeld op 60 seconden hebben ingesteld).

Na die eerste keer laden, wanneer u begint met zoeken op de pagina, zou u netwerkverzoeken van uw browser naar de /api/todos lijst. Terwijl u zoekt naar dingen waarnaar u al heeft gezocht (in de afgelopen 60 seconden), zouden de antwoorden onmiddellijk moeten worden geladen omdat ze in de cache zijn opgeslagen.

Wat vooral cool is aan deze aanpak, is dat, aangezien dit caching is via de native caching van de browser, deze oproepen (afhankelijk van hoe je de cachebusting beheert waar we naar kijken) kunnen blijven cachen, zelfs als je de pagina opnieuw laadt (in tegenstelling tot de initiële server-side load, die het eindpunt altijd vers aanroept, zelfs als dit in de afgelopen 60 seconden is gebeurd).

Het is duidelijk dat gegevens op elk moment kunnen veranderen, dus we hebben een manier nodig om deze cache handmatig te wissen, wat we hierna zullen bekijken.

Cache ongeldig maken

Op dit moment worden gegevens gedurende 60 seconden in de cache opgeslagen. Wat er ook gebeurt, na een minuut worden nieuwe gegevens uit onze datastore gehaald. U wilt misschien een kortere of langere periode, maar wat gebeurt er als u enkele gegevens muteert en uw cache onmiddellijk wilt wissen, zodat uw volgende zoekopdracht up-to-date is? We lossen dit op door een query-busting waarde toe te voegen aan de URL die we naar onze nieuwe /todos eindpunt.

Laten we deze cachebusting-waarde opslaan in een cookie. Die waarde kan op de server worden ingesteld, maar nog steeds op de client worden gelezen. Laten we eens kijken naar wat voorbeeldcode.

We kunnen een +layout.server.js bestand aan de basis van onze routes map. Dit wordt uitgevoerd bij het opstarten van de applicatie en is een perfecte plek om een ​​initiële cookiewaarde in te stellen.

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

Je hebt misschien de isDataRequest waarde. Onthoud dat lay-outs opnieuw worden uitgevoerd wanneer de clientcode wordt aangeroepen invalidate(), of wanneer we een serveractie uitvoeren (ervan uitgaande dat we standaardgedrag niet uitschakelen). isDataRequest geeft die herhalingen aan, en daarom plaatsen we de cookie alleen als dat zo is false; anders sturen we mee wat er al is.

De httpOnly: false vlag is ook belangrijk. Hierdoor kan onze klantcode deze cookiewaarden inlezen document.cookie. Dit zou normaal gesproken een beveiligingsprobleem zijn, maar in ons geval zijn dit betekenisloze cijfers waarmee we kunnen cachen of cachebusten kunnen maken.

Cachewaarden lezen

Onze universele lader is wat ons noemt /todos eindpunt. Dit draait op de server of de client en we moeten die cachewaarde lezen die we zojuist hebben ingesteld, waar we ook zijn. Als we op de server zitten is het relatief eenvoudig: we kunnen bellen await parent() om de gegevens uit bovenliggende lay-outs te halen. Maar op de client hebben we wat grove code nodig om te ontleden 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] ?? "";
};

Gelukkig hebben we het maar één keer nodig.

De cachewaarde verzenden

Maar nu moeten we sturen deze waarde aan onze /todos eindpunt.

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') heeft een controle om te zien of we op de client zijn (door het type document te controleren), en retourneert niets als we dat zijn, op welk moment we weten dat we op de server zijn. Vervolgens gebruikt het de waarde uit onze lay-out.

De cache doorbreken

Maar hoe updaten we die cachebusting-waarde echt wanneer dat nodig is? Omdat het in een cookie is opgeslagen, kunnen we het vanuit elke serveractie zo noemen:

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

De implementatie

Vanaf hier gaat het bergafwaarts; we hebben het harde werk gedaan. We hebben de verschillende primitieven van het webplatform behandeld die we nodig hebben, evenals waar ze naartoe gaan. Laten we nu wat plezier hebben en applicatiecode schrijven om alles samen te binden.

Om redenen die zo meteen duidelijk zullen worden, laten we beginnen met het toevoegen van een bewerkingsfunctionaliteit aan onze /list bladzijde. We voegen deze tweede tabelrij toe voor elke taak:

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>

En natuurlijk moeten we een formulieractie toevoegen voor onze /list bladzijde. Acties kunnen er alleen in .server pagina's, dus we zullen een toevoegen +page.server.js in onze /list map. (Ja een +page.server.js bestand naast een +page.js het dossier.)

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

We pakken de formuliergegevens, forceren een vertraging, werken onze todo bij en, belangrijker nog, wissen onze cachebust-cookie.

Laten we dit proberen. Laad uw pagina opnieuw en bewerk vervolgens een van de actiepunten. U zou de tabelwaarde-update na een moment moeten zien. Als u op het tabblad Netwerk in DevToold kijkt, ziet u een ophaalactie naar het /todos eindpunt, dat uw nieuwe gegevens retourneert. Simpel, en werkt standaard.

Gegevens opslaan

Onmiddellijke updates

Wat als we dat ophalen dat gebeurt nadat we ons actiepunt hebben bijgewerkt, willen vermijden en in plaats daarvan het gewijzigde item direct op het scherm willen bijwerken?

Dit is niet alleen een kwestie van prestatie. Als je zoekt naar 'posten' en vervolgens het woord 'posten' verwijdert uit een van de actiepunten in de lijst, zullen ze na de bewerking uit de lijst verdwijnen omdat ze niet langer in de zoekresultaten van die pagina staan. Je zou de UX kunnen verbeteren met wat smaakvolle animaties voor de spannende taken, maar laten we zeggen dat we dat wilden niet voer de laadfunctie van die pagina opnieuw uit, maar wis nog steeds de cache en werk de gewijzigde taak bij zodat de gebruiker de bewerking kan zien. SvelteKit maakt dat mogelijk - laten we eens kijken hoe!

Laten we eerst een kleine wijziging aanbrengen in onze lader. Laten we in plaats van onze to-do-items terug te sturen a beschrijfbare winkel met onze to-do's.

return { todos: writable(todos),
};

Vroeger hadden we toegang tot onze taken op de data prop, die niet van ons is en niet kan worden bijgewerkt. Maar Svelte laat ons onze gegevens retourneren in hun eigen winkel (ervan uitgaande dat we een universele lader gebruiken, wat we zijn). We moeten alleen nog een aanpassing aan onze maken /list pagina.

In plaats van dit:

{#each todos as t}

...we moeten dit sindsdien doen todos is zelf nu een winkel.:

{#each $todos as t}

Nu worden onze gegevens geladen zoals voorheen. Maar sinds todos een beschrijfbare winkel is, kunnen we deze updaten.

Laten we eerst een functie geven aan our use:enhance attribuut:

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

Dit wordt uitgevoerd vóór een indiening. Laten we dat hierna schrijven:

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

Deze functie zorgt voor een data bezwaar maken met onze formuliergegevens. Wij terugkeer een asynchrone functie die zal worden uitgevoerd na onze bewerking is klaar. de documenten leg dit allemaal uit, maar door dit te doen, hebben we de standaard formulierverwerking van SvelteKit afgesloten die onze lader opnieuw zou hebben uitgevoerd. Dit is precies wat we willen! (We zouden dat standaardgedrag gemakkelijk kunnen terugkrijgen, zoals de documenten uitleggen.)

We bellen nu update op onze todos array omdat het een winkel is. En dat is dat. Na het bewerken van een to-do-item, worden onze wijzigingen onmiddellijk weergegeven en wordt onze cache gewist (zoals eerder, aangezien we een nieuwe cookiewaarde hebben ingesteld in onze editTodo vormen actie). Dus als we zoeken en vervolgens teruggaan naar deze pagina, krijgen we nieuwe gegevens van onze lader, die bijgewerkte actiepunten correct uitsluit.

De code voor de onmiddellijke updates is beschikbaar op GitHub.

Dieper graven

We kunnen cookies instellen in elke serverlaadfunctie (of serveractie), niet alleen in de rootlay-out. Dus als sommige gegevens alleen onder een enkele lay-out of zelfs een enkele pagina worden gebruikt, kunt u die cookiewaarde daar instellen. Meer nog, als je dat bent niet terwijl ik de truc deed die ik zojuist liet zien, handmatig gegevens op het scherm bijwerken, en in plaats daarvan wilt dat uw lader opnieuw wordt uitgevoerd na een mutatie, dan kunt u altijd een nieuwe cookiewaarde instellen in die laadfunctie zonder enige controle tegen isDataRequest. Het zal in eerste instantie worden ingesteld, en elke keer dat u een serveractie uitvoert, wordt die pagina-indeling automatisch ongeldig en wordt uw lader opnieuw aangeroepen, waarbij de cachebust-tekenreeks opnieuw wordt ingesteld voordat uw universele lader wordt aangeroepen.

Een herlaadfunctie schrijven

Laten we afronden door nog een laatste functie te bouwen: een herlaadknop. Laten we gebruikers een knop geven waarmee de cache wordt gewist en vervolgens de huidige query opnieuw wordt geladen.

We zullen een vuil eenvoudige formulieractie toevoegen:

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

In een echt project zou je waarschijnlijk niet dezelfde code kopiëren/plakken om dezelfde cookie op meerdere plaatsen op dezelfde manier in te stellen, maar voor dit bericht zullen we optimaliseren voor eenvoud en leesbaarheid.

Laten we nu een formulier maken om ernaar te posten:

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

Dat werkt!

Gebruikersinterface na opnieuw laden.

We zouden dit klaar kunnen noemen en verder gaan, maar laten we deze oplossing een beetje verbeteren. Laten we met name feedback op de pagina geven om de gebruiker te vertellen dat het opnieuw laden plaatsvindt. Ook worden SvelteKit-acties standaard ongeldig alles. Elke lay-out, pagina, etc. in de hiërarchie van de huidige pagina zou opnieuw worden geladen. Er kunnen enkele gegevens zijn die eenmaal in de hoofdlay-out zijn geladen en die we niet ongeldig hoeven te maken of opnieuw moeten laden.

Dus laten we ons een beetje concentreren en onze taken alleen opnieuw laden als we deze functie aanroepen.

Laten we eerst een functie doorgeven om te verbeteren:

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

We stellen een nieuwe in reloading variabel naar true de begin van deze actie. En dan, om het standaardgedrag van het ongeldig maken van alles te overschrijven, retourneren we an async functie. Deze functie wordt uitgevoerd wanneer onze serveractie is voltooid (die zojuist een nieuwe cookie heeft ingesteld).

Zonder dit async functie geretourneerd, zou SvelteKit alles ongeldig maken. Aangezien we deze functie bieden, maakt het niets ongeldig, dus het is aan ons om te vertellen wat het opnieuw moet laden. Dit doen we met de invalidate functie. We noemen het met een waarde van reload:todos. Deze functie retourneert een belofte, die wordt opgelost wanneer de ongeldigverklaring is voltooid, op welk punt we instellen reloading terug naar false.

Ten slotte moeten we onze lader synchroniseren met deze nieuwe reload:todos invalidatie waarde. Dat doen we in onze loader met de depends functie:

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

En dat is dat. depends en invalidate zijn ongelooflijk handige functies. Wat cool is, is dat invalidate neemt niet alleen willekeurige waarden aan die we bieden, zoals we deden. We kunnen ook een URL opgeven, die SvelteKit zal volgen, en alle laders die afhankelijk zijn van die URL ongeldig maken. Daarom, als u zich afvraagt ​​of we het gesprek kunnen overslaan naar depends en onze ongeldig maken /api/todos eindpunt helemaal, dat kan, maar je moet de exact URL, inclusief de search termijn (en onze cachewaarde). U kunt dus ofwel de URL voor de huidige zoekopdracht samenstellen, of overeenkomen met de padnaam, zoals deze:

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

Persoonlijk vind ik de oplossing die gebruikt depends explicieter en eenvoudiger. Maar zie de documenten voor meer info natuurlijk en beslis zelf.

Als je de herlaadknop in actie wilt zien, de code ervoor is binnen deze tak van de repo.

Afscheid gedachten

Dit was een lang bericht, maar hopelijk niet overweldigend. We doken in verschillende manieren waarop we gegevens kunnen cachen bij het gebruik van SvelteKit. Veel hiervan was gewoon een kwestie van webplatform-primitieven gebruiken om de juiste cache- en cookiewaarden toe te voegen, waarvan kennis u zal helpen bij webontwikkeling in het algemeen, verder dan alleen SvelteKit.

Bovendien is dit iets wat je absoluut moet hebben niet de hele tijd nodig. Ongetwijfeld zou u alleen naar dit soort geavanceerde functies moeten grijpen als u heb ze echt nodig. Als uw datastore gegevens snel en efficiënt aanlevert en u geen enkele vorm van schaalproblemen ondervindt, heeft het geen zin om uw toepassingscode op te blazen met onnodige complexiteit om de dingen te doen waar we het hier over hadden.

Schrijf zoals altijd duidelijke, schone, eenvoudige code en optimaliseer waar nodig. Het doel van dit bericht was om u die optimalisatietools te bieden voor wanneer u ze echt nodig heeft. Ik hoop dat je het leuk vond!

Chat met ons

Hallo daar! Hoe kan ik u helpen?