Xlera8

Bufre data i SvelteKit

My Forrige innlegg var en bred oversikt over SvelteKit hvor vi så hvilket flott verktøy det er for webutvikling. Dette innlegget vil avsløre det vi gjorde der og dykke inn i alle utvikleres favorittemne: caching. Så husk å lese det siste innlegget mitt hvis du ikke allerede har gjort det. Koden for dette innlegget er tilgjengelig på GitHub, i tillegg til en live demo.

Dette innlegget handler om datahåndtering. Vi vil legge til litt rudimentær søkefunksjonalitet som vil endre sidens søkestreng (ved å bruke innebygde SvelteKit-funksjoner), og utløse sidens loader på nytt. Men i stedet for bare å spørre på nytt i vår (imaginære) database, vil vi legge til noe caching, slik at gjensøking av tidligere søk (eller ved å bruke tilbake-knappen) vil vise tidligere hentede data raskt fra cache. Vi skal se på hvordan du kontrollerer hvor lenge de bufrede dataene forblir gyldige og, enda viktigere, hvordan du manuelt ugyldiggjør alle bufrede verdier. Som prikken over i-en vil vi se på hvordan vi manuelt kan oppdatere dataene på gjeldende skjerm, klientsiden, etter en mutasjon, mens vi fortsatt renser cachen.

Dette blir et lengre og vanskeligere innlegg enn det meste av det jeg vanligvis skriver siden vi dekker vanskeligere emner. Dette innlegget vil i hovedsak vise deg hvordan du implementerer vanlige funksjoner i populære dataverktøy som reagere-spørring; men i stedet for å hente inn et eksternt bibliotek, bruker vi bare nettplattformen og SvelteKit-funksjonene.

Dessverre er funksjonene på nettplattformen litt lavere, så vi kommer til å gjøre litt mer arbeid enn du kanskje er vant til. Fordelen er at vi ikke trenger noen eksterne biblioteker, noe som vil bidra til å holde buntstørrelsene fine og små. Vennligst ikke bruk tilnærmingene jeg skal vise deg med mindre du har en god grunn til det. Bufring er lett å ta feil, og som du vil se, er det litt kompleksitet som vil resultere i applikasjonskoden din. Forhåpentligvis er datalageret ditt raskt, og brukergrensesnittet ditt er bra, slik at SvelteKit bare alltid kan be om dataene den trenger for en gitt side. Hvis det er det, la det være. Nyt enkelheten. Men dette innlegget vil vise deg noen triks for når det slutter å være tilfelle.

Apropos react-query, det ble nettopp løslatt for Svelte! Så hvis du finner deg selv lener på manuelle caching-teknikker mye, sørg for å sjekke det prosjektet, og se om det kan hjelpe.

Setter opp

Før vi begynner, la oss gjøre noen små endringer i koden vi hadde før. Dette vil gi oss en unnskyldning for å se noen andre SvelteKit-funksjoner og, enda viktigere, sette oss opp for suksess.

Først, la oss flytte datainnlastingen fra lasteren inn +page.server.js til en API-rute. Vi lager en +server.js filen i routes/api/todos, og legg deretter til en GET funksjon. Dette betyr at vi nå vil kunne hente (ved å bruke standard GET-verb) til /api/todos sti. Vi legger til den samme datainnlastingskoden 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);
}

La oss deretter ta sidelasteren vi hadde, og bare gi nytt navn til filen fra +page.server.js til +page.js (eller .ts hvis du har stillaset prosjektet ditt til å bruke TypeScript). Dette endrer lasteren vår til å være en "universell" laster i stedet for en serverlaster. SvelteKit-dokumentene forklare forskjellen, men en universell laster kjører på både serveren og klienten. En fordel for oss er at fetch call into vårt nye endepunkt vil kjøre rett fra nettleseren vår (etter den første innlastingen), ved å bruke nettleserens opprinnelige fetch funksjon. Vi legger til standard HTTP-bufring om litt, men foreløpig er alt vi skal gjøre å kalle endepunktet.

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

La oss nå legge til et enkelt skjema i vår /list side:

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

Jepp, skjemaer kan målrettes direkte mot våre vanlige sidelastere. Nå kan vi legge til et søkeord i søkeboksen, trykk Enter, og en "søk"-term vil bli lagt til URL-adressens søkestreng, som vil kjøre lasteren vår på nytt og søke i oppgavene våre.

søke~~POS=TRUNC skjema~~POS=HEADCOMP

La oss også øke forsinkelsen i vår todoData.js filen i /lib/data. Dette vil gjøre det enkelt å se når data er og ikke er bufret mens vi jobber gjennom dette innlegget.

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

Husk at hele koden for dette innlegget er alt på GitHub, hvis du trenger å referere til det.

Grunnleggende caching

La oss komme i gang ved å legge til litt caching i vår /api/todos endepunkt. Vi går tilbake til vår +server.js fil og legg til vår første cache-control header.

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

…som vil la hele funksjonen se slik ut:

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 skal snart se på manuell ugyldiggjøring, men alt denne funksjonen sier er å bufre disse API-kallene i 60 sekunder. Sett dette til hva du vil, og avhengig av din brukssituasjon, stale-while-revalidate kan også være verdt å se nærmere på.

Og akkurat slik bufres spørsmålene våre.

Buffer i DevTools.

Merknader vær sikker på at du fjern merket avmerkingsboksen som deaktiverer hurtigbufring i utviklerverktøy.

Husk at hvis den første navigasjonen din på appen er listesiden, vil disse søkeresultatene bli bufret internt til SvelteKit, så ikke forvent å se noe i DevTools når du går tilbake til det søket.

Hva er bufret, og hvor

Vår aller første servergjengitte innlasting av appen vår (forutsatt at vi starter ved /list side) vil bli hentet på serveren. SvelteKit vil serialisere og sende disse dataene ned til vår klient. Dessuten vil den observere Cache-Control header på svaret, og vil vite å bruke disse bufrede dataene for det endepunktanropet i hurtigbuffervinduet (som vi satte til 60 sekunder i eksempelet).

Etter den første innlastingen, når du begynner å søke på siden, bør du se nettverksforespørsler fra nettleseren din til /api/todos liste. Når du søker etter ting du allerede har søkt etter (i løpet av de siste 60 sekundene), bør svarene lastes umiddelbart siden de er bufret.

Det som er spesielt kult med denne tilnærmingen er at siden dette er caching via nettleserens opprinnelige caching, kan disse anropene (avhengig av hvordan du administrerer cache-bustingen vi skal se på) fortsette å cache selv om du laster inn siden på nytt (i motsetning til innledende belastning på serversiden, som alltid kaller endepunktet ferskt, selv om det gjorde det i løpet av de siste 60 sekundene).

Data kan selvsagt endres når som helst, så vi trenger en måte å rense denne cachen manuelt, som vi skal se på neste gang.

Ugyldig cache

Akkurat nå vil data bli bufret i 60 sekunder. Uansett hva, etter et minutt vil ferske data bli hentet fra datalageret vårt. Du vil kanskje ha en kortere eller lengre tidsperiode, men hva skjer hvis du muterer noen data og vil tømme hurtigbufferen umiddelbart slik at neste spørring vil være oppdatert? Vi løser dette ved å legge til en spørringsavbruddsverdi i URL-en vi sender til vår nye /todos endepunkt.

La oss lagre denne cache-busting-verdien i en informasjonskapsel. Denne verdien kan settes på serveren, men fortsatt leses på klienten. La oss se på noen eksempelkode.

Vi kan lage en +layout.server.js fil i selve roten av vår routes mappe. Dette vil kjøre ved oppstart av applikasjonen, og er et perfekt sted å angi en innledende verdi for informasjonskapsler.

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 kanskje lagt merke til isDataRequest verdi. Husk at oppsett kjøres på nytt når som helst klientkodeanrop invalidate(), eller når som helst vi kjører en serverhandling (forutsatt at vi ikke slår av standardatferd). isDataRequest indikerer disse omkjøringene, og derfor setter vi bare informasjonskapselen hvis det er det false; ellers sender vi med det som allerede er der.

De httpOnly: false flagget er også viktig. Dette gjør at vår klientkode kan lese disse informasjonskapselverdiene inn document.cookie. Dette vil normalt være et sikkerhetsproblem, men i vårt tilfelle er dette meningsløse tall som lar oss cache eller cache bust.

Leser cache-verdier

Vår universallaster er det som kaller vår /todos endepunkt. Dette kjører på serveren eller klienten, og vi må lese cacheverdien vi nettopp har satt opp uansett hvor vi er. Det er relativt enkelt hvis vi er på serveren: vi kan ringe await parent() for å hente dataene fra overordnede layouter. Men på klienten må vi bruke litt bruttokode for å analysere 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 trenger vi det bare én gang.

Sender ut cache-verdien

Men nå må vi send denne verdien for vår /todos endepunkt.

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 sjekk i den for å se om vi er på klienten (ved å sjekke typen dokument), og returnerer ingenting hvis vi er det, da vet vi at vi er på serveren. Da bruker den verdien fra layouten vår.

Buster cachen

Men hvordan oppdaterer vi faktisk cache-busting-verdien når vi trenger det? Siden den er lagret i en informasjonskapsel, kan vi kalle den slik fra hvilken som helst serverhandling:

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

Implementeringen

Det går nedoverbakke herfra; vi har gjort det harde arbeidet. Vi har dekket de ulike nettplattformprimitivene vi trenger, samt hvor de går. La oss nå ha det gøy og skrive søknadskode for å knytte det hele sammen.

Av grunner som vil bli tydelige om litt, la oss starte med å legge til en redigeringsfunksjonalitet til vår /list side. Vi legger til denne andre tabellraden for hver gjøremål:

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 må vi legge til en skjemahandling for vår /list side. Handlinger kan bare gå inn .server sider, så vi legger til en +page.server.js i vår /list mappe. (Ja, a +page.server.js fil kan eksistere side om side ved siden av 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 tak i skjemadataene, tvinger fram en forsinkelse, oppdaterer gjøremålet vårt, og så, viktigst av alt, tømmer vi cache-bust-informasjonskapselen.

La oss prøve dette. Last inn siden på nytt, og rediger deretter ett av gjøremålene. Du bør se tabellverdioppdateringen etter et øyeblikk. Hvis du ser i Network-fanen i DevToold, vil du se en henting til /todos endepunkt, som returnerer de nye dataene dine. Enkelt, og fungerer som standard.

Lagrer data

Umiddelbare oppdateringer

Hva om vi ønsker å unngå hentingen som skjer etter at vi har oppdatert oppgaveelementet vårt, og i stedet oppdaterer det endrede elementet rett på skjermen?

Dette er ikke bare et spørsmål om ytelse. Hvis du søker etter «innlegg» og deretter fjerner ordet «innlegg» fra noen av gjøremålene i listen, vil de forsvinne fra listen etter redigeringen siden de ikke lenger er i den sidens søkeresultater. Du kan gjøre brukeropplevelsen bedre med litt smakfull animasjon for spennende gjøremål, men la oss si at vi ønsket å ikke kjør sidens lastefunksjon på nytt, men tøm fortsatt hurtigbufferen og oppdater den endrede gjøremålet slik at brukeren kan se redigeringen. SvelteKit gjør det mulig - la oss se hvordan!

Først, la oss gjøre en liten endring på lasteren vår. I stedet for å returnere oppgavene våre, la oss returnere en skrivbar butikk som inneholder gjøremålene våre.

return { todos: writable(todos),
};

Før hadde vi tilgang til gjøremålene våre på data prop, som vi ikke eier og ikke kan oppdatere. Men Svelte lar oss returnere dataene våre i deres egen butikk (forutsatt at vi bruker en universallaster, som vi er). Vi trenger bare å gjøre en finjustering til /list side.

Istedenfor dette:

{#each todos as t}

...vi må gjøre dette siden todos er selv nå en butikk.:

{#each $todos as t}

Nå lastes dataene våre som før. Men siden todos er en skrivbar butikk, kan vi oppdatere den.

Først, la oss gi en funksjon til vår use:enhance Egenskap:

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

Dette vil kjøre før en innsending. La oss skrive det neste:

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 funksjonen gir en data objekt med skjemadataene våre. Vi retur en asynkronfunksjon som vil kjøre etter redigeringen vår er ferdig. Dokumentene forklare alt dette, men ved å gjøre dette, slår vi av SvelteKits standard skjemahåndtering som ville ha kjørt lasteren vår på nytt. Dette er akkurat det vi ønsker! (Vi kan enkelt få tilbake den standardoppførselen, som dokumentene forklarer.)

Vi ringer nå update på vår todos rekke siden det er en butikk. Og det er det. Etter å ha redigert et gjøremål, vises endringene våre umiddelbart og bufferen vår tømmes (som før, siden vi angir en ny verdi for informasjonskapsler i vår editTodo form handling). Så hvis vi søker og deretter navigerer tilbake til denne siden, får vi ferske data fra lasteren vår, som korrekt ekskluderer alle oppdaterte gjøremålselementer som ble oppdatert.

Koden for de umiddelbare oppdateringene er tilgjengelig på GitHub.

Graver dypere

Vi kan sette informasjonskapsler i hvilken som helst serverinnlastingsfunksjon (eller serverhandling), ikke bare rotoppsettet. Så hvis noen data bare brukes under en enkelt layout, eller til og med en enkelt side, kan du angi denne informasjonskapselverdien der. Dessuten, hvis du er det ikke gjør trikset jeg nettopp viste manuell oppdatering av data på skjermen, og i stedet vil at lasteren skal kjøres på nytt etter en mutasjon, så kan du alltid sette en ny verdi for informasjonskapsler rett i den lastefunksjonen uten noen sjekk mot isDataRequest. Det vil settes til å begynne med, og hver gang du kjører en serverhandling vil sideoppsettet automatisk ugyldiggjøre og kalle opp lasteren på nytt, og stille inn hurtigbufferstrengen før den universelle lasteren kalles opp.

Skrive en reload-funksjon

La oss avslutte med å bygge en siste funksjon: en reload-knapp. La oss gi brukerne en knapp som vil tømme hurtigbufferen og deretter laste inn gjeldende spørring på nytt.

Vi legger til en enkel formhandling:

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

I et ekte prosjekt ville du sannsynligvis ikke kopiert/limt inn den samme koden for å sette den samme informasjonskapselen på samme måte på flere steder, men for dette innlegget vil vi optimalisere for enkelhet og lesbarhet.

La oss nå lage et skjema for å legge ut på det:

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

Det fungerer!

UI etter omlasting.

Vi kan kalle dette gjort og gå videre, men la oss forbedre denne løsningen litt. Nærmere bestemt, la oss gi tilbakemelding på siden for å fortelle brukeren at omlastingen skjer. Som standard blir SvelteKit-handlinger ugyldige alt. Hver layout, side osv. i gjeldende sides hierarki vil lastes inn på nytt. Det kan være noen data som er lastet inn én gang i rotoppsettet som vi ikke trenger å ugyldiggjøre eller laste inn på nytt.

Så la oss fokusere ting litt, og bare laste inn gjøremålene våre på nytt når vi kaller denne funksjonen.

Først, la oss sende en funksjon for å 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 setter en ny reloading variabel til trueBegynn av denne handlingen. Og så, for å overstyre standardoppførselen til å ugyldiggjøre alt, returnerer vi en async funksjon. Denne funksjonen vil kjøre når serverhandlingen vår er fullført (som bare setter en ny informasjonskapsel).

Uten dette async funksjonen returnerte, ville SvelteKit ugyldiggjøre alt. Siden vi tilbyr denne funksjonen, vil den ikke gjøre noe ugyldig, så det er opp til oss å fortelle den hva den skal laste på nytt. Vi gjør dette med invalidate funksjon. Vi kaller det med en verdi på reload:todos. Denne funksjonen returnerer et løfte, som løser seg når ugyldiggjøringen er fullført, på hvilket tidspunkt vi setter reloading tilbake til false.

Til slutt må vi synkronisere lasteren vår med denne nye reload:todos ugyldighetsverdi. Det gjør vi i lasteren vår med depends funksjon:

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

Og det er det. depends og invalidate er utrolig nyttige funksjoner. Det som er kult er det invalidate tar ikke bare vilkårlige verdier vi gir som vi gjorde. Vi kan også gi en URL, som SvelteKit vil spore, og ugyldiggjøre eventuelle lastere som er avhengige av den URLen. For det formål, hvis du lurer på om vi kan hoppe over samtalen til depends og ugyldiggjøre vår /api/todos endepunkt helt, du kan, men du må oppgi eksakte URL, inkludert search term (og cacheverdien vår). Så du kan enten sette sammen URL-en for gjeldende søk, eller matche på banenavnet, slik:

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

Personlig finner jeg løsningen som bruker depends mer eksplisitt og enkelt. Men se dokumentene for mer info, selvfølgelig, og avgjør selv.

Hvis du vil se reload-knappen i aksjon, er koden for den inne denne grenen av repoen.

Avskjedstanker

Dette ble et langt innlegg, men forhåpentligvis ikke overveldende. Vi dykket inn på forskjellige måter vi kan hurtigbufre data når vi bruker SvelteKit. Mye av dette var bare et spørsmål om å bruke nettplattformprimitiver for å legge til riktig cache, og verdier for informasjonskapsler, kunnskap om hvilke vil tjene deg i webutvikling generelt, utover bare SvelteKit.

Dessuten er dette noe du absolutt trenger ikke hele tiden. Uten tvil bør du bare strekke deg etter denne typen avanserte funksjoner når du faktisk trenger dem. Hvis datalageret ditt leverer data raskt og effektivt, og du ikke har noen form for skaleringsproblemer, er det ingen vits i å fylle opp applikasjonskoden med unødvendig kompleksitet ved å gjøre de tingene vi snakket om her.

Som alltid, skriv klar, ren, enkel kode, og optimer når det er nødvendig. Hensikten med dette innlegget var å gi deg disse optimaliseringsverktøyene når du virkelig trenger dem. Jeg håper du likte det!

Chat med oss

Hei der! Hvordan kan jeg hjelpe deg?