Xlera8

Mise en cache des données dans SvelteKit

My post précédent était un large aperçu de SvelteKit où nous avons vu à quel point c'est un excellent outil pour le développement Web. Ce post débranchera ce que nous avons fait là-bas et plongera dans le sujet préféré de chaque développeur : la mise en cache. Alors, assurez-vous de donner une lecture à mon dernier message si vous ne l'avez pas déjà fait. Le code de ce post est disponible sur GitHub, aussi bien que une démo en direct.

Cet article concerne la gestion des données. Nous ajouterons une fonctionnalité de recherche rudimentaire qui modifiera la chaîne de requête de la page (à l'aide des fonctionnalités intégrées de SvelteKit) et déclenchera à nouveau le chargeur de la page. Mais, plutôt que de simplement réinterroger notre base de données (imaginaire), nous ajouterons une mise en cache afin que la recherche de recherches précédentes (ou l'utilisation du bouton de retour) affiche les données précédemment récupérées, rapidement, à partir du cache. Nous verrons comment contrôler la durée de validité des données mises en cache et, plus important encore, comment invalider manuellement toutes les valeurs mises en cache. Cerise sur le gâteau, nous verrons comment mettre à jour manuellement les données sur l'écran actuel, côté client, après une mutation, tout en purgeant le cache.

Ce sera un article plus long et plus difficile que la plupart de ce que j'écris habituellement puisque nous couvrons des sujets plus difficiles. Cet article vous montrera essentiellement comment implémenter les fonctionnalités courantes des utilitaires de données populaires tels que requête de réaction; mais au lieu d'utiliser une bibliothèque externe, nous n'utiliserons que la plate-forme Web et les fonctionnalités de SvelteKit.

Malheureusement, les fonctionnalités de la plate-forme Web sont un peu inférieures, nous allons donc faire un peu plus de travail que ce à quoi vous pourriez être habitué. L'avantage est que nous n'aurons pas besoin de bibliothèques externes, ce qui aidera à garder des tailles de bundles petites et agréables. S'il vous plaît, n'utilisez pas les approches que je vais vous montrer à moins que vous n'ayez une bonne raison de le faire. La mise en cache est facile à se tromper, et comme vous le verrez, il y a un peu de complexité qui se traduira par votre code d'application. J'espère que votre magasin de données est rapide et que votre interface utilisateur est correcte, ce qui permet à SvelteKit de toujours demander les données dont il a besoin pour une page donnée. Si c'est le cas, laissez-le tranquille. Profitez de la simplicité. Mais cet article vous montrera quelques astuces lorsque cela cessera d'être le cas.

En parlant de requête de réaction, il vient d'être libéré pour Svelte ! Donc, si vous vous penchez sur des techniques de mise en cache manuelles beaucoup, assurez-vous de vérifier ce projet et voyez s'il peut vous aider.

Mise en place

Avant de commencer, apportons quelques petites modifications à le code que nous avions avant. Cela nous donnera une excuse pour voir d'autres fonctionnalités de SvelteKit et, plus important encore, nous préparera au succès.

Tout d'abord, déplaçons notre chargement de données depuis notre chargeur dans +page.server.js à un Route API. Nous allons créer un +server.js fichier dans routes/api/todos, puis ajoutez un GET une fonction. Cela signifie que nous pourrons désormais récupérer (en utilisant le verbe GET par défaut) le /api/todos chemin. Nous ajouterons le même code de chargement de données qu'auparavant.

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

Ensuite, prenons le chargeur de page que nous avions, et renommez simplement le fichier de +page.server.js à +page.js (ou .ts si vous avez échafaudé votre projet pour utiliser TypeScript). Cela transforme notre chargeur en un chargeur "universel" plutôt qu'un chargeur de serveur. La documentation SvelteKit expliquer la différence, mais un chargeur universel s'exécute à la fois sur le serveur et sur le client. Un avantage pour nous est que le fetch l'appel dans notre nouveau point de terminaison s'exécutera directement à partir de notre navigateur (après le chargement initial), en utilisant le navigateur natif fetch une fonction. Nous allons ajouter la mise en cache HTTP standard dans un instant, mais pour l'instant, tout ce que nous ferons est d'appeler le point de terminaison.

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

Ajoutons maintenant un formulaire simple à notre /list page:

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

Oui, les formulaires peuvent cibler directement nos chargeurs de page normaux. Nous pouvons maintenant ajouter un terme de recherche dans le champ de recherche, appuyez sur Entrer, et un terme de « recherche » sera ajouté à la chaîne de requête de l'URL, ce qui réexécutera notre chargeur et recherchera nos éléments à faire.

Formulaire de recherche

Augmentons également le délai de notre todoData.js fichier dans /lib/data. Cela permettra de voir facilement quand les données sont et ne sont pas mises en cache pendant que nous travaillons sur cet article.

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

N'oubliez pas que le code complet de ce message est tout sur GitHub, si vous avez besoin de le référencer.

Mise en cache de base

Commençons par ajouter du cache à notre /api/todos point final. Nous reviendrons à notre +server.js fichier et ajoutez notre premier en-tête de contrôle de cache.

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

…ce qui laissera toute la fonction ressemblant à ceci :

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

Nous examinerons l'invalidation manuelle sous peu, mais tout ce que dit cette fonction est de mettre en cache ces appels d'API pendant 60 secondes. Réglez ceci sur ce que vous voulez, et selon votre cas d'utilisation, stale-while-revalidate pourrait également valoir la peine d'être examiné.

Et juste comme ça, nos requêtes sont mises en cache.

Cache dans DevTools.

Notes assurez-vous décocher la case à cocher qui désactive la mise en cache dans les outils de développement.

N'oubliez pas que si votre navigation initiale sur l'application est la page de liste, ces résultats de recherche seront mis en cache en interne dans SvelteKit, alors ne vous attendez pas à voir quoi que ce soit dans DevTools lorsque vous revenez à cette recherche.

Qu'est-ce qui est mis en cache et où

Notre tout premier chargement rendu par le serveur de notre application (en supposant que nous commencions au /list page) sera récupéré sur le serveur. SvelteKit sérialisera et enverra ces données à notre client. De plus, il observera le Cache-Control entête sur la réponse, et saura utiliser ces données mises en cache pour cet appel de point de terminaison dans la fenêtre de cache (que nous avons définie sur 60 secondes dans l'exemple put).

Après ce chargement initial, lorsque vous lancez une recherche sur la page, vous devriez voir les requêtes réseau de votre navigateur vers le /api/todos liste. Lorsque vous recherchez des éléments que vous avez déjà recherchés (au cours des 60 dernières secondes), les réponses doivent se charger immédiatement car elles sont mises en cache.

Ce qui est particulièrement intéressant avec cette approche, c'est que, puisqu'il s'agit de la mise en cache via la mise en cache native du navigateur, ces appels pourraient (selon la façon dont vous gérez le contournement du cache que nous allons examiner) continuer à se mettre en cache même si vous rechargez la page (contrairement au charge initiale côté serveur, qui appelle toujours le point de terminaison frais, même s'il l'a fait au cours des 60 dernières secondes).

Évidemment, les données peuvent changer à tout moment, nous avons donc besoin d'un moyen de purger ce cache manuellement, ce que nous verrons ensuite.

Invalidation du cache

À l'heure actuelle, les données seront mises en cache pendant 60 secondes. Quoi qu'il en soit, après une minute, de nouvelles données seront extraites de notre magasin de données. Vous voudrez peut-être une période plus courte ou plus longue, mais que se passe-t-il si vous faites muter certaines données et que vous souhaitez vider votre cache immédiatement afin que votre prochaine requête soit à jour ? Nous allons résoudre ce problème en ajoutant une valeur de contournement des requêtes à l'URL que nous envoyons à notre nouveau /todos point final.

Stockons cette valeur de contournement du cache dans un cookie. Cette valeur peut être définie sur le serveur mais toujours lue sur le client. Regardons un exemple de code.

Nous pouvons créer un +layout.server.js fichier à la racine même de notre routes dossier. Cela s'exécutera au démarrage de l'application et constitue un endroit idéal pour définir une valeur de cookie initiale.

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

Vous avez peut-être remarqué le isDataRequest évaluer. N'oubliez pas que les mises en page seront réexécutées à tout moment les appels de code client invalidate(), ou chaque fois que nous exécutons une action de serveur (en supposant que nous ne désactivons pas le comportement par défaut). isDataRequest indique ces répétitions, et donc nous ne définissons le cookie que si c'est false; sinon, nous envoyons ce qui est déjà là.

La httpOnly: false le drapeau est également important. Cela permet à notre code client de lire ces valeurs de cookie dans document.cookie. Ce serait normalement un problème de sécurité, mais dans notre cas, ce sont des nombres sans signification qui nous permettent de mettre en cache ou de casser le cache.

Lecture des valeurs du cache

Notre chargeur universel est ce qui appelle notre /todos point final. Cela s'exécute sur le serveur ou le client, et nous devons lire cette valeur de cache que nous venons de configurer, où que nous soyons. C'est relativement simple si on est sur le serveur : on peut appeler await parent() pour obtenir les données des mises en page parent. Mais sur le client, nous devrons utiliser du code grossier pour analyser 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] ?? "";
};

Heureusement, nous n'en avons besoin qu'une seule fois.

Envoi de la valeur du cache

Mais maintenant nous devons envoyer cette valeur à notre /todos point 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') contient une vérification pour voir si nous sommes sur le client (en vérifiant le type de document), et ne renvoie rien si nous le sommes, à quel point nous savons que nous sommes sur le serveur. Ensuite, il utilise la valeur de notre mise en page.

Casser le cache

Mais how mettons-nous réellement à jour cette valeur de contournement du cache lorsque nous en avons besoin ? Puisqu'il est stocké dans un cookie, nous pouvons l'appeler ainsi à partir de n'importe quelle action du serveur :

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

La mise en oeuvre

C'est tout en descente d'ici; nous avons travaillé dur. Nous avons couvert les différentes primitives de plate-forme Web dont nous avons besoin, ainsi que leur destination. Maintenant, amusons-nous et écrivons le code d'application pour lier le tout.

Pour des raisons qui deviendront claires dans un instant, commençons par ajouter une fonctionnalité d'édition à notre /list page. Nous ajouterons cette deuxième ligne de tableau pour chaque tâche :

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>

Et, bien sûr, nous devrons ajouter une action de formulaire pour notre /list page. Les actions ne peuvent aller que dans .server pages, nous allons donc ajouter un +page.server.js dans notre /list dossier. (Oui un +page.server.js fichier peut coexister à côté d'un +page.js déposer.)

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

Nous récupérons les données du formulaire, forçons un délai, mettons à jour notre tâche, puis, plus important encore, effaçons notre cookie de buste de cache.

Essayons ça. Rechargez votre page, puis modifiez l'un des éléments à faire. Vous devriez voir la mise à jour de la valeur de la table après un moment. Si vous regardez dans l'onglet Réseau de DevToold, vous verrez une récupération vers le /todos point de terminaison, qui renvoie vos nouvelles données. Simple et fonctionne par défaut.

La sauvegarde des données

Mises à jour immédiates

Que se passe-t-il si nous voulons éviter cette récupération qui se produit après la mise à jour de notre tâche à faire et, à la place, mettre à jour l'élément modifié directement à l'écran ?

Ce n'est pas qu'une question de performances. Si vous recherchez "poster" puis supprimez le mot "poster" de l'un des éléments à faire de la liste, ils disparaîtront de la liste après la modification car ils ne figurent plus dans les résultats de recherche de cette page. Vous pourriez améliorer l'UX avec une animation de bon goût pour la tâche sortante, mais disons que nous voulions ne sauraient relancez la fonction de chargement de cette page tout en vidant le cache et en mettant à jour la tâche modifiée afin que l'utilisateur puisse voir la modification. SvelteKit rend cela possible — voyons comment !

Tout d'abord, apportons une petite modification à notre chargeur. Au lieu de retourner nos choses à faire, retournons un magasin inscriptible contenant nos tâches.

return { todos: writable(todos),
};

Avant, nous accédions à nos tâches sur le data prop, que nous ne possédons pas et que nous ne pouvons pas mettre à jour. Mais Svelte nous permet de retourner nos données dans leur propre magasin (en supposant que nous utilisons un chargeur universel, ce que nous sommes). Nous avons juste besoin de faire un ajustement supplémentaire à notre /list .

Au lieu de cela:

{#each todos as t}

… nous devons le faire puisque todos est lui-même maintenant un magasin. :

{#each $todos as t}

Maintenant, nos données se chargent comme avant. Mais depuis todos est un magasin accessible en écriture, nous pouvons le mettre à jour.

Tout d'abord, donnons une fonction à notre use:enhance attribut:

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

Cela s'exécutera avant une soumission. Écrivons cela ensuite :

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

Cette fonction fournit une data objet avec nos données de formulaire. Nous retourner une fonction asynchrone qui s'exécutera après notre édition est terminée. Les docs expliquons tout cela, mais en faisant cela, nous avons désactivé la gestion des formulaires par défaut de SvelteKit qui aurait réexécuté notre chargeur. C'est exactement ce que nous voulons ! (Nous pourrions facilement récupérer ce comportement par défaut, comme l'expliquent les documents.)

Nous appelons maintenant update sur notre todos tableau puisque c'est un magasin. Et c'est ça. Après avoir modifié une tâche à faire, nos modifications s'affichent immédiatement et notre cache est vidé (comme auparavant, puisque nous avons défini une nouvelle valeur de cookie dans notre editTodo action de formulaire). Ainsi, si nous recherchons puis revenons à cette page, nous obtiendrons de nouvelles données de notre chargeur, qui excluront correctement tous les éléments de tâche mis à jour qui ont été mis à jour.

Le code pour les mises à jour immédiates est disponible sur GitHub.

Creuser plus profond

Nous pouvons définir des cookies dans n'importe quelle fonction de chargement de serveur (ou action de serveur), pas seulement dans la disposition racine. Ainsi, si certaines données ne sont utilisées que sous une seule mise en page, ou même une seule page, vous pouvez y définir cette valeur de cookie. De plus, si vous êtes ne sauraient faire l'astuce que je viens de montrer en mettant à jour manuellement les données à l'écran, et à la place que votre chargeur se relance après une mutation, vous pouvez toujours définir une nouvelle valeur de cookie directement dans cette fonction de chargement sans aucune vérification isDataRequest. Il sera défini initialement, puis chaque fois que vous exécuterez une action de serveur, cette mise en page invalidera automatiquement et rappellera votre chargeur, réinitialisant la chaîne de suppression du cache avant que votre chargeur universel ne soit appelé.

Écrire une fonction de rechargement

Concluons en créant une dernière fonctionnalité : un bouton de rechargement. Donnons aux utilisateurs un bouton qui videra le cache, puis rechargera la requête en cours.

Nous allons ajouter une action de formulaire simple :

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

Dans un projet réel, vous ne copieriez/collerez probablement pas le même code pour définir le même cookie de la même manière à plusieurs endroits, mais pour cet article, nous optimiserons la simplicité et la lisibilité.

Créons maintenant un formulaire pour y publier :

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

Ça marche!

Interface utilisateur après rechargement.

Nous pourrions dire que c'est fait et passer à autre chose, mais améliorons un peu cette solution. Plus précisément, fournissons des commentaires sur la page pour informer l'utilisateur que le rechargement est en cours. De plus, par défaut, les actions SvelteKit invalident peut. Chaque mise en page, page, etc. dans la hiérarchie de la page actuelle se rechargerait. Il se peut que certaines données soient chargées une fois dans la disposition racine que nous n'avons pas besoin d'invalider ou de recharger.

Donc, concentrons-nous un peu sur les choses et ne rechargeons nos tâches que lorsque nous appelons cette fonction.

Tout d'abord, passons une fonction pour améliorer :

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

Nous établissons un nouveau reloading variable à true au Commencer de cette action. Et puis, afin de remplacer le comportement par défaut d'invalider tout, nous retournons un async une fonction. Cette fonction s'exécutera lorsque notre action de serveur sera terminée (qui définit simplement un nouveau cookie).

Sans cela async fonction retournée, SvelteKit invaliderait tout. Puisque nous fournissons cette fonction, elle n'invalidera rien, c'est donc à nous de lui dire quoi recharger. Nous faisons cela avec le invalidate une fonction. Nous l'appelons avec une valeur de reload:todos. Cette fonction renvoie une promesse, qui se résout lorsque l'invalidation est terminée, à quel point nous définissons reloading retour à false.

Enfin, nous devons synchroniser notre chargeur avec ce nouveau reload:todos valeur d'invalidation. Nous le faisons dans notre chargeur avec le depends fonction:

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

Et c'est ça. depends et invalidate sont des fonctions incroyablement utiles. Ce qui est cool c'est que invalidate ne prend pas simplement des valeurs arbitraires que nous fournissons comme nous l'avons fait. Nous pouvons également fournir une URL, que SvelteKit suivra, et invalidera tous les chargeurs qui dépendent de cette URL. À cette fin, si vous vous demandez si nous pourrions passer l'appel à depends et invalider notre /api/todos endpoint tout à fait, vous pouvez, mais vous devez fournir le exacte URL, y compris le search terme (et notre valeur de cache). Ainsi, vous pouvez soit assembler l'URL de la recherche en cours, soit faire correspondre le nom du chemin, comme ceci :

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

Personnellement, je trouve la solution qui utilise depends plus explicite et simple. Mais voyez les docs pour plus d'informations, bien sûr, et décidez par vous-même.

Si vous souhaitez voir le bouton de rechargement en action, le code correspondant se trouve dans cette branche du repo.

Pensées de séparation

Ce fut un long post, mais j'espère pas écrasant. Nous avons plongé dans différentes manières de mettre en cache les données lors de l'utilisation de SvelteKit. Une grande partie de cela consistait simplement à utiliser des primitives de plate-forme Web pour ajouter le cache correct et les valeurs de cookie, dont la connaissance vous servira dans le développement Web en général, au-delà de SvelteKit.

De plus, c'est quelque chose que vous devez absolument pas besoin tout le temps. On peut dire que vous ne devriez atteindre ce type de fonctionnalités avancées que lorsque vous vraiment besoin d'eux. Si votre magasin de données fournit des données rapidement et efficacement et que vous ne rencontrez aucun type de problème de mise à l'échelle, cela n'a aucun sens de gonfler votre code d'application avec une complexité inutile en faisant les choses dont nous avons parlé ici.

Comme toujours, écrivez un code clair, propre et simple et optimisez-le si nécessaire. Le but de cet article était de vous fournir ces outils d'optimisation lorsque vous en avez vraiment besoin. J'espère que tu as aimé!

Discutez avec nous

Salut! Comment puis-je t'aider?