Идеальный инструмент для создания прогрессивных веб-приложений или Всё, что вы хотели знать о Workbox. Часть 1

в 12:00, , рубрики: api, javascript, JS, Workbox, Блог компании Маклауд, Разработка веб-сайтов, разработка приложений

Идеальный инструмент для создания прогрессивных веб-приложений или Всё, что вы хотели знать о Workbox. Часть 1 - 1

Что такое Workbox?

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

Если вы впервые слышите о СВ, то перед изучением данного руководства настоятельно рекомендуется ознакомиться со следующими материалами:

WB предоставляет следующие возможности:

  • предварительное кэширование
  • кэширование во время выполнения
  • стратегии (кэширования)
  • обработка (перехват сетевых) запросов
  • фоновая синхронизация
  • помощь в отладке

На что похож WB API?

Ниже приведены примеры основных подходов к разработке прогрессивных веб-приложений (приложений, в которых используются возможности, предоставляемые СВ).

Кэширование шрифтов Google

Хотели бы вы, чтобы ваши гугл-шрифты были доступны в режиме офлайн после того, как пользователь в первый раз посетил ваш сайт? Тогда запишите их в кэш:

import { ExpirationPlugin } from 'workbox-expiration'
import { registerRoute } from 'workbox-routing'
import { StaleWhileRevalidate } from 'workbox-strategies'

// Записываем гугл-шрифты в кэш с применением стратегии `stale-while-revalidate` (см. ниже) с
// ограничением максимального количества вхождений (записей в кэше)
registerRoute(
  ({ url }) =>  url.origin === 'https://fonts.googleapis.com' ||
                url.origin === 'https://fonts.gstatic.com',
  new StaleWhileRevalidate({
    cacheName: 'google-fonts',
    plugins: [
      new ExpirationPlugin({ maxEntries: 20 })
    ]
  })
)

Кэширование JavaScript и CSS

Сделайте ваш JS и CSS-код быстрым за счет его доставки из кэша, будучи уверенными в его актуальности за счет периодического обновления (в фоновом режиме):

import { registerRoute } from 'workbox-routing'
import { StaleWhileRevalidate } from 'workbox-strategies'

registerRoute(
  ({ request }) =>  request.destination === 'script' ||
                    request.destination === 'style',
  new StaleWhileRevalidate()
)

Кэширование изображений

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

import { CacheableResponsePlugin } from 'workbox-cacheable-response'
import { CacheFirst } from 'workbox-strategies'
import { ExpirationPlugin } from 'workbox-expiration'
import { registerRoute } from 'workbox-routing'

registerRoute(
  ({ register }) => request.destination === 'image',
  new CacheFirst({
    cacheName: 'images',
    plugins: [
      new CacheableResponsePlugin({
        statuses: [0, 200]
      }),
      new ExpirationPlugin({
        maxEntries: 60,
        maxAgeSeconds: 30 * 24 * 60 * 60 // 30 дней
      })
    ]
  })
)

Предварительное кэширование

Используйте WB для предварительного кэширования ресурсов, используемых вашим приложением, с помощью CLI, Node-модуля или Webpack-плагина.

// CLI
workbox wizard

// Node
const { generateSW } = require('workbox-build')

generateSW({
  swDest: './build/sw.js',
  globDirectory: './app',
  globPatterns: '**/*.{js, css, html, png}'
})

// Webpack
const { GenerateSW } = require('workbox-webpack-plugin')

module.exports = {
  // другие настройки вебпака
  plugins: [
    // другие плагины
    new GenerateSW()
  ]
}

Гугл-аналитика в режиме офлайн

Хотите, чтобы гугл-аналитика продолжала работать при отсутствии подключения к сети? Никаких проблем.

import * as googleAnalytics from 'wortkbox-google-analytics'

googleAnalytics.initialize()

Начало работы

WB — это набор библиотек для помощи в создании и управлении СВ и кэшированием через CacheStorage API. СВ и CacheStorage API при совместном использовании позволяют управлять тем, откуда загружаются ресурсы, используемые приложением (JavaScript, CSS, HTML, изображения и т.д.), из сети или из кэша.

Установка

Начиная с 5 версии, WB может быть использован внутри СВ с помощью JS-модулей и npm посредством установки необходимых модулей WB и их импорта.

К сожалению, JS-модули не работают в СВ, поэтому для компиляции СВ требуется сборщик модулей, такой как Webpack, Rollup или Parcel (см. ниже).

Создание СВ

Перед использованием WB, необходимо создать СВ и зарегистрировать его на странице. Начнем с создания файла service-worker.js в корневой директории проекта:

console.log('Привет от сервис-воркера!')

Далее регистрируем СВ на странице:

<script>
// Проверяем поддержку СВ
if ('serviceWorker' in navigator) {
  // Ожидаем полной загрузки страницы
  window.addEventListener('load', () => {
    navigator.serviceWorker.register('/service-worker.js')
  })
}
</script>

Это сообщает браузеру, что данный СВ должен использоваться для текущей страницы. Если вы перезагрузите страницу, откроете инструменты разработчика Chrome и перейдете во вкладку Console, то увидите там сообщение от СВ.

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

После этого можно подключать WB.

Использование WB

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

Маршрутизация и стратегии кэширования

WB позволяет управлять кэшированием HTTP-запросов с помощью различных стратегий кэширования. Первым делом необходимо определить, соответствует ли поступивший запрос определенному критерию и, если это так, применить к нему соответствующую стратегию. Можно использовать предварительно определенную стратегию (предоставляемую WB) или определить собственную. Базовая реализация маршрутизации и кэширования может выглядеть так:

import { registerRoute } from 'workbox-routing'
import {
  NetworkFirst,
  StaleWhileRevalidate,
  CacheFirst
} from 'workbox-strategies'

// Используется для фильтрации запросов на основе статус-кодов, заголовков или обоих сразу (см. ниже)
import { CacheableResponsePlugin } from 'workbox-cacheable-response'
// Используется для ограничения количества записей в кэше и удаления записей по истечении определенного времени (см. ниже)
import { ExpirationPlugin } from 'workbox-expiration'

// Кэшируем страницы (`HTML`) с помощью стратегии `Network First` (сначала сеть)
registerRoute(
  // проверяем, что запрос - это переход на новую страницу
  ({ request }) => request.mode === 'navigate',
  new NetworkFirst({
    // помещаем все файлы в кэш с названием 'pages'
    cacheName: 'pages',
    plugins: [
      // кэшируем только результаты со статусом 200
      new CacheableResponsePlugin({
        statuses: [200]
      })
    ]
  })
)

// Кэшируем запросы на получение `CSS`, `JS` и веб-воркеров с помощью стратегии `Stale While Revalidate` (считается устаревшим после запроса)
registerRoute({
  // проверяем, что цель запроса - это таблица стилей, скрипт или воркер
  ({ request }) =>
    request.destination === 'style' ||
    request.destination === 'script' ||
    request.destination === 'worker',
  new StaleWhileRevalidate({
    // помещаем файлы в кэш с названием 'assets'
    cacheName: 'assets',
    plugins: [
      new CacheableResponsePlugin({
        statuses: [200]
      })
    ]
  })
})

// Кэшируем изображения с помощью стратегии `Cache First` (сначала кэш)
registerRoute(
  // проверяем, что цель запроса - изображение
  ({ request }) => request.destination === 'image',
  new CacheFirst({
    // помещаем файлы в кэш с названием 'images'
    cacheName: 'images',
    plugins: [
      new CacheableResponsePlugin({
        statuses: [200]
      }),
      // кэшируем до 50 изображений в течение 30 дней
      new ExpirationPlugin({
        maxEntries: 50,
        maxAgeSeconds: 60 * 60 * 24 * 30
      })
    ]
  })
)

Предварительное кэширование

В дополнение к кэшированию после выполнения запроса (кэшированию во время выполнения), WB поддерживает предварительное кэширование — кэширование ресурсов во время установки СВ. Хорошими кандидатами для предварительного кэширования являются начальный URL приложения, резервная (офлайн) страница, а также ключевые JS и CSS-файлы. Предварительное кэширование позволяет гарантировать, что основные ресурсы приложения являются доступными в момент, когда СВ получает контроль над страницей.

Предварительное кэширование можно использовать внутри СВ с помощью плагина, предоставляемого сборщиком модулей, поддерживающим встраивание (внедрение) “манифеста предварительного кэширования” (см. ниже):

import { precacheAndRoute } from 'workbox-precaching'

precacheAndRoute(self.__WB_MANIFEST)

Данный СВ предварительно кэширует файлы во время установки, заменяя self.__WB_MANIFEST списком вхождений, добавляемых в СВ во время сборки.

Резервный контент

Как правило, для подготовки приложения к работе в режиме офлайн вместо "дефолтной" страницы с ошибкой предоставляется резервная страница. С помощью WB этот паттерн можно реализовать при помощи нескольких строк кода:

import { precacheAndRoute } from 'workbox-precaching'
import { setCatchHandler } from 'workbox-routing'

// Обеспечиваем кэширование `/offline.html` во время сборки
precacheAndRoute(self.__WB_MANIFEST)

// Перехватываем ошибки, связанные с маршрутизацией, такие как отсутствие подключения к сети
setCatchHandler(async ({ event }) => {
  // Возвращаем предварительно сохраненную резервную страницу при запросе “документа”
  if (event.request.destination === 'document') {
    return matchPrecache('/offline.html')
  }

  return Response.error()
})

Данный СВ предварительно сохраняет резервную страницу и возвращает ее при отсутствии подключения к сети вместо ошибки.

Использование сборщиков модулей с WB

Как было отмечено ранее, начиная с 5 версии, WB может использоваться в СВ с помощью JS-модулей.

Создание сборки

Выбор сборщика модулей

Подойдет любой сборщик, поддерживающий модули, например, Webpack, Rollup или Parcel.

Написание кода СВ

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

import { precacheAndRoute } from 'workbox-precaching'
import { registerRoute } from 'workbox-routing'
import { CacheFirst } from 'workbox-strategies'

// Используем импортированные библиотеки для реализации кэширования,
// маршрутизации и другой логики
precacheAndRoute(self.__WB_MANIFEST)

registerRoute(
  ({ request }) => request.destination === 'image',
  new CacheFirst({ cacheName: 'images' })
)

// и т.д.

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

Опционально: встраивание манифеста предварительного кэширования

Для того, чтобы кэшировать ресурсы во время установки СВ, необходимо предоставить WB оформленный определенным образом список URL. Данный список называется “манифестом предварительного кэширования” (precaching manifest).

Место для вставки такого манифеста во время сборки обозначается с помощью self.__WB_MANIFEST.

Настройка вебпака

Плагин Inject Manifest отвечает как за сборку СВ, так и за встраивание манифеста. При его использование не требуется отдельно настраивать СВ.

Настройка CLI

Вместо плагина для сборщика можно использовать workbox-cli в режиме injectManifest.

При использовании данного подхода после каждой успешной сборки необходимо запускать injectManifest, передавая ему свежего СВ в качестве настройки swSrc.

GenerateSW

В режиме generateSW при использовании workbox-build, workbox-cli или GenerateSW в workbox-webpack-plugin не нужно настраивать процесс сборки — готовый к использованию СВ будет сгенерирован автоматически.

Обработка запросов

Маршрутизация в WB — это процесс определения совпадения маршрута с запросом и обработка совпавшего запроса (отправка ответа на него).

Существует три способа реализации определения совпадений с помощью workbox-routing:

  1. Строка
  2. Регулярное выражение
  3. Функция обратного вызова

Поиск совпадений

С помощью строк

Запрашиваемый URL сравнивается со строкой и, если они равны, запрос передается обработчику (handler). Маршрутизатор для /logo.png может быть реализован так:

import { registerRoute } from 'workbox-routing'

registerRoute('/logo.png', handler)

Проблема такого подхода состоит в том, что запросы из другого источника (origin) не будут совпадать со строкой. Поэтому лучше определять абсолютный путь:

import { registerRoute } from 'workbox-routing'

registerRoute('https://some-other-origin.com/logo.png', handler)

С помощью регулярных выражений

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

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

import { registerRoute } from 'workbox-routing'

registerRoute(new RegExp('\.js$'), jsHandler)

registerRoute(new RegExp('\.css$'), cssHandler)

Регулярные выражения также могут использоваться для поиска совпадения с определенным форматом URL, например:

// /blog/<год>/<месяц>/<пост заголовок идентификатор>
import { registerRoute } from 'workbox-routing'

registerRoute(new RegExp('/blog/\d{4}/\d{2}/.+'), handler)

И снова наш маршрут не будет совпадать с запросом из другого источника. Эту проблему можно решить с помощью wildcard (.+):

import { registerRoute } from 'workbox-routing'

registerRoute(new RegExp('.+/blog/\d{4}/\d{2}/.+'), handler)

Такой же фокус можно проделать с первым примером:

import { registerRoute } from 'workbox-routing'

registerRoute(new RegExp('.+\.js$'), jsHandler)

registerRoute(new RegExp('.+\.css$'), cssHandler)

С помощью колбэков

Для дальнейшей кастомизации определения совпадений можно воспользоваться функцией matchCallback. Данная функция принимает ExtendableEvent, Request и объект URL.

Простой пример:

import { registerRoute } from 'workbox-routing'

const matchFunction = ({ url, request, event }) => {
  return url.href === 'https://example.com/app.js'
}

registerRoute(matchFunction, handler)

Обработка запросов

Существует два способа обработки запросов:

  1. Использование одной из стратегий, предоставляемых workbox-strategies
  2. Предоставление функции обратного вызова, возвращающей промис, который разрешается объектом Response

С помощью стратегий

WB предоставляет следующие стратегии кэширования:

  • Stale While Revalidate (ресурс считается устаревшим после запроса) — ответ возвращается из кэша, после чего выполняется обновление кэша в фоновом режиме (если ответ еще не кэширован, он возвращается из сети). Данная стратегия является безопасной, поскольку кэш все время обновляется. Ее недостатком является постоянный запрос ресурсов из сети
  • Network First (сначала сеть) — сперва предпринимается попытка вернуть ответ из сети. Если данная попытка увенчалась успехом, ответ возвращается и после этого записывается в кэш. В противном случае, возвращается ответ из кэша
  • Cache First (сначала кэш) — если ответ имеется в кэше, он возвращается. Иначе отправляется запрос к сети и полученный (валидный) ответ записывается в кэш и после этого передается браузеру
  • Network Only (только сеть)
  • Cache Only (только кэш)

import { registerRoute } from 'workbox-routing'
import * as strategies from 'workbox-strategies'

registerRoute(match, new strategies.StaleWhileRevalidate())

registerRoute(match, new strategies.NetworkFirst())

registerRoute(match, new strategies.CacheFirst())

registerRoute(match, new strategies.NetworkOnly())

registerRoute(match, new strategies.CacheOnly())

Поведение маршрутизатора при использовании любой стратегии может быть кастомизировано посредством определения используемого кэша или добавления плагинов:

import { StaleWhileRevalidate } from 'workbox-strategies'

new StaleWhileRevalidate({
   // используем кастомный кэш
  cacheName: 'my-cache-name',

  // добавляем массив плагинов (например, `ExpirationPlugin`)
  plugins: [
    ...
  ]
})

С помощью колбэков

Для дальнейшей кастомизации обработки запросов можно воспользоваться асинхронным колбэком, возвращающим объект Response. Данный колбэк принимает объект со свойствами url и event:

import { registerRoute } from 'workbox-routing'

const handler = async ({ url, event }) => {
  return new Response(`Ответ от кастомного обработчика.`)
}

registerRoute(match, handler)

Следует отметить, что значение, возвращаемое колбэком match, передается в функцию обратного вызова handler в виде аргумента params:

import { registerRoute } from 'workbox-routing'

const match = ({ url, event }) => {
  if (url.pathname === '/example') {
    return {
      name: 'Workbox',
      type: 'Руководство',
    }
  }
}

const handler = async ({ url, event, params }) => {
  // ответ будет выглядеть как "Руководство по Workbox"
  return new Response(`${params.type} по ${params.name}`)
}

registerRoute(match, handler)

Это может быть полезным, например, в случае, когда у нас имеется какая-то информации, “зашитая” в URL, которая извлекается в match (путем разбора URL) и используется в handler.

Настройка WB

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

Настройка названия кэша

При использовании различных WB API можно заметить, что некоторый кэш создается автоматически.

При отсутствии названий кэша WB использует дефолтные названия, определенные в workbox-core. Предопределенные названия кэша выглядят так:

import { cacheNames } from 'workbox-core'

const precacheCacheName = cacheNames.precache
const runtimeCacheName = cacheNames.runtime
const googleAnalyticsCacheName = cacheNames.googleAnalytics

Каждое название состоит из трех частей:

<prefix>-<cacheId>-<suffix>

Эти названия можно менять как целиком, так и по частям:

import { setCacheNameDetails } from 'workbox-core'

setCacheNameDetails({
  prefix: 'my-app',
  suffix: 'v1'
})

Или так:

import { setCacheNameDetails } from 'workbox-core'

setCacheNameDetails({
  prefix: 'my-app',
  suffix: 'v1',
  precache: 'custom-precache-name',
  runtime: 'custom-runtime-name',
  googleAnalytics: 'custom-google-analytics-name'
})

Кастомные названия кэша в стратегиях

Некоторые части WB API принимают настройку cacheName с названием кэша. К таким API, в частности, относятся стратегии. В случае установки настройки cacheName, префикс и суффикс не используются. Например, вот как можно определить название для кэша изображений:

import { registerRoute } from 'workbox-routing'
import { CacheFirst } from 'workbox-strategies'

registerRoute(
  ({ request }) => request.destination === 'image',
  new CacheFirst({
    cacheName: 'my-image-cache'
  })
)

Теперь изображения будут записываться в кэш my-image-cache.

Настройки запросов в стратегиях

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

Для реализации такого сценария мы можем передать в конструктор стратегии настройку fetchOptions, соответствующую настройкам init Fetch API. Эти настройки будут применяться в отношении всех исходящих запросов, которые обрабатываются данной стратегией.

import { registerRoute } from 'workbox-routing'
import { NetworkFirst } from 'workbox-strategies'

registerRoute(
  ({ url }) => url.origin === 'https://third-party.example.com',
  new NetworkFirst({
    fetchOptions: {
      credentials: 'include'
    }
  })
)

Обработка запросов к другим источникам

Запросы к другим источникам обрабатываются WB особым образом.

Запросы к другим источникам и непрозрачные ответы

Одним из основных защитных механизмов, используемых браузером, является запрет доступа к телу и другим частям ответа, поступившего из другого источника без CORS-заголовков. Такие ответы являются "непрозрачными" (opaque). СВ в этом отношении не является исключением.

Не забывайте включать режим CORS

Если вы запрашиваете ресурсы с источника, поддерживающего CORS, но получаете непрозрачные ответы, возможно, вы неправильно их запрашиваете.

Например, следующий HTML запускает запросы, приводящие к непрозрачным ответам, даже если example.com поддерживает CORS:

<link rel="stylesheet" href="https://example.com/path/to/style.css" />
<img src="https://example.com/path/to/image.png" />

В данном случае, для отправки запросов, в ответ на которые возвращаются прозрачные ответы, следует включить режим CORS с помощью атрибута crossorigin:

<link
  crossorigin="anonymous"
  rel="stylesheet"
  href="https://example.com/path/to/style.css"
/>
<img crossorigin="anonymous" src="https://example.com/path/to/image.png" />

Опасность кэширования непрозрачных ответов

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

import { registerRoute } from 'workbox-routing'
import { CacheFirst } from 'workbox-strategies'

registerRoute('https://cdn.google.com/example-script.min.js', new CacheFirst())

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

Тем не менее, данный подход можно использовать с такими стратегиями как StaleWhileRevalidate и NetworkFirst, поскольку в этом случае время жизни плохого ответа будет непродолжительным за счет регулярного обновления кэша.

import { registerRoute } from 'workbox-routing'
import { NetworkFirst, StaleWhileRevalidate } from 'workbox-strategies'

registerRoute(
  'https://cdn.google.com/example-script.min.js',
  new NetworkFirst()
)

// или
registerRoute(
  'https://cdn.google.com/example-script.min.js',
  new StaleWhileRevalidate()
)

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

Принудительное кэширование непрозрачных ответов

Непрозрачные ответы могут принудительно записываться в кэш с помощью плагинов, таких как workbox-cacheable-response:

import { registerRoute } from 'workbox-routing'
import { NetworkFirst, StaleWhileRevalidate } from 'workbox-strategies'
import { CacheableResponsePlugin } from 'workbox-cacheable-response'

registerRoute(
  'https://cdn.google.com/example-script.min.js',
  new CacheFirst({
    plugins: [
      new CacheableResponsePlugin({
        statuses: [0, 200]
      })
    ]
  })
)

Использование плагинов

Плагины позволяют определять дополнительное поведение, связанное с обработкой запросов или ответов, в течение жизненного цикла запроса.

Плагины, предоставляемые WB

WB предоставляет следующие плагины:

  • BackgroundSyncPlugin — при отсутствии подключения к сети запрос помещается в очередь фоновой синхронизации (background sync queue) и повторно отправляется при следующем возникновении события sync
  • BroadcastUpdatePlugin — при любом обновлении кэша отправляется (dispatch) сообщение в BroadcastChannel или через postMessage()
  • CacheableResponsePlugin — кэширование только тех запросов, которые соответствуют установленным критериям
  • ExpirationPlugin — определение количества или максимального возраста записей в кэше
  • RangeRequestsPlugin — отправка ответов на запросы, включающие заголовок Range (частичный контент из кэша)

import { registerRoute } from 'workbox-routing'
import { CacheFirst } from 'workbox-strategies'
import { ExpirationPlugin } from 'workbox-expiration'

registerRoute(
  ({ request }) => request.destination === 'image',
  new CacheFirst({
    cacheName: 'images',
    plugins: [
      new ExpirationPlugin({
        maxEntries: 60,
        maxAgeSeconds: 30 * 24 * 60 * 60 // 30 дней
      })
    ]
  })
)

Кастомные плагины

Пользовательский плагин — это объект, содержащий один из следующих методов:

  • cacheWillUpdate: вызывается перед тем, как Response используется для обновления кэша. Он позволяет изменять ответ перед его добавлением в кэш или возвращать null для предотвращения обновления
  • cacheDidUpdate: вызывается перед добавлением новой записи в кэш или обновлением существующей записи. Может использоваться для выполнения каких-либо операций после обновления кэша
  • cacheKeyWillBeUsed: вызывается перед использованием запроса в качестве ключа для кэша, как для поиска кэша (в режиме read), так и для записи в кэш (в режиме write). Может использоваться для перезаписи или нормализации URL перед их использованием для доступа к кэшу
  • cacheResponseWillBeUsed: вызывается перед использованием ответа из кэша. Позволяет проверять ответ и возвращать null или другой ответ
  • requestWillFetch: вызывается перед выполнением сетевого запроса. Позволяет изменять Request
  • fetchDidFail: вызывается при провале запроса, чаще всего связанным с NetworkError. Обратите внимание, что данный метод не вызывается при получении ошибки, например, 404 Not Found
  • fetchDidSucceed: вызывается при успешном запросе, независимо от статуса ответа
  • handlerWillStart: вызывается перед запуском обработчика ответа. Может использоваться для установки начального состояния обработчика
  • handlerWillRespond: вызывается перед возвращением ответа обработчиком стратегии. Может использоваться для модификации ответа, возвращаемого обработчиком
  • handlerDidRespond: вызывается после возвращения ответа методом handle(). Может использоваться для записи любых деталей ответа, например, после изменений, произведенных с ним плагинами
  • handleDidComplete: вызывается после добавления в event всех "промисов для расширения жизненного цикла". Может использоваться для обработки данных, которые становятся доступными после выполнения своих задач обработчиком (например, кэширование статуса обращений, задержка кэширования, задержка сетевого запроса и т.д.)
  • handleDidError: вызывается, когда обработчик не может предоставить валидный ответ ни из кэша, ни из сети. Может использоваться для предоставления резервного контента при возникновении сетевой ошибки

Все эти функции вызываются с ключевым словом await при достижении кэшем (или событием fetch) контрольной точки.

Вот как выглядит использование этих колбэков:

const myPlugin = {
  cacheWillUpdate: async ({ request, response, event, state }) => {
    // Возвращаем `response`, другой объект `Response` или `null`
    return response
  },
  cacheDidUpdate: async ({
    cacheName,
    request,
    oldResponse,
    newResponse,
    event,
    state
  }) => {
    // Ничего не возвращаем.
    // Обратите внимание `newResponse.bodyUsed` имеет значение `true` при вызове.
    // Это означает, что тело запроса уже прочитано. Если вам требуется доступ
    // к телу свежего ответа:
    // const freshResponse = await caches.match(request, { cacheName })
  },
  cacheKeyWillBeUsed: async ({ request, mode, params, event, state }) => {
    // `request` - это объект `Request`, который может быть использован в качестве ключа кэша.
    // `mode` может иметь значение 'read' или 'write'.
    // Возвращаем строку или `Request`, свойство `url` которого будет использовано как ключ кэша.
    // Возврат оригинального `request` лишает смысла использование этого колбэка
    return request
  },
  cachedResponseWillBeUsed: async ({
    cacheName,
    request,
    matchOptions,
    cachedResponse,
    event,
    state,
  }) => {
    // Возвращаем `cachedResponse`, другой объект `Response` или `null`
    return cachedResponse
  },
  requestWillFetch: async ({ request, event, state }) => {
    // Возвращаем `request` или другой объект `Request`
    return request
  },
  fetchDidFail: async ({ originalRequest, request, error, event, state }) => {
    // Ничего не возвращаем.
    // Обратите внимание: `originalRequest` - это запрос браузера, `request` - это
    // запрос, обработанный плагинами в
    // колбэках `requestWillFetch`, а `error` - это исключение, вызвавшее
    // провал `fetch()`
  },
  fetchDidSucceed: async ({ request, response, event, state }) => {
    // Возвращаем `response` для использования сетевого ответа как есть или создаем и возвращаем новый объект `Response`
    return response
  },
  handlerWillStart: async ({ request, event, state }) => {
    // Ничего не возвращаем.
    // Здесь можно установить начальное состояние обработчика
  },
  handlerWillRespond: async ({ request, response, event, state }) => {
    // Возвращаем `response` или другой объект `Response`
    return response
  },
  handlerDidRespond: async ({ request, response, event, state }) => {
    // Ничего не возвращаем.
    // Здесь можно зафиксировать конечный результат
  },
  handlerDidComplete: async ({ request, response, error, event, state }) => {
    // Ничего не возвращаем.
    // Здесь можно работать с дополнительными данными
  },
  handlerDidError: async ({ request, event, error, state }) => {
    // Возвращаем `response` или другой объект `Response` (в качестве подстраховки), или `null`
    return response
  }
}

Как правило, объект event, передаваемый в колбэк — это исходное событие, вызвавшее операцию fetch или cache. Тем не менее, всегда следует проверять наличие данного объекта перед его использованием. При вызове метода handle() в стратегии event, передаваемый в него, будет передан в колбэки плагинов.

Все колбэки также принимают объект state, который является уникальным по отношению к объекту конкретного плагина и вызову стратегии. Это делает возможным реализацию плагинов, где один колбэк выполняет операции на основе данных, полученных от другого колбэка в том же плагине (например, вычисление дельты времени между запуском requestWillFetch() и fetchDidSucceed() или fetchDidFail()).

Квота хранилища

Каждый браузер устанавливает определенный лимит объема памяти, который может использоваться приложением. Данный лимит зависит от браузера. Объем используемой и доступной памяти можно проверить с помощью navigator.storage.estimate(). WB позволяет настроить автоматическую очистку хранилища при достижении квоты (storage quota).

import { registerRoute } from 'workbox-routing'
import { CacheFirst } from 'workbox-strategies'
import { ExpirationPlugin } from 'workbox-expiration'

registerRoute(
  ({ request }) => request.destination === 'image',
  // Используем стратегию "сначала кэш" со следующими настройками
  new CacheFirst({
    // Название кэша
    cacheName: 'images',
    plugins: [
      new ExpirationPlugin({
        // Максимальное количество записей в кэше (сохраняемых файлов)
        maxEntries: 50,
        // Срок хранения файлов в кэше
        maxAgeSeconds: 30 * 24 * 60 * 60,
        // Автоматическая очистка хранилища при достижении квоты
        purgeOnQuotaError: true
      })
    ]
  })
)

Общие рекомендации

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

Гугл-шрифты

Сервис гугл-шрифтов состоит из двух частей:

  • таблица стилей с определениями @font-face, содержащими ссылки на шрифты
  • статические файлы со шрифтами

Таблица стилей может часто изменяться, поэтому для нее лучше использовать стратегию stale-while-revalidate, которая выполняет обновление кэша после каждого запроса. Для файлов шрифтов, которые меняются редко, можно использовать стратегию cache-first.

import { registerRoute } from 'workbox-routing'
import { CacheFirst, StaleWhileRevalidate } from 'workbox-strategies'
import { CacheableResponsePlugin } from 'workbox-cacheable-response'
import { ExpirationPlugin } from 'workbox-expiration'

// Кэшируем таблицу стилей с помощью стратегии `stale-while-revalidate`
registerRoute(
  ({ url }) => url.origin === 'https://fonts.googleapis.com',
  new StaleWhileRevalidate({
    cacheName: 'google-fonts-stylesheets'
  })
)

// Кэшируем файлы со шрифтами с помощью стратегии `cache-first` на 1 год
registerRoute(
  ({ url }) => url.origin === 'https://fonts.gstatic.com',
  new CacheFirst({
    cacheName: 'google-fonts-webfonts',
    plugins: [
      new CacheableResponsePlugin({
        statuses: [0, 200],
      }),
      new ExpirationPlugin({
        maxAgeSeconds: 60 * 60 * 24 * 365,
        maxEntries: 30
      })
    ]
  })
)

Кэширование изображений

import { registerRoute } from 'workbox-routing'
import { ExpirationPlugin } from 'workbox-expiration'

registerRoute(
  ({ request }) => request.destination === 'image',
  new CacheFirst({
    cacheName: 'images',
    plugins: [
      new ExpirationPlugin({
        maxEntries: 60,
        maxAgeSeconds: 30 * 24 * 60 * 60 // 30 дней
      })
    ]
  })
)

Кэширование JS и CSS-файлов

import { registerRoute } from 'workbox-routing'
import { StaleWhileRevalidate } from 'workbox-strategies'

registerRoute(
  ({ request }) =>
    request.destination === 'script' || request.destination === 'style',
  new StaleWhileRevalidate({
    cacheName: 'static-resources'
  })
)

Кэширование контента из разных источников

import { registerRoute } from 'workbox-routing'
import { StaleWhileRevalidate } from 'workbox-strategies'

registerRoute(
  ({ url }) =>
    url.origin === 'https://fonts.googleapis.com' ||
    url.origin === 'https://fonts.gstatic.com',
  new StaleWhileRevalidate()
)

Кэширование ресурсов из определенного источника

import { registerRoute } from 'workbox-routing'
import { CacheFirst } from 'workbox-strategies'
import { CacheableResponsePlugin } from 'workbox-cacheable-response'
import { ExpirationPlugin } from 'workbox-expiration'

registerRoute(
  ({ url }) => url.origin === 'https://hacker-news.firebaseio.com',
  new CacheFirst({
    cacheName: 'stories',
    plugins: [
      new ExpirationPlugin({
        maxEntries: 50,
        maxAgeSeconds: 5 * 60 // 5 минут
      }),
      new CacheableResponsePlugin({
        statuses: [0, 200]
      })
    ]
  })
)

Установка таймера на выполнение сетевого запроса

В следующем примере мы пытаемся вернуть ответ из сети. Если запрос выполняется дольше 3 секунд, возвращается ответ из кэша:

import { registerRoute } from 'workbox-routing'
import { NetworkFirst } from 'workbox-strategies'
import { ExpirationPlugin } from 'workbox-expiration'

registerRoute(
  ({ url }) => url.origin === 'https://hacker-news.firebaseio.com',
  new NetworkFirst({
    networkTimeoutSeconds: 3,
    cacheName: 'stories',
    plugins: [
      new ExpirationPlugin({
        maxEntries: 50,
        maxAgeSeconds: 5 * 60 // 5 минут
      })
    ]
  })
)

Кэширование ресурсов из определенной поддиректории

import { registerRoute } from 'workbox-routing'
import { StaleWhileRevalidate } from 'workbox-strategies'

registerRoute(
  ({ url }) =>
    url.origin === self.location.origin && url.pathname.startsWith('/static/'),
  new StaleWhileRevalidate()
)

Кэширование на основе типа ресурса

import { registerRoute } from 'workbox-routing'
import { CacheFirst } from 'workbox-strategies'
import { ExpirationPlugin } from 'workbox-expiration'

registerRoute(
  // Кастомная функция `matchCallback`
  ({ request }) => request.destination === 'audio',
  new CacheFirst({
    cacheName: 'audio',
    plugins: [
      new ExpirationPlugin({
        maxEntries: 60,
        maxAgeSeconds: 30 * 24 * 60 * 60 // 30 дней
      })
    ]
  })
)

Доступ к кэшу из приложения

Cache Storage API доступен как в СВ, так и в контексте window. Если вы хотите изменить кэш — добавить или удалить из него записи или получить список кэшированных URL, вы можете сделать это напрямую, без обращения к СВ через postMessage().

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

// В app.js
async function addToCache(urls) {
  const myCache = await window.caches.open('my-cache')
  await myCache.addAll(urls)
}

// Вызываем `addToCache` в любое время, например, после загрузки страницы
window.addEventListener('load', () => {
  // Определяем список относительных `URL` для текущей страницы
  addToCache(['/static/relatedUrl1', '/static/relatedUrl2'])
})

Название кэша my-cache затем может быть использовано в СВ:

// Inside service-worker.js:

import { registerRoute } from 'workbox-routing'
import { StaleWhileRevalidate } from 'workbox-strategies'

registerRoute(
  ({ url }) =>
    url.origin === self.location.origin && url.pathname.startsWith('/static/'),
  new StaleWhileRevalidate({
    cacheName: 'my-cache'
  })
)

Продвинутые техники

Предлагаем пользователю перезагрузить страницу

Хорошей практикой является отображение “баннера” (оповещения) во время обновления и установки СВ.

Для реализации этого паттерна требуется некоторый код на странице и в СВ.

Код на странице

<script type="module">
  import { Workbox } from 'https://storage.googleapis.com/workbox-cdn/releases/6.1.5/workbox-window.prod.mjs'

  if ('serviceWorker' in navigator) {
    const wb = new Workbox('/sw.js')
    let registration

    const showSkipWaitingPrompt = (event) => {
      // `event.wasWaitingBeforeRegister` будет иметь значение `false`, если это
      // первый раз, когда обновленный СВ находится в режиме ожидания.
      // Когда `event.wasWaitingBeforeRegister` имеет значение `true`, предыдущий
      // обновленный СВ все еще находится в режиме ожидания.
      // Это позволяет кастомизировать UI.

      // Предположим, что приложение имеет несколько элементов UI,
      // которые пользователь может принять или отклонить
      const prompt = createUIPrompt({
        onAccept: () => {
          // Допустим, пользователь принял обновление, регистрируем обработчик,
          // который перезагрузит страницу как только предыдущий ожидающий
          // СВ получит контроль над ней
          wb.addEventListener('controlling', (event) => {
            window.location.reload()
          })

          wb.messageSkipWaiting()
        },

        onReject: () => {
          prompt.dismiss()
        }
      })
    }

    // Регистрируем обработчик, который определяет момент, когда
    // СВ был установлен, но ожидает активации
    wb.addEventListener('waiting', showSkipWaitingPrompt)

    wb.register()
  }
</script>

При обнаружении ожидающего СВ, мы сообщаем пользователю о том, что доступна обновленная версия приложения и запрашиваем у него разрешение на перезагрузку страницы. Если пользователь предоставил разрешение, мы используем messageSkipWaiting() для указания ожидающему СВ запустить self.skipWaiting(), что означает начало его активации. После того, как новый СВ был активирован и получил контроль над страницей, мы перезагружаем ее, что приводит к отображению последней версии предварительно кэшированных ресурсов.

Код в СВ

Если вы используете один из встроенных инструментов WB в режиме GenerateSW и skipWaiting имеет значение false (значение по умолчанию), то приведенный ниже код будет автоматически включен в генерируемого СВ.

Если вы создаете собственного СВ или работаете в режиме InjectManifest, тогда в код СВ необходимо добавить следующее:

addEventListener('message', (event) => {
  if (event.data && event.data.type === 'SKIP_WAITING') {
    self.skipWaiting()
  }
})

Этот код "слушает" сообщения с type: 'SKIP_WAITING' и запускает метод self.skipWaiting() для активации СВ.

Подготовка кэша во время выполнения

После настройки маршрутов для кэширования ресурсов, может возникнуть желание добавить некоторые файлы в кэш во время установки СВ.

Для этого нужные ресурсы следует записать в кэш во время выполнения:

import { cacheNames } from 'workbox-core'

self.addEventListener('install', (event) => {
  const urls = [
    /* ... */
  ]
  const cacheName = cacheNames.runtime
  event.waitUntil(
    caches
      .open(cacheName)
      .then((cache) => cache.addAll(urls))
    )
})

При использовании стратегий с кастомным названием для кэша просто присвойте это название свойству cacheName.

Резервный контент

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

Резервная страница

import * as navigationPreload from 'workbox-navigation-preload'
import { registerRoute, NavigationRoute } from 'workbox-routing'
import { NetworkOnly } from 'workbox-strategies'

const CACHE_NAME = 'offline-html'
// Мы исходим из предположения, что `/offline.html` - это `URL` самодостаточной страницы
// (без внешних изображений или стилей)
const FALLBACK_HTML_URL = '/offline.html'
// Записываем резервную страницу в кэш при установке СВ
self.addEventListener('install', async (event) => {
  event.waitUntil(
    caches.open(CACHE_NAME).then((cache) => cache.add(FALLBACK_HTML_URL))
  )
})

navigationPreload.enable()

const networkOnly = new NetworkOnly()
const navigationHandler = async (params) => {
  try {
    // Пытаемся выполнить сетевой запрос
    return await networkOnly.handle(params)
  } catch (error) {
    // В случае провала запроса, возвращаем резервную cтраницу из кэша
    return caches.match(FALLBACK_HTML_URL, {
      cacheName: CACHE_NAME
    })
  }
}

// Регистрируем данную стратегию для обработки всех маршрутов
registerRoute(new NavigationRoute(navigationHandler))

Более сложный пример

Все встроенные стратегии отклоняются (reject) одинаково при провале запроса и/или отсутствии кэша. Это позволяет добавить глобальный "перехватчик" для обработки всех ошибок.

import { matchPrecache, precacheAndRoute } from 'workbox-precaching'
import { registerRoute, setDefaultHandler, setCatchHandler } from 'workbox-routing'
import { CacheFirst, StaleWhileRevalidate } from 'workbox-strategies'

// Опционально: используем режим `injectManifest` с одним из
// встроенных инструментов для предварительного кэширования списка `URL`, включая резервные
precacheAndRoute(self.__WB_MANIFEST)

// Используем стратегию "сначала кэш" и определенный кэш для изображений
registerRoute(
  ({ request }) => request.destination === 'image',
  new CacheFirst({
    cacheName: 'images',
    plugins: [...]
  })
)

// Используем стратегию "считается устаревшим после запроса" для других ресурсов
setDefaultHandler(new StaleWhileRevalidate())

// Данный "перехватчик" запускается при провале любого запроса
setCatchHandler(async ({ event }) => {
  // `FALLBACK_URL` должен быть записан в кэш во время выполнения или предварительно
  // Если он был предварительно кэширован, следует вызывать
  // `matchPrecache(FALLBACK_URL)` (из пакета `workbox-precaching`)
  // для получения ответа из правильного кэша
  //
  // Используйте `event`, `request` и `url` для формирования правильного ответа.
  // Одним из способов является использование `request.destination`
  switch (event.request.destination) {
    case 'document':
      // При использовании предварительно кэшированных `URL`,
      // возвращаем matchPrecache(FALLBACK_HTML_URL)
      return caches.match(FALLBACK_HTML_URL)
    break

    case 'image':
      return caches.match(FALLBACK_IMAGE_URL)
    break

    case 'font':
      return caches.match(FALLBACK_FONT_URL)
    break

    default:
      // При отсутствии резервного контента, возвращаем ошибку
      return Response.error()
  }
})

Выполнение автономных запросов

Большинство разработчиков использует одну из стратегий как часть настройки маршрутизатора. Данная настройка позволяет автоматически реагировать на определенное событие fetch ответом, полученным от стратегии.

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

В таких случаях можно использовать стратегию в "автономном" режиме:

import { NetworkFirst } from 'workbox-strategies'

// В СВ
const strategy = new NetworkFirst({ networkTimeoutSeconds: 10 })

const response = await strategy.handle({
  request: new Request('https://example.com/path/to/file')
})
// Обрабатываем ответ

Параметр request является обязательным и должен иметь тип Request.

Параметр event является опциональным ExtendableEvent. Если данный параметр указан, он будет использоваться для сохранения СВ (через event.waitUntil()) в течение времени, достаточного для завершения "фонового" обновления кэша и его очистки.

handle() возвращает промис для объекта Response.

Более сложный пример

import { StaleWhileRevalidate } from 'workbox-strategies'

self.addEventListener('fetch', (event) => {
  if (event.request.url.endsWith('/complexRequest')) {
    event.respondWith(
      (async () => {
        // Продвинутая настройка стратегии
        const strategy = new StaleWhileRevalidate({ cacheName: 'api-cache' })

        // Выполняем два запроса с помощью стратегии.
        // Поскольку мы передаем `event`, `event.waitUntil()` вызывается автоматически
        const firstPromise = strategy.handle({
          event,
          request: 'https://example.com/api1',
        })
        const secondPromise = strategy.handle({
          event,
          request: 'https://example.com/api2',
        })

        const [firstResponse, secondResponse] = await Promise.all(
          firstPromise,
          secondPromise
        )
        const [firstBody, secondBody] = await Promise.all(
          firstResponse.text(),
          secondResponse.text()
        )

        // Предположим, что мы просто хотим объединить первый ответ от API со вторым для создания
        // финального ответа в формате HTML
        const compositeResponse = new Response(firstBody + secondBody, {
          headers: { 'content-type': 'text/html' },
        })

        return compositeResponse
      })()
    )
  }
})

Кэширование аудио и видео

<!-- На странице -->
<!-- В настоящее время установка атрибута `crossorigin` является обязательной даже для запросов из одного источника -->
<video src="movie.mp4" crossorigin="anonymous"></video>

import { registerRoute } from 'workbox-routing'
import { CacheFirst } from 'workbox-strategies'
import { CacheableResponsePlugin } from 'workbox-cacheable-response'
import { RangeRequestsPlugin } from 'workbox-range-requests'

// В СВ
// Для записи медиа в кэш можно использовать как предварительное кэширование, так и прямой вызов `cache.add()`
//
// Данный маршрутизатор отправит сетевой запрос при отсутствии совпадения с кэшем,
// но он не будет заполнять кэш во время выполнения.
// Если имеется совпадение с кэшем, он, возможно, будет иметь дело с частичными ответами
registerRoute(
  ({ url }) => url.pathname.endsWith('.mp4'),
  new CacheFirst({
    cacheName: 'your-cache-name-here',
    plugins: [
      new CacheableResponsePlugin({ statuses: [200] }),
      new RangeRequestsPlugin()
    ]
  })
)

Автоматическое создание СВ

workbox-cli

Устанавливаем CLI:

yarn global add workbox-cli
# или
npm i -g workbox-cli

Запускам wizard. Он настраивает CLI для проекта, задавая несколько вопросов о его структуре для определения файлов для кэширования.

workbox wizard

Создаем СВ:

workbox generateSW workbox-config.js

Регистрируем СВ на странице:

<script>
  // Проверяем поддержку СВ
  if ('serviceWorker' in navigator) {
    // Используем событие `load` объекта `window` для определения момента полной загрузки страницы
    window.addEventListener('load', () => {
      navigator.serviceWorker.register('/service-worker.js')
    })
  }
</script>

Добавляем в workbox-config.js настройку runtimeCaching для кэширования изображений во время выполнения:

module.exports = {
  globDirectory: 'build/',
  globPatterns: ['**/*.{html,json,js,css}'],
  swDest: 'build/sw.js',

  // Определяем правила для кэширования во время выполнения
  runtimeCaching: [
    {
      // Обрабатываем запрос на получение файлов с расширениями .png, .jpg, .jpeg или .svg
      urlPattern: /.(?:png|jpg|jpeg|svg)$/,

      // Применяем стратегию "сначала кэш"
      handler: 'CacheFirst',

      options: {
        // Используем кастомное название для кэша
        cacheName: 'images',

        // Кэшируем не более 10 изображений
        expiration: {
          maxEntries: 10
        }
      }
    }
  ]
}

Под капотом workbox-cli использует модуль workbox-build, облегчая интеграцию WB в процесс сборки проекта с помощью командной строки.

Режимы CLI

CLI имеет 4 режима:

  • wizard: пошаговое руководство по настройке WB для проекта
  • generateSW: генерация готового СВ
  • injectManifest: встраивание ресурсов для предварительного кэширования
  • copyLibraries: копирование библиотек WB в директорию проекта

wizard

wizard задает несколько вопросов, связанных с настройкой проекта, а также с тем, какие файлы нуждаются в предварительном кэшировании. Ответы используются для генерации файла с настройками, который используется при запуске в режиме generateSW.

Начальный файл с настройками можно кастомизировать с помощью любых поддерживаемых настроек.

generateSW

Используется для генерации готового СВ на основе файла с настройками.

workbox generateSW path/to/config.js

Когда использовать:

  • вы хотите предварительно кэшировать файлы
  • вам достаточно простой настройки кэширования во время выполнения (данная настройка позволяет определять маршрутизаторы и стратегии)

Когда лучше не использовать:

  • вы хотите использовать другие возможности СВ (например, пуш-уведомления)
  • вы хотите импортировать дополнительные скрипты или добавить дополнительную логику

injectManifest

Режим injectManifest предоставляет более полный контроль над тем, как будет выглядеть финальный СВ. Использование этого режима предполагает наличие файла с СВ (в локации, определенной в config.js).

При запуске injectManifest он ищет специальную строку (precacheAndRoute(self.__WB_MANIFEST) по умолчанию) в коде СВ. Он заменяет пустой массив списком URL для предварительного кэширования и записывает СВ в локацию, определенную в config.js. Остальной код СВ остается неизменным.

workbox injectManifest path/to/config.js

Когда использовать:

  • вам требуется больше контроля над СВ
  • вы хотите предварительно кэшировать файлы
  • вам требуется большая гибкость с точки зрения маршрутизации
  • вы хотите использовать СВ совместно с другими API (например, с пуш-уведомлениями)

Когда лучше не использовать:

  • вы ищете самый простой способ добавления СВ в приложение

copyLibraries

Данный режим может быть полезен, когда вы хотите использовать режим injectManifest и локальную копию WB вместо CDN.

workbox-build

Установка:

yarn add workbox-build
# или
npm i workbox-build

Вызываем generateSW(). Для генерации СВ необходимо добавить workboxBuild.generateSW() в Node.js-скрипт:

const workboxBuild = require('workbox-build')

// Обратите внимание: это должно запускаться после создания всех ресурсов
const buildSW = () => {
  // возвращается промис
  return workboxBuild.generateSW({
    globDirectory: 'build',
    globPatterns: ['**/*.{html,json,js,css}'],
    swDest: 'build/sw.js'
  })
}

Этот код создаст СВ в build/sw.js, который предварительно кэширует все файлы в директории build, совпадающие с globPatterns.

Регистрируем СВ на странице:

<script>
  if ('serviceWorker' in navigator) {
    window.addEventListener('load', () => {
      navigator.serviceWorker.register('/service-worker.js')
    })
  }
</script>

Настройка СВ для кэширования изображений во время выполнения идентична настройке, используемой для создания СВ с помощью workbox-cli.

Модуль workbox-build интегрируется в процесс сборки проекта с помощью Node.js и может генерировать готовый СВ или формировать список ресурсов для предварительного кэширования, который используется существующим СВ.

generateSW

// в build.js
const { generateSW } = require('workbox-build')

const swDest = 'build/sw.js'
generateSW({
  swDest,
  // другие настройки
}).then(({ count, size }) => {
  console.log(
    `Сгенерирован ${swDest}, который предварительно кэширует ${count} файлов. Общий размер кэшируемых файлов составляет ${size} байт.`
  )
})

injectManifest

// в build.js
const { injectManifest } = require('workbox-build')

const swSrc = 'src/sw.js'
const swDest = 'build/sw.js'
injectManifest({
  swSrc,
  swDest,
  // другие настройки
}).then(({ count, size }) => {
  console.log(
    `Сгенерирован ${swDest}, который предварительно кэширует ${count} файлов. Общий размер кэшируемых файлов составляет ${size} байт.`
  )
})

Webpack

WB предоставляет два плагина для вебпака: один для создания готового СВ, другой для формирования списка ресурсов для предварительного кэширования.

Эти плагины реализованы в виде двух классов в модуле workbox-webpack-plugin: GenerateSW и InjectManifest.

GenerateSW

// в webpack.config.js:
const { GenerateSW } = require('workbox-webpack-plugin')

module.exports = {
  // другие настройки вебпака
  plugins: [
    // другие плагины
    new GenerateSW({
      // дополнительные настройки
      option: 'value'
    })
  ]
}

InjectManifest

// в webpack.config.js:
const { InjectManifest } = require('workbox-webpack-plugin')

module.exports = {
  plugins: [
    new InjectManifest({
      swSrc: './src/sw.js'
    })
  ]
}

На этом первая часть руководства завершена. В следующей части мы подробно рассмотрим большинство модулей, предоставляемых WB, разработаем несколько сниппетов, которые можно будет использовать в приложениях “как есть”, а также кратко рассмотрим готовые решения для создания PWA, предоставляемые такими фреймворками для фронтенда как React и Vue.


Автор: S_ILya

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js