Кслера8

Кэширование данных в SvelteKit

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

Этот пост посвящен обработке данных. Мы добавим некоторые элементарные функции поиска, которые изменят строку запроса страницы (используя встроенные функции SvelteKit) и повторно запустят загрузчик страницы. Но вместо того, чтобы просто повторно запрашивать нашу (воображаемую) базу данных, мы добавим некоторое кэширование, чтобы повторный поиск предыдущих поисков (или с помощью кнопки «Назад») быстро отображал ранее извлеченные данные из кэша. Мы рассмотрим, как контролировать продолжительность времени, в течение которого кэшированные данные остаются действительными, и, что более важно, как вручную аннулировать все кэшированные значения. В качестве вишенки на торте мы рассмотрим, как мы можем вручную обновить данные на текущем экране на стороне клиента после мутации, продолжая очищать кеш.

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

К сожалению, функции веб-платформы немного ниже, поэтому мы будем делать немного больше работы, чем вы привыкли. Положительным моментом является то, что нам не понадобятся никакие внешние библиотеки, что поможет сохранить небольшие размеры пакетов. Пожалуйста, не используйте подходы, которые я собираюсь вам показать, если у вас нет для этого веской причины. В кэшировании легко ошибиться, и, как вы увидите, в коде вашего приложения есть некоторая сложность. Надеюсь, ваше хранилище данных работает быстро, а ваш пользовательский интерфейс в порядке, что позволяет SvelteKit всегда запрашивать данные, необходимые для любой страницы. Если это так, оставьте его в покое. Наслаждайтесь простотой. Но этот пост покажет вам некоторые приемы, когда это перестанет быть таковым.

Говоря о реагирующем запросе, это был только что выпущен для Свелте! Поэтому, если вы обнаружите, что полагаетесь на методы ручного кэширования много, обязательно ознакомьтесь с этим проектом и посмотрите, может ли он помочь.

Настройка

Прежде чем мы начнем, давайте внесем несколько небольших изменений в код, который у нас был раньше. Это даст нам повод увидеть некоторые другие функции SvelteKit и, что более важно, настроит нас на успех.

Во-первых, давайте переместим загрузку данных из нашего загрузчика в +page.server.js к API-маршрут. Мы создадим +server.js файл в routes/api/todos, а затем добавьте 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="/ru/list"> <label>Search</label> <input autofocus name="search" /> </form>
</div>

Да, формы могут ориентироваться непосредственно на наши обычные загрузчики страниц. Теперь мы можем добавить поисковый запрос в поле поиска, нажмите Enter, и к строке запроса URL будет добавлен термин «поиск», который повторно запустит наш загрузчик и выполнит поиск наших элементов списка дел.

форма поиска

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

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

Помните, что полный код для этого поста все на гитхабе, если вам нужно сослаться на него.

Базовое кэширование

Давайте начнем с добавления кэширования в наш /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 секунд в примере размещения).

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

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

Очевидно, что данные могут измениться в любое время, поэтому нам нужен способ очистить этот кеш вручную, что мы и рассмотрим далее.

Аннулирование кеша

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

Давайте сохраним это значение очистки кеша в файле cookie. Это значение можно установить на сервере, но все равно прочитать на клиенте. Давайте посмотрим на пример кода.

Мы можем создать +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 страницы, поэтому мы добавим +page.server.js в нашем /list папка. (Да +page.server.js файл может сосуществовать рядом с +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 }); },
};

Мы собираем данные формы, вызываем задержку, обновляем нашу задачу, а затем, что наиболее важно, очищаем наш кеш-куки.

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

Сохранение данных

Немедленные обновления

Что, если мы хотим избежать выборки, которая происходит после обновления нашего элемента списка дел, и вместо этого обновить измененный элемент прямо на экране?

Это не просто вопрос производительности. Если вы ищете «публикацию», а затем удаляете слово «публикация» из любого элемента списка дел, они исчезнут из списка после редактирования, поскольку их больше нет в результатах поиска на этой странице. Вы могли бы улучшить UX с помощью красивой анимации для текущих дел, но, скажем, мы хотели не повторно запустите функцию загрузки этой страницы, но все же очистите кеш и обновите измененную задачу, чтобы пользователь мог видеть редактирование. SvelteKit делает это возможным — давайте посмотрим, как!

Во-первых, давайте внесем одно небольшое изменение в наш загрузчик. Вместо того, чтобы возвращать наши задачи, давайте вернем записываемый магазин содержащий наши задачи.

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

Эта функция обеспечивает 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 на Начало данного действия. А затем, чтобы переопределить поведение по умолчанию, заключающееся в аннулировании всего, мы возвращаем 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 term (и значение нашего кеша). Таким образом, вы можете либо составить URL-адрес для текущего поиска, либо сопоставить имя пути, например:

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

Лично я нахожу решение, которое использует depends более явным и простым. Но см. документы для получения дополнительной информации, конечно, и решить для себя.

Если вы хотите увидеть кнопку перезагрузки в действии, код для нее находится в эта ветка репо.

Прощальные мысли

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

Более того, это то, что вы абсолютно не нужно все время. Возможно, вы должны использовать такие расширенные функции только тогда, когда вы на самом деле они нужны. Если ваше хранилище данных обслуживает данные быстро и эффективно, и вы не сталкиваетесь с какими-либо проблемами масштабирования, нет смысла раздувать код вашего приложения ненужной сложностью, выполняя то, о чем мы здесь говорили.

Как всегда, пишите четкий, чистый, простой код и оптимизируйте его при необходимости. Цель этого поста состояла в том, чтобы предоставить вам эти инструменты оптимизации, когда они вам действительно нужны. Надеюсь, вам понравилось!

Чат с нами

Всем привет! Могу я чем-нибудь помочь?