Cache API — кэшируем данные на стороне клиента
- пятница, 26 января 2024 г. в 00:00:16
Cache API - сравнительно старый API для управления хранилищем кэша, доступный уже во всех современных браузерах и являющийся частью ServiceWorker.
Прежде чем мы будем говорить о самом API, немного поговорим про контекст. Когда мы говорим о кэшировании веб-приложений, перед нами несколько (зачастую независимых) путей:
Image Cache
Preload Cache
Cache API
HTTP Cache
Рассказ о каждом из них, ровно, как и сравнение, — отдельная статья. Здесь же я хочу рассказать именно о Cache API.
Представим, что нам необходимо сделать веб-приложение, запрашивающее какие-то данные с сервера. Данные меняются не так часто, поэтому, стоило бы их поместить в кэш, чтобы не обращаться каждый раз к БД. Также хотелось бы, чтобы кэш был более управляем на стороне клиента.
Но и тут путей организации кэширования на стороне клиента несколько:
Local Storage
IndexedDB
Cache Storage
И еще много других 😖
А вот тут поговорим о каждом из данных путей:
С Local Storage понятно - устоявшийся стандарт хранилища на стороне клиента, но, как и всегда, есть нюансы - максимальный размер 5 Мб, а еще он синхронен и блокирует основной поток.
IndexedDB - интересное решение. Стоит учитывать, что максимальный размер варьируется в зависимости от браузера, однако этот размер в любом случае больше, чем 5 Мб. Но все упирается в то, что есть следующий метод - Cache Storage, созданный специально для хранения кэша?
Cache Storage - хранилище, созданное специально для кэша. Так же как и в IndexedDB, максимальный размер варьируется в зависимости от браузера.
Также, если вы хотите детальнее узнать о концепциях, лежащих в основе Web Storage - есть прекрасная статья.
Я упомянул о том, что размер Local Storage ограничен 5 Мб, а IndexedDB и Cache Storage - ограничен свободным пространством и различными реализациями в зависимости от браузера, но я не сказал о том, что мы можем узнать, сколько доступно свободного пространства для записи и сколько уже занято.
Для этого есть прекрасное API - Storage Manager. И еще одна новость - он имеет хорошую поддержку в большинстве современных браузеров.
Помимо двух методов, предоставляющих информацию по различным политикам хранения данных на вашем сайте и в браузере в целом, он содержит очень полезный для нас метод - estimate()
, возвращающий информацию о свободном и используемом пространстве.
Вот пример, как мы можем посчитать сколько процентов от свободного пространства мы используем и сколько свободно в байтах:
if (navigator.storage && navigator.storage.estimate) {
const quota = await navigator.storage.estimate();
// quota.usage -> свободное пространство ( в байтах).
// quota.quota -> доступное пространство (в байтах).
const percentageUsed = (quota.usage / quota.quota) * 100;
console.log(`Вы используете ${percentageUsed}% от доступного пространства.`);
const remaining = quota.quota - quota.usage;
console.log(`Вы можете записать еще ${remaining} байт.`);
}
Итак, подытожим:
Local Storage | IndexedDB | Cache Storage | |
Позволяет хранить большие объемы данных | ❌ | ✅ | ✅ |
Асинхронен | ❌ | ✅ | ✅ |
Удобен для хранения кэша | ❌ | ❌ | ✅ |
Разобрав каждый из методов, давай уже приступим к написанию своего примера. Представим, что нам необходимо реализовать веб-приложение с новостной лентой. Поскольку мы хотим снизить нагрузку на сервер - добавим кэширование. Сами же новости будем забирать с помощью JSONPlaceholder. А рабочий же пример будет в конце.
Для начала, давайте получим данные и добавим их на нашу страницу.
const postFeed = document.querySelector("#postFeed");
const createArticleElement = (title: string, body: string) => `
<article class='article card'>
<h2 class='title'>${title}</h2>
<p class='text'>${body}</p>
</article>
`;
const addToFeed = (data: { title: string; body: string }[]) => {
if (postFeed) {
data.forEach(({ title, body }: { title: string; body: string }) => {
postFeed.innerHTML += createArticleElement(title, body);
});
}
};
if (postFeed) {
fetch("https://jsonplaceholder.typicode.com/posts").then(async (res) => {
const data = await res.json();
addToFeed(data);
});
}
}
А вот наш HTML
<main id="app">
<div class='feed' id='postFeed'>
</div>
</main>
В итоге, мы получаем такую страницу:
Если мы зайдем в DevTools и откроем вкладку Network, то заметим, что сейчас запрос на получение новостей происходит на каждое открытие страницы.
Как раз здесь в игру вступает CacheAPI:
(async () => {
if (postFeed) {
const url = "https://jsonplaceholder.typicode.com/posts";
const cache = await caches.open("cache"); // Открываем кэш. Если такого не было до - создаестся новый.
const cachedResponse = await cache.match(url);
if (cachedResponse) {
const data = await cachedResponse.json();
addToFeed(data);
return;
}
fetch(url).then(async (res) => {
cache.put(url, res);
const cloned = res.clone();
addToFeed(await cloned.json());
});
}
})();
Что здесь добавилось?
Во-первых, мы открываем новый кэш. Если же кэша с таким именем нет - добавится новый.
const cache = await caches.open("cache");
После этого мы можем использовать его, что дальше и делаем: проверяем, есть ли кэш в хранилище по переданному URL запроса. Если есть - просто добавляем значения на страницу.
const cachedResponse = await cache.match(url);
if (cachedResponse) {
const data = await cachedResponse.json();
addToFeed(data);
return;
}
Если же кэша нет - запрашиваем с сервера данные и кладем их в кэш по этому запросу и, опять же, добавляем на страницу.
fetch(url).then(async (res): Promise<void> => {
cache.put(url, res);
const cloned = res.clone();
addToFeed(await cloned.json());
});
Хорошо, с одной проблемой разобрались - теперь мы не будем делать лишние запросы на сервер и храним данные ответа на устройстве клиента. Но есть одна проблема: если мы добавим новую новость, запрос не уйдет на сервер, потому что достанется из кэша, и, следовательно, пользователь ее не увидит. Чтобы решить эту проблему - можно использовать несколько способов, однако, для простоты, я приведу один - в ответе будем возвращать заголовок Expires
для удаления кэша, как время истекает.
На клиенте же будем проверять, истекло ли время. Если да - удаляем данные из кэша.
(async () => {
if (postFeed) {
const url = "https://jsonplaceholder.typicode.com/posts";
const cache = await caches.open("cache");
const cachedResponse = await cache.match(url);
if (cachedResponse) {
const expiresValue = cachedResponse.headers.get("expires");
const dateValue = cachedResponse.headers.get("date");
const maxAge = 7200000; // 2 часа
const exp =
expiresValue && expiresValue !== "-1"
? Date.parse(expiresValue)
: Date.parse(dateValue ?? "") + maxAge;
if (Date.now() < exp) {
const data = await cachedResponse.json();
addToFeed(data);
return;
}
cache.delete(url);
}
fetch(url).then(async (res): Promise<void> => {
const cloned = res.clone();
const headers = new Headers([...cloned.headers.entries()]);
headers.set("date", new Date().toUTCString());
const { body, ...rest } = res;
await cache.put(
url,
new Response(body, {
...rest,
headers,
}),
);
addToFeed(await cloned.json());
});
}
})();
Сначала мы получаем заголовок Expires
и Date
, они нам пригодятся для того, чтобы определять, просрочен ли кэш. Далее мы определяем дефолтное время жизни токена, если сервер не вернул заголовок Expires
- 2 часа. Если время жизни токена еще не просрочено, то берем значение из кэша, если же просрочена - удаляем значение.
if (cachedResponse) {
const expiresValue = cachedResponse.headers.get("expires");
const dateValue = cachedResponse.headers.get("date");
const maxAge = 7200000; // 2 часа
const exp =
expiresValue && expiresValue !== "-1"
? Date.parse(expiresValue)
: Date.parse(dateValue ?? "") + maxAge;
if (Date.now() < exp) {
const data = await cachedResponse.json();
addToFeed(data);
return;
}
cache.delete(url);
}
Заметьте, что при добавлении кэша из запроса мы добавляем в заголовки Date
. Данный заголовок мы используем, если сервер не вернул Expires
.
fetch(url).then(async (res): Promise<void> => {
const cloned = res.clone();
const headers = new Headers([...cloned.headers.entries()]);
headers.set("date", new Date().toUTCString());
const { body, ...rest } = res;
await cache.put(
url,
new Response(body, {
...rest,
headers,
}),
);
addToFeed(await cloned.json());
});
А вот и рабочий пример.
Также советую почитать больше про Cache Storage API в официальной документации. Или же в упрощенной версии - MDN.
Напоследок хочется сказать, что Cache API - довольно полезная технология, позволяющая использовать дисковое пространство пользователя для кэширования ответов сетевых запросов. Более того, мы можем хранить в кэше видоизмененные ответы, меняя заголовки или тело ответа. Такая гибкость дает свои преимущества, а выбор того, что использовать для кэширования в вашем веб-приложении остается за вами.