Xlera8

Кешування даних у SvelteKit

My попередній публікації був широкий огляд SvelteKit, де ми побачили, який це чудовий інструмент для веб-розробки. Ця публікація розщедриться на те, що ми зробили там, і зануриться в улюблену тему кожного розробника: кешування. Тому обов’язково прочитайте мій останній пост, якщо ви цього ще не зробили. Код для цієї публікації доступний на GitHub, А також живий демо.

Ця публікація стосується обробки даних. Ми додамо елементарну функцію пошуку, яка змінюватиме рядок запиту сторінки (за допомогою вбудованих функцій SvelteKit) і повторно запускатиме завантажувач сторінки. Але замість того, щоб просто повторно запитувати нашу (уявну) базу даних, ми додамо деяке кешування, щоб повторний пошук попередніх пошуків (або використання кнопки «Назад») швидко показував раніше отримані дані з кешу. Ми розглянемо, як контролювати час, протягом якого кешовані дані залишаються дійсними, і, що більш важливо, як вручну зробити недійсними всі кешовані значення. Як вишеньку на торті, ми розглянемо, як ми можемо вручну оновити дані на поточному екрані на стороні клієнта після мутації, водночас очищаючи кеш.

Це буде довший і складніший пост, ніж більшість того, що я зазвичай пишу, оскільки ми охоплюємо складніші теми. Ця публікація по суті покаже вам, як реалізувати загальні функції популярних утиліт даних, як реагувати-запит; але замість залучення зовнішньої бібліотеки ми будемо використовувати лише веб-платформу та функції SvelteKit.

На жаль, функції веб-платформи дещо нижчого рівня, тому ми будемо виконувати трохи більше роботи, ніж ви, можливо, звикли. Перевагою є те, що нам не знадобляться жодні зовнішні бібліотеки, що допоможе зберегти приємні та маленькі розміри пакетів. Будь ласка, не використовуйте підходи, які я вам покажу, якщо у вас немає на це вагомих причин. З кешуванням легко помилитися, і, як ви побачите, у коді програми є певна складність. Сподіваємось, ваше сховище даних працює швидко, а ваш інтерфейс користувача в порядку, що дозволяє SvelteKit завжди запитувати дані, необхідні для будь-якої сторінки. Якщо так, залиште це. Насолоджуйтесь простотою. Але ця публікація покаже вам деякі хитрощі, коли це припиниться.

Говорячи про реакційний запит, це щойно був звільнений для Свелте! Тож якщо ви виявите, що покладаєтеся на методи ручного кешування багато, обов’язково перевірте цей проект і подивіться, чи може він допомогти.

Налаштовуючи

Перш ніж почати, давайте внесемо кілька невеликих змін у код, який ми мали раніше. Це дасть нам привід побачити деякі інші функції SvelteKit і, що більш важливо, налаштує нас на успіх.

По-перше, давайте перенесемо завантаження даних із нашого завантажувача +page.server.js в Маршрут API. Ми створимо a +server.js файл у routes/api/todos, а потім додайте a GET функція. Це означає, що тепер ми зможемо отримати (використовуючи дієслово GET за замовчуванням) до /api/todos шлях. Ми додамо той самий код завантаження даних, що й раніше.

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

Далі візьмемо завантажувач сторінок, який у нас був, і просто перейменуємо файл +page.server.js до +page.js (Або .ts якщо ви налаштували свій проект на використання TypeScript). Це змінює наш завантажувач на «універсальний», а не на серверний. Документи SvelteKit пояснити різницю, але універсальний завантажувач працює як на сервері, так і на клієнті. Однією з наших переваг є те, що fetch виклик у нашу нову кінцеву точку виконуватиметься прямо з нашого браузера (після початкового завантаження), використовуючи власний браузер fetch функція. Трохи ми додамо стандартне HTTP-кешування, але наразі все, що ми зробимо, це виклик кінцевої точки.

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

Тепер давайте додамо просту форму до нашої /list сторінки:

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

Так, форми можуть націлюватися безпосередньо на наші звичайні завантажувачі сторінок. Тепер ми можемо додати пошуковий термін у вікно пошуку, натисніть Що натомість? Створіть віртуальну версію себе у , а термін «пошук» буде додано до рядка запиту URL-адреси, який повторно запустить наш завантажувач і здійснить пошук наших завдань.

форма пошуку

Давайте також збільшимо затримку в нашому todoData.js файл у /lib/data. Це дозволить легко побачити, коли дані кешуються, а коли ні, під час роботи над цією публікацією.

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

Пам’ятайте, повний код для цієї публікації все на GitHub, якщо потрібно посилатися на нього.

Базове кешування

Давайте почнемо з додавання кешування до нашого /api/todos кінцева точка. Ми повернемося до нашого +server.js файл і додайте наш перший заголовок керування кешем.

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

…що залишить всю функцію такою:

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

Незабаром ми розглянемо ручне визнання недійсним, але ця функція говорить лише про кешування цих викликів API протягом 60 секунд. Встановіть це як завгодноі залежно від вашого випадку використання, stale-while-revalidate також може бути варто розглянути.

І саме так наші запити кешуються.

Кешувати в DevTools.

примітки переконайтесь, що ви зніміть прапорець прапорець, який вимикає кешування в інструментах розробника.

Пам’ятайте: якщо вашою початковою навігацією в додатку є сторінка зі списком, ці результати пошуку будуть кешовані у внутрішньому кеш-пам’яті SvelteKit, тому не очікуйте, що побачите щось у DevTools, коли повернетесь до цього пошуку.

Що кешується і де

Наше перше серверне завантаження нашої програми (якщо ми починаємо з /list сторінка) буде завантажено на сервер. SvelteKit серіалізує та надішле ці дані нашому клієнту. Більше того, він спостерігатиме за Cache-Control заголовок на відповідь, і знатиме, що використовувати ці кешовані дані для цього виклику кінцевої точки у вікні кешу (яке ми встановили на 60 секунд у прикладі put).

Після початкового завантаження, коли ви починаєте пошук на сторінці, ви повинні побачити мережеві запити від вашого браузера до /api/todos список. Коли ви шукаєте те, що вже шукали (протягом останніх 60 секунд), відповіді мають завантажуватися негайно, оскільки вони кешуються.

Що особливо круто в цьому підході, так це те, що, оскільки це кешування через власне кешування браузера, ці виклики можуть (залежно від того, як ви керуєте очищенням кешу, яке ми розглядатимемо) продовжувати кешувати, навіть якщо ви перезавантажуєте сторінку (на відміну від початкове завантаження на стороні сервера, яке завжди викликає кінцеву точку свіжою, навіть якщо вона зробила це протягом останніх 60 секунд).

Очевидно, що дані можуть змінитися будь-коли, тому нам потрібен спосіб очистити цей кеш вручну, який ми розглянемо далі.

Скасування кешу

Зараз дані будуть кешуватися протягом 60 секунд. Незважаючи ні на що, через хвилину свіжі дані будуть витягнуті з нашого сховища даних. Вам може знадобитися коротший або довший період часу, але що станеться, якщо ви зміните деякі дані й захочете негайно очистити кеш, щоб ваш наступний запит був актуальним? Ми вирішимо цю проблему, додавши значення блокування запитів до URL-адреси, яку надсилаємо до нашого нового /todos кінцева точка.

Давайте збережемо це значення очищення кешу в файлі cookie. Це значення можна встановити на сервері, але все ще читати на клієнті. Давайте розглянемо зразок коду.

Ми можемо створити a +layout.server.js файл у самому корені нашого routes папку. Це буде працювати під час запуску програми та є ідеальним місцем для встановлення початкового значення cookie.

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

Можливо, ви помітили isDataRequest значення. Пам’ятайте, що макети запускатимуться повторно щоразу під час виклику коду клієнта invalidate()або будь-коли, коли ми виконуємо серверну дію (якщо ми не вимкнемо поведінку за умовчанням). isDataRequest вказує на ці повторні запуски, тому ми встановлюємо файл cookie, лише якщо це так false; інакше ми надсилаємо те, що вже є.

Команда httpOnly: false прапор також має значення. Це дозволяє нашому клієнтському коду читати ці значення файлів cookie document.cookie. Зазвичай це було б проблемою безпеки, але в нашому випадку це безглузді числа, які дозволяють нам кешувати або виключати кеш.

Читання значень кешу

Наш універсальний навантажувач - це те, що називається нашим /todos кінцева точка. Це виконується на сервері або клієнті, і нам потрібно прочитати це значення кешу, яке ми щойно встановили, незалежно від того, де ми знаходимося. Це відносно легко, якщо ми на сервері: ми можемо подзвонити await parent() щоб отримати дані з батьківських макетів. Але на клієнті нам знадобиться використовувати деякий загальний код для аналізу 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] ?? "";
};

На щастя, нам це потрібно лише один раз.

Надсилання значення кешу

Але тепер нам потрібно послати це значення для нас /todos кінцева точка.

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') перевіряє, чи є ми на клієнті (перевіряючи тип документа), і нічого не повертає, якщо ми є, після чого ми знаємо, що ми на сервері. Тоді він використовує значення з нашого макета.

Злом кеша

але як чи справді ми оновлюємо це значення очищення кешу, коли нам потрібно? Оскільки він зберігається в файлі cookie, ми можемо викликати його так під час будь-якої дії сервера:

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

Впровадження

Звідси все йде вниз; ми виконали важку роботу. Ми розглянули різні примітиви веб-платформи, які нам потрібні, а також куди вони йдуть. А тепер давайте розважимося і напишемо код програми, щоб зв’язати все разом.

З причин, які стануть зрозумілими трохи пізніше, давайте почнемо з додавання функції редагування до нашого /list сторінки. Ми додамо цей другий рядок таблиці для кожного завдання:

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>

І, звичайно, нам потрібно буде додати дію форми для нашого /list сторінки. Дії можна лише вводити .server сторінок, тому ми додамо a +page.server.js в нашому /list папку. (Так, а +page.server.js файл може співіснувати поруч із a +page.js файл.)

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

Ми захоплюємо дані форми, примусово відкладаємо, оновлюємо наше завдання, а потім, що найважливіше, очищаємо наш файл cookie для відновлення кешу.

Давайте спробуємо. Перезавантажте свою сторінку, а потім відредагуйте один із завдань. Через деякий час ви побачите оновлення значення таблиці. Якщо ви подивіться на вкладку «Мережа» в DevToold, ви побачите вибірку до /todos кінцева точка, яка повертає нові дані. Простий і працює за замовчуванням.

Збереження даних

Негайні оновлення

Що, якщо ми хочемо уникнути того вибору, який відбувається після того, як ми оновили наш елемент справ, і натомість оновимо змінений елемент прямо на екрані?

Це не лише питання ефективності. Якщо ви шукаєте «опублікувати», а потім видалите слово «опублікувати» з будь-якого пункту справ у списку, вони зникнуть зі списку після редагування, оскільки їх більше не буде в результатах пошуку на цій сторінці. Ви можете зробити UX кращим за допомогою вишуканої анімації для хвилюючих справ, але, скажімо, ми хотіли НЕ повторно запустіть функцію завантаження цієї сторінки, але все одно очистіть кеш і оновіть змінені завдання, щоб користувач міг побачити редагування. SvelteKit робить це можливим — давайте подивимося, як!

По-перше, давайте внесемо невелику зміну в наш завантажувач. Замість того, щоб повертати наші справи, давайте повернемо a записуваний магазин містить наші завдання.

return { todos: writable(todos),
};

Раніше ми отримували доступ до своїх завдань на data prop, яким ми не володіємо і не можемо оновити. Але Svelte дозволяє нам повертати наші дані у їхнє власне сховище (якщо ми використовуємо універсальний завантажувач, яким ми і є). Нам просто потрібно зробити ще одне налаштування /list стр.

Замість цього:

{#each todos as t}

…нам потрібно це зробити з тих пір todos сама тепер магазин.:

{#each $todos as t}

Тепер наші дані завантажуються, як і раніше. Але оскільки todos є магазином для запису, ми можемо його оновити.

По-перше, давайте надамо функцію для нашого use:enhance атрибут:

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

Це буде виконано перед надсиланням. Напишемо це далі:

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

Ця функція забезпечує a data об’єкт із даними нашої форми. ми повертати асинхронну функцію, яка буде працювати після наше редагування завершено. Документи пояснити все це, але роблячи це, ми вимикаємо обробку форм за замовчуванням SvelteKit, яка мала б повторно запустити наш завантажувач. Це саме те, чого ми хочемо! (Ми могли б легко повернути цю поведінку за замовчуванням, як пояснюється в документах.)

Зараз дзвонимо update задати на нашому todos масив, оскільки це магазин. І це все. Після редагування елемента справ наші зміни відображаються негайно, а наш кеш очищається (як і раніше, оскільки ми встановили нове значення cookie у editTodo форма дії). Отже, якщо ми здійснимо пошук, а потім повернемося на цю сторінку, ми отримаємо свіжі дані від нашого завантажувача, який правильно виключатиме будь-які оновлені пункти справ, які були оновлені.

Код для негайних оновлень доступний на GitHub.

Копання глибше

Ми можемо встановити файли cookie в будь-якій функції завантаження сервера (або дії сервера), а не лише в кореневому макеті. Отже, якщо деякі дані використовуються лише під одним макетом або навіть під однією сторінкою, ви можете встановити це значення cookie там. Більше того, якщо ви НЕ виконали трюк, який я щойно показав, вручну оновлюючи дані на екрані, і замість цього хочете, щоб ваш завантажувач повторно запускався після мутації, тоді ви завжди можете встановити нове значення cookie прямо у цій функції завантаження без жодної перевірки isDataRequest. Він встановлюється спочатку, а потім кожного разу, коли ви виконуєте серверну дію, цей макет сторінки автоматично скасовуватиме недійсність і повторно викличе ваш завантажувач, повторно встановлюючи рядок перебору кешу перед викликом вашого універсального завантажувача.

Написання функції перезавантаження

Підсумуємо створенням останньої функції: кнопки перезавантаження. Давайте надамо користувачам кнопку, яка очистить кеш і перезавантажить поточний запит.

Ми додамо просту дію форми:

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

У реальному проекті ви, ймовірно, не будете копіювати/вставляти той самий код, щоб установити той самий файл cookie однаковим способом у кількох місцях, але для цієї публікації ми оптимізуємо його для простоти та читабельності.

Тепер давайте створимо форму для публікації в ній:

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

Це працює!

Інтерфейс користувача після перезавантаження.

Ми могли б назвати це готовим і рухатися далі, але давайте трохи вдосконалимо це рішення. Зокрема, давайте надамо відгук на сторінці, щоб повідомити користувача, що відбувається перезавантаження. Крім того, за замовчуванням дії SvelteKit стають недійсними все. Кожен макет, сторінка тощо в ієрархії поточної сторінки буде перезавантажено. Можуть бути деякі дані, які завантажуються один раз у кореневому макеті, і нам не потрібно анулювати чи повторно завантажувати.

Отже, давайте трохи зосередимося на речах і перезавантажуватимемо наші завдання лише тоді, коли ми викличемо цю функцію.

Спочатку давайте передамо функцію для покращення:

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

Ми встановлюємо новий reloading змінна до true в старт цієї дії. А потім, щоб перевизначити поведінку за замовчуванням анулювання всього, ми повертаємо an async функція. Ця функція запуститься, коли завершиться дія нашого сервера (що лише встановить новий файл cookie).

Без цього async функція повертається, SvelteKit анулює все. Оскільки ми надаємо цю функцію, вона нічого не скасовує, тому ми повинні вказати їй, що перезавантажувати. Ми робимо це за допомогою invalidate функція. Ми називаємо це зі значенням reload:todos. Ця функція повертає обіцянку, яка вирішується після завершення недійсності, яку ми встановлюємо reloading назад до false.

Нарешті, нам потрібно синхронізувати наш завантажувач із цим новим reload:todos значення недійсності. Ми робимо це в нашому завантажувачі за допомогою depends функція:

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

І це все. depends та invalidate неймовірно корисні функції. Ось що круто invalidate не просто приймає довільні значення, які ми надаємо, як ми. Ми також можемо надати URL-адресу, яку SvelteKit відстежуватиме, і зробити недійсними будь-які завантажувачі, які залежать від цієї URL-адреси. З цією метою, якщо вам цікаво, чи можемо ми пропустити дзвінок до depends і визнати недійсними наші /api/todos кінцева точка в цілому, ви можете, але ви повинні надати точний URL-адреса, включаючи search термін (і наше значення кешу). Отже, ви можете або зібрати URL-адресу для поточного пошуку, або зіставити назву шляху, ось так:

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

Особисто я знаходжу рішення, яке використовує depends більш чітко і просто. Але бачите документи для отримання додаткової інформації, звичайно, і вирішуйте самі.

Якщо ви хочете побачити кнопку перезавантаження в дії, код для неї є ця гілка репо.

Розставання думок

Це був довгий допис, але, сподіваюся, не надто великий. Ми досліджували різні способи кешування даних під час використання SvelteKit. Значна частина цього була просто питанням використання примітивів веб-платформи для додавання правильного кешу та значень файлів cookie, знання яких стане в нагоді вам у веб-розробці загалом, не лише у SvelteKit.

Більш того, це те, що ви абсолютно не потрібно весь час. Можна стверджувати, що ви повинні використовувати такі розширені функції лише тоді, коли ви насправді вони потрібні. Якщо ваше сховище даних обслуговує дані швидко й ефективно, і ви не маєте жодних проблем із масштабуванням, немає сенсу роздувати код програми непотрібною складністю, роблячи те, про що ми тут говорили.

Як завжди, пишіть чіткий, чистий, простий код і оптимізуйте його, якщо це необхідно. Метою цієї публікації було надати вам інструменти оптимізації, коли вони вам справді потрібні. Сподіваюся, вам сподобалось!

Зв'яжіться з нами!

Привіт! Чим я можу вам допомогти?