Перейти к содержанию

Кэширование данных вашего приложения

изображение названия и автора

Добро пожаловать на неделю 2 день 5 из серии 30 Days of PWA! В сегодняшней статье мы расскажем вам о том, как стать более эффективным и организованным в вопросах кэширования.

Начнем с краткого обзора кэширования в PWA, который Nitya представила в четвертый день...

Основы кэширования

Прогрессивные веб-приложения имеют большой контроль над тем, как они управляют загрузкой ресурсов. В значительной степени это возможно благодаря сервис-воркерам, которые имеют возможность перехватывать, манипулировать и отвечать непосредственно на сетевые запросы. В дополнение к этому Cache API позволяет сервису-воркеру хранить и извлекать ранее полученные (или созданные) объекты Response, что дает возможность не обращаться к сети для получения долгоживущих ресурсов, таких как таблицы стилей и изображения.

Прежде чем приступить к работе, хочу отметить, что Cache API доступен везде, а не только в рамках сервис-воркера. Если вы используете его только в контексте сервис-воркера, то он гарантированно будет доступен, но если вы используете его в других местах, то вам необходимо протестировать, чтобы убедиться в доступности этой функции:

1
2
3
if ('caches' in this) {
    // Yay! The Cache API is accessible as caches.
}

Вот краткая информация о том, как работает Cache API. Следует помнить, что он основан на промисе. Начнем с создания/открытия кэша:

1
const app_cache = await caches.open('app');

В результате будет создан кэш app, если он еще не существует, и затем он будет открыт. Затем вы можете добавлять элементы в кэш с помощью функций add(), addAll() или put(). Вот краткое описание того, как они работают:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// Add a single Request
app_cache.add('/path/to/resource.ext'); // requests the resource and adds it to the cache
app_cache.add(new Request('/path/to/resource.ext')); // does the same thing
app_cache.put('/path/to/resource.ext'); // same again, but using put()

// Add a bunch of Requests
const app_files = [
    '/path/to/resource-1.ext',
    '/path/to/resource-2.ext',
];
app_cache.addAll(app_files);
// requests & caches all of them

// generates a new synthetic response &
// caches it as though it was
// "/path/to/generated.json"
app_cache.put(
    '/path/to/generated.json',
    new Response('{ "generated_by": "my service worker" }'),
);

// Store a non-CORS/3rd party Request
app_cache.put('https://another.tld/resource.ext');

Довольно круто, правда? Теперь, когда элементы находятся в кэше, их можно извлечь, используя match():

1
2
3
const response = await app_cache.match(
    '/path/to/generated.json',
);

Обычно это делается в контексте события Fetch в сервисе-воркере, но можно использовать и в основном потоке для выполнения таких действий, как заполнение автономной страницы списком страниц, находящихся в кэше. Довольно интересная вещь.

И наконец, в завершение, вы можете удалять элементы из кэша так же легко, как и добавлять их:

1
cache.delete('/path/to/generated.json');

С предварительными вопросами покончено, давайте рассмотрим, как управлять кэшированными данными.

Наведение порядка

В кэше браузера можно хранить большое количество данных — в некоторых случаях гигабайты. Однако то, что мы можем это делать, не означает, что мы должны это делать. В конце концов, в определенный момент браузер вытеснит из памяти весь сайт, если места станет мало. Лучше не быть "загонщиком" ресурсов, когда они придут очищать пространство 😉.

Прежде чем мы начнем более тщательно подходить к выбору объема кэшируемых данных, необходимо разделить их на категории. Например, в PWA будет содержаться более долгоживущий контент, такой как CSS, JavaScript, логотип сайта и, возможно, ваша автономная страница. Я предпочитаю рассматривать их как ресурсы и создаю для них специальный кэш. Аналогичным образом, скорее всего, вы будете кэшировать запросы для других категорий содержимого, таких как страницы, изображения и т.д. Мне нравится определять эти категории как отдельные кэши, чтобы можно было добавлять в них элементы и удалять из них более целенаправленно. Я определяю их в верхней части своего сервис-воркера:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
const version = 'v1:';
const sw_caches = {
    assets: {
        name: `${version}assets`,
    },
    images: {
        name: `${version}images`,
    },
    pages: {
        name: `${version}pages`,
    },
};

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

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

С помощью этой настройки я могу использовать событие сервис-воркера install для кэширования своих ресурсов:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
const offline_page = '/offline/';
const preinstall = [
    '/favicon.png',
    '/c/default.min.css',
    '/c/advanced.min.css',
    '/j/main.min.js',
    offline_page,
];

self.addEventListener('install', function (event) {
    event.waitUntil(
        caches
            .open(sw_caches.assets.name)
            .then(function (cache) {
                return cache.addAll(preinstall);
            }),
    );
});

Здесь я определил URL-адрес своей автономной страницы (offline_page) отдельно, поскольку ссылаюсь на эту строку в другом месте сервис-воркера. Затем я включил этот URL вместе с favicon моего PWA, а также его основными CSS и JavaScript в preinstall, который, в свою очередь, попадает в cache.addAll() как часть события сервис-воркера install. Поскольку Cache API основан на промисах, можно видеть, что событие install (event) запрашивается для ожидания открытия соответствующего кэша (в данном случае sw_caches.assets.name) и завершения операции addAll().

Мы можем использовать эту организацию и в событии fetch. Обычно я использую различные рецепты кэша/сети для разных типов ресурсов. Аналогично, я могу хранить любой из этих ресурсов в наиболее подходящем кэше. Вот сокращенный пример:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
self.addEventListener('fetch', (event) => {
    // Destination gives us a clue as to the type of resource
    const destination = event.request.destination;
    switch (destination) {
        case 'image':
            event
                .respondWith
                // check the cache first,
                // fall back to the network
                // and store a copy in
                // sw_caches.images.name
                ();
            break;
        case 'document':
            event
                .respondWith
                // check the network first
                // and store a copy in
                // sw_caches.pages.name,
                // fall back to the cache
                ();
        default:
            event
                .respondWith
                // network only
                ();
    }
});

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

Теперь, правда, вы можете искать совпадения сразу во всех кэшах вашего PWA, используя caches.match(), в отличие от открытия и поиска в конкретном, именованном кэше. Поэтому вы можете задаться вопросом, почему я рекомендую поддерживать разные кэши. Вот почему: Это позволяет нам более целенаправленно подходить к очистке кэшированных ресурсов.

Уборка за собой

Когда Нити рассказывала о жизненном цикле сервис-воркера в первой неделе, она упомянула событие activate. Активация — это отличное время для очистки устаревших кэшей. Обычно это обсуждается в контексте истечения срока действия старого кэшированного содержимого путем проверки, не совпадают ли имена кэшей с текущим значением version:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
self.addEventListener('activate', (event) => {
    event.waitUntil(
        caches
            .keys()
            .then((keys) => {
                return Promise.all(
                    keys
                        .filter((key) => {
                            return !key.startsWith(version);
                        })
                        .map((key) => {
                            return caches.delete(key);
                        }),
                );
            })
            .then(() => clients.claim()),
    );
});

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

Когда вы начнете разделять свои кэши на несколько частей, вы также сможете установить ограничения на количество ресурсов, которые вы хотите держать под рукой. Некоторые кэши, например, ресурсы, вы, вероятно, захотите хранить в течение длительного времени. Другие кэши, возможно, следует ограничить определенным количеством ресурсов. Давайте настроим sw_caches для этого:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
const sw_caches = {
    assets: {
        name: `${version}assets`,
    },
    images: {
        name: `${version}images`,
        limit: 50,
    },
    pages: {
        name: `${version}pages`,
        limit: 10,
    },
};

Здесь я установил жесткие ограничения на количество элементов в кэше изображений и страниц (50 и 10 соответственно). Установив эти ограничения, мы можем создать более методичную утилиту для обрезки кэша:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
function trimCache(cache_name, limit) {
    caches.open(cache_name).then((cache) => {
        cache.keys().then((items) => {
            if (items.length > limit) {
                (async function () {
                    let i = 0,
                        end = items.length - limit;
                    while (i < end) {
                        console.log(
                            'deleting item',
                            i,
                            items[i],
                        );
                        cache.delete(items[i++]);
                    }
                })();
            }
        });
    });
}

Эта функция принимает два аргумента: имя кэша (cache_name) и максимальное количество элементов, которое мы хотим вместить в кэш (limit). Вот что она делает:

  1. Открывает именованный кэш (caches.open()), затем
  2. Получить элементы в кэше (cache.keys()), затем
  3. Проверить, не превышает ли количество элементов установленный лимит,
  4. Если превышает, то определить, сколько элементов нужно удалить, и (наконец)
  5. Удалить излишки, начиная с первого (самого старого) элемента.

Эта функция может быть вызвана в любое время, но я часто запускаю ее при загрузке страницы, вызывая из главного потока:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// main.js
// After my code to register the service worker…
if (navigator.serviceWorker.controller) {
    window.addEventListener('load', function () {
        navigator.serviceWorker.controller.postMessage(
            'clean up',
        );
    });
}

// serviceworker.js
self.addEventListener('message', (messageEvent) => {
    if (messageEvent.data == 'clean up') {
        for (let key in sw_caches) {
            if (sw_caches[key].limit != undefined) {
                trimCache(
                    sw_caches[key].name,
                    sw_caches[key].limit,
                );
            }
        }
    }
});

Первый блок этого фрагмента находится в моем основном файле JavaScript, сразу после регистрации сервис-воркера. Он проверяет, существует ли контроллер сервис-воркера, и, если существует, отправляет ему команду "очистить" через postMessage(). Второй блок, показанный здесь, находится в файле сервис-воркера и прослушивает входящие сообщения. Получив сообщение "clean up", он просматривает список определенных мною кэшей и запускает функцию trimCache() для всех, у которых есть limit.

Попробуйте

Cache API — это невероятно мощный инструмент, и в этой заметке мы лишь поверхностно рассмотрим его возможности. Поиграйте с ним и найдите те подходы, которые работают лучше всего для вас. Помните, что они будут отличаться от проекта к проекту и от ресурса к ресурсу. Оставайтесь гибкими и думайте о том, какие стратегии кэширования имеют наибольший смысл. Также не бойтесь передумать; вы всегда можете пересмотреть version и начать все с нуля.

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

Ресурсы

Комментарии