Almacenamiento en caché de datos en SvelteKit | trucos CSS
Mi publicación anterior fue una revisión extensa de SvelteKit donde vimos qué gran herramienta de desarrollo web es. Esta publicación desglosará lo que hicimos allí y se sumergirá en el tema favorito de cada programador: almacenamiento en cachéAsí que no olvides leer mi última publicación si aún no lo has hecho El código de esta publicación está disponible en GitHubasí como demostración en vivo.
Esta publicación trata sobre el procesamiento de datos. Agregaremos una funcionalidad de búsqueda simple que cambiará la cadena de consulta de la página (usando las funciones integradas de SvelteKit) y volverá a activar el recargador de la página. Pero en lugar de simplemente volver a consultar nuestra base de datos (imaginaria), agregaremos algo de almacenamiento en caché para que volver a buscar búsquedas anteriores (o usar el botón Atrás) muestre datos obtenidos previamente, rápidamente, desde el caché. Veremos cómo controlar la cantidad de tiempo que los datos almacenados en caché siguen siendo válidos y, lo que es más importante, cómo invalidar manualmente cualquier valor almacenado en caché. Como guinda del pastel, veremos cómo podemos actualizar manualmente los datos en la pantalla actual, desde el cliente, después de la mutación, mientras se limpia el caché.
Esta va a ser una publicación más larga y más difícil que la mayoría de lo que suelo escribir, ya que estamos cubriendo temas más difíciles. Esta publicación esencialmente le mostrará cómo implementar funciones comunes de utilidades de datos populares como respuesta-consulta; pero en lugar de usar una biblioteca externa, solo usaremos la plataforma web y las funciones de SvelteKit.
Desafortunadamente, las funciones de la plataforma web son un poco más bajas, por lo que haremos un poco más de trabajo del que podría estar acostumbrado. La ventaja es que no necesitaremos bibliotecas externas, lo que nos ayudará a mantener los tamaños de paquete agradables y pequeños. Por favor, no use los enfoques que le voy a mostrar a menos que tenga una buena razón para no hacerlo. Es fácil equivocarse con el almacenamiento en caché y, como verá, traerá poca complejidad al código de su aplicación. Con suerte, su almacén de datos es rápido y su interfaz de usuario está bien, lo que permite que SvelteKit solicite siempre los datos que necesita para cada página. Si es así, déjalo. Disfruta de la sencillez. Pero esta publicación te mostrará algunos trucos cuando eso deje de ser el caso.
Hablando de consulta-reacción, es acaba de ser lanzado para Svelte! Entonces, si se encuentra confiando en técnicas manuales de almacenamiento en caché muchoasegúrese de revisar este proyecto y ver si puede ayudar.
estoy configurando
Antes de comenzar, hagamos algunos pequeños cambios en el el código que teníamos antesEsto nos dará una excusa para ver otras funciones de SvelteKit y, lo que es más importante, nos preparará para el éxito.
Primero, movamos nuestro cargador de datos de nuestro cargador de arranque +page.server.js
a ana rutas APICrearemos un +server.js
presentar en routes/api/todos
y luego agregar un GET
Esto significa que ahora podremos buscar (usando el verbo GET predeterminado) para /api/todos
Agregaremos el mismo código de carga de datos que antes.
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);
}
A continuación, tomemos el cargador de páginas que teníamos y cambiemos el nombre del archivo de +page.server.js
a +page.js
(o .ts
si ha configurado su proyecto para usar TypeScript). Esto cambia nuestro cargador para que sea un cargador "universal" en lugar de un cargador de servidor. La documentación de SvelteKit explicar la diferenciapero el cargador universal funciona tanto en el servidor como en el cliente. Una ventaja para nosotros es que fetch
la llamada a nuestro nuevo punto final se realizará directamente desde nuestro navegador (después de la carga inicial) utilizando el propio navegador fetch
Agregaremos el almacenamiento en caché HTTP estándar en un momento, pero por ahora todo lo que haremos será llamar al punto final.
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,
};
}
Ahora agreguemos una forma simple a la nuestra. /list
página:
<div class="search-form">
<form action="/list">
<label>Search</label>
<input autofocus name="search" />
</form>
</div>
Sí, los formularios se pueden enrutar directamente a nuestros cargadores de páginas normales. Ahora podemos agregar una palabra de búsqueda en el cuadro de búsqueda, presione Ingresary el término "buscar" se agregará a la cadena de consulta de la URL, lo que reiniciará nuestro cargador y buscará nuestras tareas.
Aumentemos también el retraso en el nuestro todoData.js
presentar en /lib/data
Esto hará que sea más fácil ver cuándo los datos están y no están en caché mientras trabajamos en esta publicación.
export const wait = async amount => new Promise(res => setTimeout(res, amount ?? 500));
Recuerda que el código completo de este post es todo en GitHubsi necesita reenviarlo.
Almacenamiento en caché básico
Comencemos agregando algo de almacenamiento en caché al nuestro. /api/todos
punto final volveremos a lo nuestro +server.js
archivo y agregue nuestro primer encabezado de control de caché.
setHeaders({
"cache-control": "max-age=60",
});
... lo que dejará toda la función con este aspecto:
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);
}
Veremos la invalidación manual en breve, pero todo lo que dice esta función es almacenar en caché estas llamadas API durante 60 segundos. Establezca esto en Lo que quierasy dependiendo de su caso de uso, stale-while-revalidate
también puede valer la pena investigar.
Y así, nuestras solicitudes se almacenan en caché.
Nota Asegúrate de eso quitar la marca de verificación casilla de verificación que desactiva el almacenamiento en caché en las herramientas de desarrollo.
Recuerde que si la navegación inicial de su aplicación es la página de la lista, esos resultados de búsqueda se almacenarán en caché internamente en SvelteKit, así que no espere ver nada en DevTools cuando regrese a esa búsqueda.
Qué se almacena en caché y dónde
Nuestra primera carga renderizada por el servidor de nuestra aplicación (asumiendo que comenzamos desde /list
página) se recuperará en el servidor. SvelteKit serializará y enviará estos datos a nuestro cliente. Es más, él estará mirando Cache-Control
encabezamiento en la respuesta y sabrá usar esos datos almacenados en caché para esa llamada de punto final dentro de la ventana de caché (que configuramos en 60 segundos en el ejemplo publicado).
Después de esta carga inicial, cuando comience a navegar por la página, debería ver solicitudes de red de su navegador para /api/todos
Cuando busca cosas que ya ha buscado (en los últimos 60 segundos), las respuestas deberían cargarse inmediatamente porque están almacenadas en caché.
Lo que es particularmente bueno de este enfoque es que, dado que se almacena en caché a través del almacenamiento en caché nativo del navegador, estas llamadas podrían (dependiendo de cómo gestione la rotura de caché, que cubriremos) continuar en caché incluso si recarga la página (a diferencia de un servidor- carga inicial del lado, que siempre llama al punto final fresco, incluso si lo ha hecho en los últimos 60 segundos).
Obviamente, los datos pueden cambiar en cualquier momento, por lo que necesitamos una forma de borrar este caché manualmente, lo cual veremos a continuación.
Caché no válido
Los datos se almacenarán actualmente en caché durante 60 segundos. Pase lo que pase, en un minuto se extraerán nuevos datos de nuestro almacén de datos. Es posible que desee un período de tiempo más corto o más largo, pero ¿qué sucede si cambia algunos datos y desea vaciar su caché de inmediato para que su próxima solicitud esté actualizada? Resolveremos esto agregando un valor de interrupción de solicitud a la URL que enviamos a nuestro nuevo /todos
punto final
Almacenemos este valor de ruptura de caché en una cookie. Este valor se puede establecer en el servidor pero seguir leyendo en el cliente. Echemos un vistazo a un código de muestra.
Podemos crear un +layout.server.js
archivo en la raíz misma de nuestro routes
Esto comenzará cuando se inicie la aplicación y es el lugar perfecto para establecer un valor de cookie inicial.
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: "https://css-tricks.com/", httpOnly: false });
}
return {
todosCacheBust: cacheValue,
};
}
Es posible que hayas notado isDataRequest
Recuerde que los diseños se volverán a ejecutar cada vez que llame al código del cliente invalidate()
o cada vez que realizamos una acción en el servidor (suponiendo que no desactivemos el comportamiento predeterminado). isDataRequest
muestra estas repeticiones, por lo que solo configuramos la cookie si lo hace false
; De lo contrario, enviamos lo que ya está allí.
El httpOnly: false
también es importante Esto permite que nuestro código de cliente lea estos valores de cookies. document.cookie
Esto normalmente sería un problema de seguridad, pero en nuestro caso se trata de números sin sentido que nos permiten cachear o cachear.
Lectura de valores de caché
Nuestro Cargador Universal es lo que nos llama /todos
punto final Esto se ejecuta en el servidor o el cliente, y necesitamos leer el valor de caché que acabamos de configurar, sin importar dónde estemos. Es relativamente fácil si estamos en el servidor: podemos llamar await parent()
para obtener los datos de los diseños principales. Pero en el cliente necesitaremos usar código sin procesar para analizar 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] ?? "";
};
Afortunadamente, solo lo necesitamos una vez.
Envío del valor de caché
Pero ahora necesitamos enviar ese valor para nosotros /todos
punto 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')
tiene una verificación para ver si estamos en el cliente (verificando el tipo de documento) y no devuelve nada si lo estamos, momento en el que sabemos que estamos en el servidor. Luego usa el valor de nuestro diseño.
Chocar el caché
Pero cómo ¿Actualizamos realmente este valor de ruptura de caché cuando lo necesitamos? Dado que se almacena en una cookie, podemos llamarlo así desde cualquier acción del servidor:
cookies.set("todos-cache", cacheValue, { path: "https://css-tricks.com/", httpOnly: false });
La aplicación
Es todo cuesta abajo desde aquí; hemos hecho el trabajo duro. Hemos cubierto las diferentes plataformas web primitivas que necesitan y hacia dónde van. Ahora divirtámonos y escribamos un código de aplicación para unir todo.
Por razones que se aclararán en un momento, comencemos agregando la funcionalidad de edición a la nuestra. /list
Agregaremos esta segunda fila de la tabla para cada tarea:
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>
Y, por supuesto, necesitaremos agregar una acción de formulario para la nuestra. /list
Las acciones solo pueden entrar .server
páginas, así que agregaremos un +page.server.js
en nuestro /list
carpeta. (Sí un +page.server.js
el archivo puede existir simultáneamente con un +page.js
archivo.)
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: "https://css-tricks.com/", httpOnly: false });
},
};
Tomamos los datos del formulario, forzamos un retraso, actualizamos nuestra tarea y luego, lo más importante, borramos nuestra cookie de eliminación de caché.
Intentemos esto. Vuelva a cargar su página, luego edite uno de los elementos de la tarea. Después de un tiempo, debería ver el valor de la tabla actualizado. Si mira en la pestaña Red en DevToold, verá una extracción de la /todos
punto final que devuelve sus nuevos datos. Fácil y funciona por defecto.
actualizaciones instantáneas
¿Qué pasa si queremos evitar esta recuperación que ocurre después de actualizar nuestro elemento de tarea y, en cambio, actualizamos el elemento modificado directamente en la pantalla?
No es sólo una cuestión de rendimiento. Si busca "publicar" y luego elimina la palabra "publicar" de cualquiera de los elementos de la tarea en la lista, desaparecerán de la lista después de la edición porque ya no está en los resultados de búsqueda en esa página. Podrías mejorar la interfaz de usuario con alguna animación interesante para las tareas emocionantes, pero digamos que queríamos No vuelva a ejecutar la función de recarga en esta página, pero borre la memoria caché y actualice las tareas modificadas para que el usuario pueda ver la edición. SvelteKit lo hace posible. ¡Veamos cómo!
Primero, hagamos un pequeño cambio en nuestro cargador. En lugar de devolver nuestras tareas, volvamos tienda grabable que contiene nuestras tareas.
return {
todos: writable(todos),
};
Solíamos acceder a nuestras tareas en data
prop que no poseemos y no podemos actualizar. Pero Svelte nos permite volver a colocar nuestros datos en su propia tienda (suponiendo que estemos usando un cargador de arranque universal, que es lo que estamos). Solo tenemos que hacer un ajuste más en el nuestro. /list
página.
En cambio:
{#each todos as t}
… tenemos que hacer esto desde entonces todos
ahora es una tienda.:
{#each $todos as t}
Ahora nuestros datos se cargan como antes. pero desde entonces todos
es una tienda de escritura, podemos actualizarla.
Primero, proporcionemos una función a nuestro use:enhance
atributo:
<form
use:enhance={executeSave}
on:submit={runInvalidate}
method="post"
action="?/editTodo"
>
Esto se hará antes de enviar. Escribamos esto a continuación:
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;
}
})
);
};
}
Esta función proporciona una data
objeto con nuestros datos de formulario. Nosotros devolver función asíncrona a ejecutar después nuestra edición está lista. Los documentos explique todo esto, pero al hacer esto estamos desactivando el manejo de formularios predeterminado de SvelteKit, lo que reiniciaría nuestro cargador. ¡Esto es exactamente lo que queremos! (Podemos revertir fácilmente este comportamiento al valor predeterminado, como explican los documentos).
estamos llamando ahora update
de los nuestros todos
matriz ya que se trata de una tienda. Y eso es todo. Después de editar un elemento de tarea, nuestros cambios se muestran inmediatamente y nuestro caché se borra (como antes, ya que configuramos un nuevo valor de cookie en nuestro editTodo
Entonces, si buscamos y luego regresamos a esa página, obtendremos datos nuevos de nuestro cargador, que excluirá correctamente cualquier elemento actualizado de las tareas que se hayan actualizado.
El código para actualizaciones instantáneas está disponible en GitHub.
cavamos más profundo
Podemos establecer cookies en cualquier función de carga del servidor (o acción del servidor), no solo en el diseño principal. Entonces, si algunos datos solo se usan en un diseño o incluso en una página, puede establecer ese valor de cookie allí. Además, si eres No haces el truco que acabo de mostrar, actualizando manualmente los datos en la pantalla, y en su lugar quieres que tu cargador se reinicie después de una mutación, entonces siempre puedes establecer un nuevo valor de cookie directamente en esa función de cargador sin ninguna verificación isDataRequest
Se configurará inicialmente, y luego, cada vez que realice una acción del servidor, este diseño de página invalidará automáticamente y volverá a llamar a su cargador, restableciendo la cadena de eliminación de caché antes de que se llame a su cargador universal.
Escribir una función de recarga
Terminemos construyendo una última función: un botón de recarga Démosle a los usuarios un botón que borre el caché y luego vuelva a cargar la solicitud actual.
Agregaremos una acción con una forma muy simple:
async reloadTodos({ cookies }) {
cookies.set('todos-cache', +new Date(), { path: "https://css-tricks.com/", httpOnly: false });
},
En un proyecto real, probablemente no copiará/pegará el mismo código para configurar la misma cookie de la misma manera en varios lugares, pero para esta publicación optimizaremos la simplicidad y la legibilidad.
Ahora vamos a crear un formulario para publicar en él:
<form method="POST" action="?/reloadTodos" use:enhance>
<button>Reload todos</button>
</form>
¡Esto funciona!
Podemos dar por terminado esto y seguir adelante, pero mejoremos un poco esta solución. Específicamente, proporcionemos un enlace de comentarios en la página para decirle al usuario que hay una recarga en curso. Además, de forma predeterminada, las acciones de SvelteKit se invalidan. todoCualquier diseño, página, etc. en la jerarquía de la página actual se recargará. Puede haber algunos datos que se cargaron una vez en el diseño principal que no necesitamos invalidar o volver a cargar.
Así que centrémonos un poco en las cosas y volvamos a cargar nuestras tareas solo cuando llamemos a esta función.
Primero, pasemos una función para mejorar:
<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;
});
};
};
Ponemos uno nuevo reloading
variable a true
en Empezar Y luego, para anular el comportamiento predeterminado de invalidar todo, devolvemos un async
Esta función se ejecutará cuando se complete la acción de nuestro servidor (que simplemente establece una nueva cookie).
Sin esto async
función devuelta, SvelteKit anulará todo. Dado que proporcionamos esta función, no invalidará nada, por lo que depende de nosotros decirle qué recargar. Hacemos esto con invalidate
función. Lo llamamos con un valor de reload:todos
Esta función devuelve una promesa que se resuelve cuando se completa la cancelación, momento en el que establecemos reloading
de regreso false
.
Finalmente, necesitamos sincronizar nuestro cargador con este nuevo reload:todos
valor nulo Hacemos esto en nuestro cargador con depends
función:
export async function load({ fetch, url, setHeaders, depends }) {
depends('reload:todos');
// rest is the same
Y eso es todo. depends
y invalidate
son características increíblemente útiles. Genial invalidate
no solo acepta valores aleatorios que proporcionamos como lo hicimos nosotros. También podemos proporcionar una URL que SvelteKit seguirá e invalidará cualquier cargador que dependa de esa URL. Con ese fin, si se pregunta si podemos omitir la llamada a depends
e invalidar la nuestra /api/todos
punto final juntos, puede, pero debe proporcionar exactamente URL que incluye search
término (y nuestro valor de caché). Entonces, puede recopilar la URL para la búsqueda actual o hacer coincidir el nombre de la ruta, así:
invalidate(url => url.pathname == "/api/todos");
Personalmente, encontré la solución que usó. depends
más claro y sencillo. Pero mira los documentos para obtener más información, por supuesto, y decidir por ti mismo.
Si desea ver el botón de recarga en acción, el código está dentro esta rama del repositorio.
Separarse de los pensamientos
Esta fue una publicación larga, pero espero que no abrumadora. Nos hemos sumergido en diferentes formas en que podemos almacenar datos en caché cuando usamos SvelteKit. Mucho de esto fue simplemente una cuestión de usar primitivas de la plataforma web para agregar los valores correctos de caché y cookies, sabiendo cuáles le servirán bien en el desarrollo web en general, más allá de solo SvelteKit.
Además, es algo que absolutamente no tienes que hacerlo todo el tiempoTal vez debería buscar este tipo de características avanzadas solo cuando realmente los necesitoSi su almacén de datos sirve datos de manera rápida y eficiente, y no enfrenta ningún problema de escalado, no tiene sentido inflar el código de su aplicación con una complejidad innecesaria al hacer las cosas de las que hemos hablado aquí.
Como siempre, escriba un código claro, limpio y simple y optimícelo cuando sea necesario. El propósito de esta publicación fue brindarle estas herramientas de optimización cuando realmente las necesite. ¡Espero que lo hayan disfrutado!
Deja una respuesta