Logotipo de Zephyrnet

Almacenamiento en caché de datos en SvelteKit

Fecha:

My Publicación anterior fue una descripción general amplia de SvelteKit donde vimos la gran herramienta que es para el desarrollo web. Esta publicación se basará en lo que hicimos allí y se sumergirá en el tema favorito de cada desarrollador: el almacenamiento en caché. Por lo tanto, asegúrese de leer mi última publicación si aún no lo ha hecho. El código de esta publicación. está disponible en GitHub, así como una demostración.

Esta publicación trata sobre el manejo de datos. Agregaremos algunas funciones de búsqueda rudimentarias que modificarán la cadena de consulta de la página (usando las características integradas de SvelteKit) y reactivarán el cargador 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 los datos recuperados previamente, rápidamente, del caché. Veremos cómo controlar la cantidad de tiempo que los datos almacenados en caché permanecen válidos y, lo que es más importante, cómo invalidar manualmente todos los valores almacenados en caché. Como guinda del pastel, veremos cómo podemos actualizar manualmente los datos en la pantalla actual, del lado del cliente, después de una mutación, mientras seguimos purgando el caché.

Esta será una publicación más larga y 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 reacción-consulta; pero en lugar de extraer una biblioteca externa, solo usaremos la plataforma web y las funciones de SvelteKit.

Desafortunadamente, las características de la plataforma web son de un nivel un poco más bajo, 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 ayudará a mantener los tamaños de paquetes agradables y pequeños. Por favor, no use los enfoques que le mostraré a menos que tenga una buena razón para hacerlo. Es fácil equivocarse con el almacenamiento en caché y, como verá, hay un poco de complejidad que resultará en el 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 cualquier página determinada. Si es así, déjalo en paz. Disfruta de la sencillez. Pero este post te mostrará algunos trucos para cuando eso deje de ser así.

Hablando de reaccionar-consulta, es acaba de ser lanzado para Svelte! Entonces, si te encuentras apoyándote en técnicas manuales de almacenamiento en caché bastante, asegúrese de revisar ese proyecto y ver si podría ayudar.

Configuración

Antes de comenzar, hagamos algunos pequeños cambios en el código que teníamos antes. Esto nos dará una excusa para ver otras funciones de SvelteKit y, lo que es más importante, nos preparará para el éxito.

Primero, movamos nuestra carga de datos desde nuestro cargador en +page.server.js a una Ruta API. Crearemos un +server.js archivo en routes/api/todosy luego agregue un GET función. Esto significa que ahora podremos buscar (usando el verbo GET predeterminado) al /api/todos camino. 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 simplemente cambiemos el nombre del archivo de +page.server.js a +page.js (o .ts si ha creado scaffolding en 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 explica la diferencia, pero un cargador universal se ejecuta tanto en el servidor como en el cliente. Una ventaja para nosotros es que el fetch la llamada a nuestro nuevo punto final se ejecutará directamente desde nuestro navegador (después de la carga inicial), utilizando el navegador nativo fetch función. 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 un formulario simple a nuestro /list página:

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

Sí, los formularios pueden apuntar directamente a nuestros cargadores de páginas normales. Ahora podemos agregar un término de búsqueda en el cuadro de búsqueda, presione Participar, y se agregará un término de "búsqueda" a la cadena de consulta de la URL, que volverá a ejecutar nuestro cargador y buscará nuestras tareas pendientes.

formulario de búsqueda

Aumentemos también la demora en nuestro todoData.js archivo 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));

Recuerde, el código completo para esta publicación es todo en GitHub, si necesita hacer referencia a él.

Almacenamiento en caché básico

Comencemos agregando algo de almacenamiento en caché a nuestro /api/todos punto final Volveremos a nuestro +server.js archivo y agregue nuestro primer encabezado de control de caché.

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

…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. Configura esto como quierasy dependiendo de su caso de uso, stale-while-revalidate también podría valer la pena investigar.

Y así, nuestras consultas se almacenan en caché.

Caché en DevTools.

Note Asegúrese desmarcar la casilla de verificación que desactiva el almacenamiento en caché en las herramientas de desarrollo.

Recuerde, si su navegación inicial en la 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 (suponiendo que comencemos en el /list página) se recuperará en el servidor. SvelteKit serializará y enviará estos datos a nuestro cliente. Además, observará la Cache-Control encabezamiento en la respuesta, y sabrá usar estos datos almacenados en caché para esa llamada de punto final dentro de la ventana de caché (que configuramos en 60 segundos en el ejemplo de puesta).

Después de esa carga inicial, cuando comience a buscar en la página, debería ver las solicitudes de red de su navegador al /api/todos lista. A medida que busca cosas que ya ha buscado (en los últimos 60 segundos), las respuestas deberían cargarse inmediatamente ya que están almacenadas en caché.

Lo que es especialmente bueno con este enfoque es que, dado que esto es almacenamiento en caché a través del almacenamiento en caché nativo del navegador, estas llamadas podrían (dependiendo de cómo administre la prevención de caché que veremos) continuar en caché incluso si vuelve a cargar la página (a diferencia de la carga inicial del lado del servidor, que siempre llama al punto de conexión actualizado, incluso si lo hizo en los últimos 60 segundos).

Obviamente, los datos pueden cambiar en cualquier momento, por lo que necesitamos una forma de purgar este caché manualmente, que veremos a continuación.

Invalidación de caché

En este momento, los datos se almacenarán en caché durante 60 segundos. Pase lo que pase, después de un minuto, se extraerán datos nuevos 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 muta algunos datos y desea borrar su caché de inmediato para que su próxima consulta esté actualizada? Resolveremos esto agregando un valor de prevención de consultas a la URL que enviamos a nuestro nuevo /todos punto final

Almacenemos este valor de destrucción de memoria caché en una cookie. Ese valor se puede establecer en el servidor pero seguir leyendo en el cliente. Veamos un código de muestra.

Se puede crear una +layout.server.js archivo en la raíz misma de nuestro routes carpeta. Esto se ejecutará al iniciar la aplicación y es un 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: "/", httpOnly: false }); } return { todosCacheBust: cacheValue, };
}

Puede que hayas notado la isDataRequest valor. Recuerde, los diseños se volverán a ejecutar cada vez que el código del cliente llame invalidate(), o cada vez que ejecutamos una acción del servidor (suponiendo que no desactivemos el comportamiento predeterminado). isDataRequest indica esas repeticiones, por lo que solo configuramos la cookie si eso es false; de lo contrario, enviamos lo que ya está allí.

El httpOnly: false La bandera también es significativa. Esto permite que nuestro código de cliente lea estos valores de cookies en document.cookie. Esto normalmente sería un problema de seguridad, pero en nuestro caso, estos son números sin sentido que nos permiten almacenar en caché o romper el caché.

Lectura de valores de caché

Nuestro cargador universal es lo que llama nuestro /todos punto final Esto se ejecuta en el servidor o el cliente, y necesitamos leer ese 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 un código bruto 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 tenemos que envío este valor para nuestro /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 un control 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.

Rompiendo el caché

Pero cómo ¿Actualizamos realmente ese valor de eliminación de caché cuando lo necesitamos? Como está almacenada en una cookie, podemos llamarla así desde cualquier acción del servidor:

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

La implementación

Es todo cuesta abajo desde aquí; hemos hecho el trabajo duro. Hemos cubierto las diversas primitivas de la plataforma web que necesitamos, así como adónde van. Ahora divirtámonos y escribamos el código de la aplicación para unirlo todo.

Por razones que quedarán claras en un momento, comencemos agregando una funcionalidad de edición a nuestro /list página. Agregaremos esta segunda fila de la tabla para cada tarea pendiente:

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 nuestro /list página. Las acciones solo pueden entrar .server páginas, así que agregaremos un +page.server.js en nuestro /list carpeta. (Sí un +page.server.js archivo puede coexistir junto a un +page.js expediente.)

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

Tomamos los datos del formulario, forzamos un retraso, actualizamos nuestras tareas pendientes y luego, lo más importante, borramos nuestra cookie de busto de caché.

Démosle una oportunidad a esto. Vuelva a cargar su página, luego edite uno de los elementos pendientes. Debería ver la actualización del valor de la tabla después de un momento. Si observa la pestaña Red en DevToold, verá una búsqueda en el /todos punto final, que devuelve sus nuevos datos. Simple, y funciona por defecto.

Guardar datos

Actualizaciones inmediatas

¿Qué sucede si queremos evitar esa recuperación que ocurre después de actualizar nuestro elemento pendiente y, en su lugar, actualizar el elemento modificado directamente en la pantalla?

Esto no es solo una cuestión de rendimiento. Si busca "publicar" y luego elimina la palabra "publicar" de cualquiera de los elementos pendientes de la lista, desaparecerán de la lista después de la edición, ya que ya no se encuentran en los resultados de búsqueda de esa página. Podría mejorar la UX con alguna animación de buen gusto para las tareas pendientes, pero digamos que queríamos no Vuelva a ejecutar la función de carga de esa página, pero borre la memoria caché y actualice la tarea pendiente modificada 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 pendientes, devolvamos un tienda escribible que contiene nuestras tareas pendientes.

return { todos: writable(todos),
};

Antes, accedíamos a nuestras tareas pendientes en el data prop, que no poseemos y no podemos actualizar. Pero Svelte nos permite devolver nuestros datos en su propia tienda (suponiendo que estemos usando un cargador universal, que es lo que estamos). Sólo tenemos que hacer un ajuste más a nuestro /list .

En lugar de esto:

{#each todos as t}

…necesitamos hacer esto ya que todos es en sí mismo ahora una tienda.:

{#each $todos as t}

Ahora nuestros datos se cargan como antes. Pero desde todos es una tienda escribible, 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 ejecutará antes de un envío. Escribamos eso 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 volvemos una función asíncrona que se ejecutará después de nuestra edición está lista. Los docs explique todo esto, pero al hacer esto, apagamos el manejo de formulario predeterminado de SvelteKit que habría vuelto a ejecutar nuestro cargador. ¡Esto es exactamente lo que queremos! (Podríamos recuperar fácilmente ese comportamiento predeterminado, como explican los documentos).

ahora llamamos update en nuestra todos matriz ya que es una tienda. Y eso es eso. Después de editar un elemento pendiente, nuestros cambios aparecen inmediatamente y nuestro caché se borra (como antes, ya que configuramos un nuevo valor de cookie en nuestro editTodo forma de acción). Por lo tanto, si buscamos y luego navegamos de regreso a esta página, obtendremos datos nuevos de nuestro cargador, que excluirá correctamente cualquier elemento pendiente actualizado que haya sido actualizado.

El código para las actualizaciones inmediatas. está disponible en GitHub.

Cavar 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 raíz. Por lo tanto, si algunos datos solo se usan debajo de un solo diseño, o incluso de una sola página, puede establecer ese valor de cookie allí. Además, si eres no haciendo el truco que acabo de mostrar actualizando manualmente los datos en pantalla, y en su lugar quiero que su cargador se vuelva a ejecutar después de una mutación, entonces siempre puede establecer un nuevo valor de cookie directamente en esa función de carga sin ninguna verificación contra isDataRequest. Se configurará inicialmente, y luego, cada vez que ejecute una acción del servidor, el diseño de la página invalidará automáticamente y volverá a llamar a su cargador, restableciendo la cadena de busto de caché antes de que se llame a su cargador universal.

Escribir una función de recarga

Terminemos construyendo una última característica: un botón de recarga. Démosle a los usuarios un botón que borrará el caché y luego volverá a cargar la consulta actual.

Agregaremos una acción de formulario simple:

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

En un proyecto real, probablemente no copiaría/pegaría 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>

¡Eso funciona!

Interfaz de usuario después de recargar.

Podríamos dar por hecho esto y seguir adelante, pero mejoremos un poco esta solución. Específicamente, proporcionemos comentarios sobre la página para decirle al usuario que se está recargando. Además, de forma predeterminada, las acciones de SvelteKit invalidan todo. Cada diseño, página, etc. en la jerarquía de la página actual se recargaría. Puede haber algunos datos que se cargan una vez en el diseño raíz que no necesitamos invalidar o volver a cargar.

Entonces, centrémonos un poco en las cosas y solo volvamos a cargar nuestras tareas pendientes 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; }); };
};

Estamos configurando un nuevo reloading variable a true en el comienzo de esta acción. Y luego, para anular el comportamiento predeterminado de invalidar todo, devolvemos un async función. Esta función se ejecutará cuando finalice la acción de nuestro servidor (que solo establece una nueva cookie).

Sin esto async función devuelta, SvelteKit invalidaría todo. Dado que proporcionamos esta función, no invalidará nada, por lo que depende de nosotros decirle qué recargar. Esto lo hacemos con el invalidate función. Lo llamamos con un valor de reload:todos. Esta función devuelve una promesa, que se resuelve cuando se completa la invalidación, momento en el que establecemos reloading de nuevo a false.

Por último, necesitamos sincronizar nuestro cargador con este nuevo reload:todos valor de invalidación. Hacemos eso en nuestro cargador con el depends función:

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

Y eso es eso. depends y invalidate son funciones increíblemente útiles. lo que es genial es eso invalidate no solo toma valores arbitrarios que proporcionamos como lo hicimos nosotros. También podemos proporcionar una URL, que SvelteKit rastreará e invalidará cualquier cargador que dependa de esa URL. Con ese fin, si se pregunta si podemos omitir la llamada a depends e invalidar nuestro /api/todos punto final por completo, puede, pero debe proporcionar el exacto URL, incluido el search término (y nuestro valor de caché). Entonces, puede juntar la URL para la búsqueda actual o hacer coincidir el nombre de la ruta, así:

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

Personalmente, encuentro la solución que utiliza depends más explícito y simple. Pero mira los docs 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á en esta rama del repositorio.

Pensamientos de despedida

Esta fue una publicación larga, pero espero que no abrumadora. Nos sumergimos en varias formas en que podemos almacenar datos en caché cuando usamos SvelteKit. Gran parte de esto fue solo una cuestión de usar primitivas de plataforma web para agregar el caché correcto y los valores de cookies, cuyo conocimiento le servirá en el desarrollo web en general, más allá de solo SvelteKit.

Además, esto es algo que absolutamente no necesitas todo el tiempo. Podría decirse que solo debe buscar este tipo de funciones avanzadas cuando realmente los necesito. Si su almacén de datos proporciona datos de manera rápida y eficiente, y no se enfrenta a ningún tipo de problema de escalado, no tiene sentido inflar el código de su aplicación con una complejidad innecesaria haciendo las cosas de las que hablamos 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 esas herramientas de optimización para cuando realmente las necesite. ¡Espero que lo hayan disfrutado!

punto_img

Información más reciente

punto_img