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

Добро пожаловать в 12-й день #30DaysOfPWA! Новичок в серии? Три вещи, которые вы можете сделать, чтобы наверстать упущенное:

  • Прочитайте начальный пост ниже, чтобы понять контекст.
  • Следите за обновлениями в репозитории GitHub.
  • Добавьте блог #30DaysOfPWA в закладки для ресурсов.

Добро пожаловать на #30DaysOfPWA

Nitya Narasimhan, Ph.D. для Microsoft Azure ・ Feb 14 ・ 2 min read

#pwa #pwabuilder #webdev #новички

Это сокращенная версия канонического поста для #30DaysOfPWA.


Об авторе

Автор сегодняшнего поста — Аарон Густафсон (Aaron Gustafson) — главный менеджер программ в команде Microsoft Edge. Следите за Аароном по адресу @aarongustafson или здесь, на сайте dev.to.

Аарон Густафсон

Аарон Густафсон — сторонник веб-стандартов и доступности, работающий в компании Microsoft.


Добро пожаловать на вторую неделю пятого дня серии #30DaysofPWA. Сегодняшняя статья в блоге расскажет вам о том, как стать более эффективным и организованным, когда дело доходит до кэширования. Впервые мы познакомились с концепцией кэширования в следующем посте.

#04 — Сделать прогрессивные веб-приложения надежными

Nitya Narasimhan, Ph.D для Microsoft Azure ・ Feb 18 ・ 6 min read

#pwa #pwabuilder #webdev #новички

Давайте начнем сегодня с краткого обзора кэширования в PWA!


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

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

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

if ( "caches" in this ) {
  // Yay! The Cache API is accessible as caches.
}
Войти в полноэкранный режим Выйти из полноэкранного режима

Вот краткая информация о том, как работает Cache API. Помните, что он основан на обещаниях. Давайте начнем с создания/открытия кэша:

const app_cache = await caches.open("app");
Вход в полноэкранный режим Выйдите из полноэкранного режима

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

// 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():

const response = await app_cache.match("/path/to/generated.json");
Вход в полноэкранный режим Выход из полноэкранного режима

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

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

cache.delete("/path/to/generated.json");
Войти в полноэкранный режим Выйти из полноэкранного режима

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


Упорядочивание

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

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

const version = "v1:";
const sw_caches = {
  assets: {
    name: `${version}assets`
  },
  images: {
    name: `${version}images`
  },
  pages: {
    name: `${version}pages`
  }
};
Войти в полноэкранный режим Выход из полноэкранного режима

Здесь видно, что я использую переменную для отслеживания версии моего кэша. Версионирование кэша — это лучшая практика, поскольку она позволяет полностью удалять старые (устаревшие) кэши, когда вы отправляете новые версии долгоживущих ресурсов (обычно в событии «activate» Service Worker).

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

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

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) отдельно, потому что я ссылаюсь на эту строку в другом месте в Service Worker. Затем я включил этот URL вместе с фавиконом моего PWA, его основным CSS и JavaScript в качестве предварительной установки, которая, в свою очередь, будет передана в cache.addAll() как часть события install Service Worker. Поскольку Cache API основан на обещаниях, вы можете видеть, что событие install (событие) запрашивается для ожидания открытия соответствующего кэша (в данном случае sw_caches.assets.name) и завершения операции addAll().

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

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(), в отличие от открытия и поиска в конкретном кэше. Возможно, вам интересно, почему я рекомендую вести отдельные кэши. Вот почему: Это дает нам возможность более тщательно подходить к очистке кэшированных ресурсов.


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

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

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

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

const sw_caches = {
  assets: {
    name: `${version}assets`
  },
  images: {
    name: `${version}images`,
    limit: 50
  },
  pages: {
    name: `${version}pages`,
    limit: 10
  }
};
Войти в полноэкранный режим Выйти из полноэкранного режима

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

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. Удалите излишки, начиная с первого (самого старого) элемента.

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

// 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, сразу после регистрации Service Worker. Он проверяет существование контроллера Service Worker и, если он существует, отправляет ему команду «очистить» через postMessage(). Второй блок, показанный здесь, находится в файле Service Worker и слушает любые входящие сообщения. Когда он получает сообщение «clean up», он просматривает список кэшей, которые я определил, и запускает trimCache() для всех, у которых есть limit.


Попробуйте

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

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


Ресурсы

  • API Service Worker (MDN)
  • Использование Service Worker для управления сетевыми запросами и push-уведомлениями (Документация для разработчиков Edge)

Хотите читать больше материалов от технологов Microsoft? Не забудьте следить за Azure прямо здесь, на dev.to:

Microsoft Azure

Любой язык. Любая платформа.


Оставьте комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *