Русский
Русский
English
Статистика
Реклама

Workbox

WorkBox ваш toolkit в мире сервис-воркеров

13.10.2020 12:18:27 | Автор: admin

Привет, Хабр!

Меня зовут Святослав. Я работаю в компании ДомКлик и отвечаю за развитие сервисов оформления ипотеки. В начале года мы взяли курс на внедрение философии Progressive Web Application (PWA) в наших клиентских приложениях.

Одним из важных аспектов PWA является использование технологии Service Worker API, выполняющей роль прокси между браузером и сервером. Это позволяет поддерживать работу приложения в оффлайн-режиме, управлять кэшированием сетевых данных с их фоновой синхронизацией, а также работать с push-уведомлениями.

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

Workbox это разработанный в Google набор инструментов, предоставляющих высокоуровневый API для работы с такими браузерными технологиями, как Service Worker API и Cache Storage API. Инструментарий состоит из набора изолированных модулей, которые помогут вам сделать полноценное PWA-приложение.

Входящие в состав Workbox модули.Входящие в состав Workbox модули.

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

Управление кэшированием

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

Network Only

При получении запроса сервис-воркер перенаправляет его в сеть. Кэш не используется.

Cache Only

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

Network First

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

Cache First

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

Stale While Revalidate

Эта стратегия предполагает использование кэшированного ответа на запрос, если он доступен, но при этом уходит фоновый запрос на обновление кэша из сети. Это наиболее безопасная для пользователя стратегия, потому что она предполагает регулярное обновление данных кэша. Однако у неё есть недостаток: возникает нагрузка на сеть из-за фоновых запросов за обновленными данными.

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

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

import {registerRoute} from 'workbox-routing';import {CacheFirst} from 'workbox-strategies';import {CacheableResponsePlugin} from 'workbox-cacheable-response';import {ExpirationPlugin} from 'workbox-expiration';registerRoute(  ({request}) => request.destination === 'image',  new CacheFirst({    cacheName: 'assets',    plugins: [      new CacheableResponsePlugin({        statuses: [0, 200]      }),      new ExpirationPlugin({        maxEntries: 60,        maxAgeSeconds: 30 * 24 * 60 * 60, // 30 Days      })    ]  }));

Кэширование потоковых аудио- и видеоданных

Некоторые HTTP-запросы включают в себя заголовок Range:, который говорит серверу о том, чтобы тот вернул часть запрашиваемого ресурса. Такие запросы используются в случае потоковых аудио- и видеоданных для реализации чанковой загрузки информации, что является более выигрышной альтернативой по сравнению с одинарным запросом на весь объём данных.

Workbox содержит модуль workbox-range-requests, который реализует всю необходимую логику для поддержки чанковой загрузки с участием кэша.

Простой пример работы с этим модулем:

import {registerRoute} from 'workbox-routing';import {CacheFirst} from 'workbox-strategies';import {RangeRequestsPlugin} from 'workbox-range-requests';registerRoute(  ({url}) => url.pathname.endsWith('.mp4'),  new CacheFirst({    plugins: [      new RangeRequestsPlugin(),    ]  }););

Журналирование

Отладка работы сервис-воркера является неотъемлемой частью процесса разработки PWA-приложений. У Workbox есть отладочный режим, который позволяет выводить в консоль браузера подробную информацию о работе вашего сервис-воркера. Опираясь на журналы, разработчик сможет намного быстрее добраться до корня возникшей проблемы.

Кроссбраузерная работа

Workbox разрабатывается с учётом кроссбраузерной работы. Если браузер не поддерживает определённую технологию, будет использоваться альтернативная реализация.

К примеру:

  • Модуль оповещения об обновлении кэшированных данных (workbox-broadcast-cache-update) использует под капотом Broadcast Channel API. А если браузер его не поддерживает, то переключается на механизм postMessage.

  • Модуль фоновой синхронизации данных (workbox-background-sync) использует Background Sync API. При отсутствии браузерной поддержки модуль попытается повторить запрос из очереди событий во время следующего запуска сервис-воркера.

Интеграция с Google Analytics

При работе в оффлайн-режиме важно понимать, как пользователи взаимодействуют с вашим приложением. Но для работы сервисов аналитики (например, Google Analytics) важно наличие интернет-соединения для отправки отчетов на сервер. При отсутствии соединения данные о пользователе будут потеряны, и итоговый аналитический отчет окажется некорректен.

Модуль Workbox Google Analytics создан для решения этой проблемы. При оффлайн-работе он отлавливает неудачные запросы и сохраняет их в локальную базу данных браузера IndexedDB. А при возобновлении интернет-соединения запросы повторно отправляются на серверы Google Analytics.

Простой пример подключения этого модуля:

import * as googleAnalytics from 'workbox-google-analytics';googleAnalytics.initialize();

Способы использования

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

Работа с Webpack

Давайте рассмотрим один из способов использования Workbox вместе со сборщиком статических модулей Webpack. Для начала нам нужно установить плагин, который поставляется в виде npm-пакета:

npm install workbox-webpack-plugin --save-dev

Далее нужно подключить плагин в конфигурационном файле webpack.config.js, чтобы webpack автоматически сгенерировал воркер-файл на этапе сборки приложения.

Возьмем за основу пример из официальной документации. По умолчанию Workbox добавляет в предварительный кэш все файлы, которые участвуют в webpack-сборке. Но было бы ошибкой кэшировать все изображения вашего приложения, когда их число измеряется сотнями, а общий размер мегабайтами (нужно помнить, что браузер имеет квоту на хранение данных в Cache Storage). Вместо этого выставим такие параметры, чтобы сервис-воркер кэшировал изображения только тогда, когда приложение обращается за их загрузкой. А также установим лимит в 10 записей.

// Inside of webpack.config.js:const WorkboxPlugin = require('workbox-webpack-plugin');module.exports = {  // Other webpack config...  plugins: [    // Other plugins...    new WorkboxPlugin.GenerateSW({      // Do not precache images      exclude: [/\.(?:png|jpg|jpeg|svg)$/],      // Define runtime caching rules.      runtimeCaching: [{        // Match any request that ends with .png, .jpg, .jpeg or .svg.        urlPattern: /\.(?:png|jpg|jpeg|svg)$/,        // Apply a cache-first strategy.        handler: 'CacheFirst',        options: {          // Use a custom cache name.          cacheName: 'images',          // Only cache 10 images.          expiration: {            maxEntries: 10,          }        }      }]    })  ]};

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

Резюме

Workbox делает работу с сервис-воркерами более комфортной. Этот инструмент позволяет декларативно определить правила кэширования ресурсов приложения, взяв низкоуровневую работу на себя. К тому же Workbox уже интегрирован с такими инструментами разработки, как react-create-app, vue-cli, preact-cli, next.js, что говорит о его признании со стороны сообщества разработчиков.

Подробнее..

Обновление вашего PWA в продакшене

29.12.2020 02:22:15 | Автор: admin

Слышали шутку о том, что если установил ServiceWorker - пора менять домен? Сейчас я расскажу, в чём её смысл и что делать, если вы всё-таки решили, что вам необходим PWA.

В инструкциях по типу этой или этой ServiceWorker'у и работе с ним почти не уделяется внимания. И, я уверен, подобные статьи - первое, что вы прочтёте перед использованием. Но в момент, когда после подобных статей ваш свежеиспечённый PWA наконец-то появился на продакшене и у юзеров появилась возможность добавить ещё одну иконку на свой рабочий стол, знайте: вами пройдена точка невозврата.

С вашего позволения, я не буду останавливаться на описании Service Worker (далее SW) и том, как он работает. На Хабре уже есть хорошая статья об этом. Даже не важно, какой SW конкретно у вас. Может, вы используете create-react-app, а значит за SW у вас отвечает библиотека Workbox. Возможно, вы реализовывали SW сами, с какой-то мудрённой стратегией кэширования. Стек на самом деле не важен. В той же документации CRA говорится, что всё, что вам нужно - это поменять одну строчку и получить все прелести app-like поведения. Вы написали .register() и ожидаете результат. И вы его получите.

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

Обновите, пожалуйста, страницу. Как не помогает? А если CTRL+R ?

Итак, что же делать, когда судорожное обновление страницы не помогает и клиент всё ещё видит издевательски оранжевую кнопку?

Важно помнить, что SW пытается вести себя как десктопное приложение.

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

Схожим образом действует и браузер при обновлении SW.

Всего у SW три статуса: installing, waiting и active. Active - это ваш текущий, работающий SW. Стадии installing и waiting SW проходит на пути к active. На стадии installing SW нужно время, чтобы установиться. На стадии waiting ему нужна причина, чтобы заменить текущий SW (обычно это закрытие всех вкладок приложения). Вот в этом поведении и весь подвох.

Браузер получает новый SW, но юзер увидит изменения только тогда, когда приложение - все вкладки сайта будут закрыты. SW даже установит эту новую версию, но не активирует её. Пока все вкладки со старой версией не будут закрыты. Неважно, сколько раз вы обновляли страницу. Даже если это одна страница, вам всё равно нужно её закрыть.

Дело в том, что браузер начинает загружать обновлённую страницу до того, как старая "умрёт". И когда вы перезагружаете страницу, для SW существует аж две вкладки: старая, обречённая на смерть, и новая, которая ещё запускается. Пока не будут закрыты все, SW не обновится. Это нужно для того, чтобы вы не получали разные версии приложения во вкладках браузера.

Я намеренно пропускаю глубокий разбор механизма обновления SW с его озвученными выше статусами installing, waiting и active. Больше об этом написано тут - рекомендую ознакомиться. Мы уже понимаем механизм и вообще мы здесь, чтобы решить проблему.

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

Вариант 1: Заставить SW обновляться сразу

Самый простой (и опасный) способ - это просто пропустить ожидание в установке SW. В скоупе вашего SW есть прекрасная функция skipWaiting(), которая сделает это для вас. При её вызове новый SW после своей становки сразу убивает старый. Вам лишь надо дождаться "перезапуска" приложения.
Но будьте осторожны: данный подход несёт опасность, если у вашего пользователя открыты другие вкладки с приложением. Вам может показаться, что слепо вызывать skipWaiting() более чем достаточно, но это приводит к багам на вашем продакшене, которые потом сложно понять и воспроизвести.

Вариант 2: Перезагружать все вкладки когда новый SW установлен

Это слегка лучше, чем прошлый подход. В navigator.serviceWorker происходит эвентcontrollerchange ,когда новый SW получает контроль над текущей страницей. Это происходит сразу после прохождения этапа installing.
Теперь можно вызвать skipWaiting() во время установки, отловить эвент и заставить вкладку обновиться. Это будет выглядеть вот так:

navigator.serviceWorker.addEventListener('controllerchange',  ()  => window.location.reload());

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

Вариант 3: Дать пользователю самому вызвать обновление

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

Мы всё ещё перезагружаем страницу на срабатывании controllerchange, как и в предыдущем способе, но теперь пользователь знает о том, что это произойдёт и может этого избежать.
Для того, чтобы отследить новый SW, нам понадобится объект ServiceWorkerRegistration. Раньше мы просто вызывыли .register() и не знали, что этот метод возвращает промис с объектом регистрации. В этом API регистрации есть несколько интересных возможностей. Например, можно вызвать update(), чтобы обновить SW вручную. Обычно он делает это сам после регистрации, но вдруг вы хотите проверять наличие обновлений чаще.

Ссылку на текущий (active) SW можно получить через navigator.serviceWorker.controller из поля active в регистрации. Таким же образом можно достучаться до ожидающего (waiting) или устанавливающегося (installing) SW.

Любому SW можно отправить сообщение через postMessage(), если вы работали с iframe и передавали сообщения между окнами, вам знаком этот API. Внутри кода SW мы можем слушать это событие. Вы можете добавить следующий код в ваш SW.

addEventListener('message', ev => {    if (ev.data === 'skipWaiting') return skipWaiting();});

Если вы используете Workbox или CRA, то примерно этот код там уже есть.

Дальше нам нужно отследить появление ожидающего SW. На мой взгляд лучше не реагировать каждый раз на SW со статусом installing, как это пишут в некоторых руководствах, а дождаться когда объект регистрации SW вернёт true в поле waiting. Это замедляет обновление, но не триггерит ваше модальное окно когда SW устанавливается в первый раз.

После того, как мы дождались ожидающий SW, вызываем модальное окно, в котором пользователь может подтвердить обновление. При подтверждении мы вызываем skipWaiting() и насильно перезагружаем страницу, как описано выше. При отказе обновление будет отложено. Выглядеть код в моём случае будет так :

// вызов модального окнаconst askUserToUpdate = reg => {  return Modal.confirm({    onOk: async () => {      // вешаем обработчик изменения состояния      navigator.serviceWorker.addEventListener('controllerchange', () => {        window.location.reload();      });      // пропускаем ожидание       if (reg && reg.waiting) {        reg.waiting.postMessage({ type: 'SKIP_WAITING' });      }    },    onCancel: () => {      Modal.destroyAll();    },    icon: null,    title: 'Хорошие новости! 
Подробнее..

Автоматизируем сервис-воркер с Workbox 6. Доклад в Яндексе

17.04.2021 12:09:16 | Автор: admin
Задеплоил сервис-воркер нужно покупать новый домен, известная шутка о том, как сложно писать собственную логику кеширования. С приходом шестой версии библиотеки Workbox для прогрессивных веб-приложений (PWA) больше не нужен компромисс между гибкостью и удобством автоматизации сетевых задач. Максим Сальников рассказал, как начать работу с Workbox 6, реализовать типовую функциональность для офлайнового веб-приложения и пойти дальше, добавив собственную логику кеширования.

От докладчика:
Среди отзывов, полученных мной после этого доклада, занявшего второе место по полезности по результатам опроса после конференции, был такой: Краткий пересказ документации Workbox. Что ж, в какой-то степени это действительно так: я рассказал пошаговую историю превращения обычного веб-приложения в прогрессивное с помощью этой замечательной библиотеки. Буду очень рад, если доклад и конспект вдохновят вас и помогут начать ваше собственное путешествие в мир сервис-воркеров и PWA.

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

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



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

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

Например, компания Mozilla анонсировала, что в следующей версии Firefox функциональность PWA поддерживаться не будет. Немного кликбейтный заголовок, обозначающий то, что компания остановила разработку той части PWA, которая отвечает за установку веб-приложения на устройство. Но это толком там и не работало, было всегда скрыто за флагом, так что это не такая уж большая новость. К счастью, всё, что касается API сервис-воркера, остается, и разработка продолжается.

Компания Google радует теперь прогрессивные веб-приложения можно монетизировать, используя API для продажи цифровых товаров на их площадке Google Play. Для этого, правда, придется PWA обернуть в нативную оболочку мобильного приложения. К счастью, сделать это можно легко, используя инструмент от той же компании Google Trusted Web Activity, TWA.

Компания Apple на своей конференции для разработчиков анонсировала, что 16 API, связанных с доступом к аппаратным возможностям устройств, в ближайшее время не будут реализованы в WebKit (и соответственно, в Safari), потому что по наличию или отсутствию этих API и их возможностей можно составить цифровой профиль пользователя и тем самым нарушить его приватность.

Хорошие новости от компании Microsoft. Инструмент PWA Builder, позволяющий создавать дистрибутивы, которые мы можем загружать в магазины приложений, живет и развивается. И всё проще и проще становится отправлять наши прогрессивные веб-приложения в натуральном виде в магазин приложений Microsoft Store.

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


Ссылка со слайда

Что делать нам, разработчикам, как разрабатывать современные веб-приложения? Ответ давайте следовать веб-стандартам. Все API, которые мы с вами будем сегодня рассматривать часть веб-стандартов или, как минимум, черновики. То есть они прошли все необходимые обсуждения, слушания, все формальные процедуры, чтобы стать тем, что потенциально может стать веб-стандартом, если он сейчас в статусе черновика.

Технически прогрессивные улучшения или, как механизм для этого, feature detection то, что нам известно в мире фронтенда с незапамятных времен.


Ссылки со слайда: первая, вторая

Как понять все эти термины? PWA что это, как это связано с сервис-воркером, что включает в себя сервис-воркер? Я сделал простую диаграмму, чтобы задать контекст нашей сегодняшней дискуссии. Под идеей PWA понимаются два концепта, связанных с веб-приложениями: возможность их установки на устройства пользователей и интересные возможности через набор новых web-API.

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

Из многообразия этого подуровня API что-то связано с кешированием, но не все.


Ссылка со слайда

Если вам нужна мотивация в плане количественных показателей популярности сервис-воркеров 1% из выборки сайтов, которые находятся в HTTP Archive, используют сервис-воркеры. Рост заметен, не взрывной на текущий момент времени, но и останавливаться он не планирует.


Ссылка со слайда

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

Почему наблюдается такая популярность сервис-воркеров именно со стороны известных веб-ресурсов?



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

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

При этом мы оставляем всю природу и силу веба, если мы используем именно веб-технологии общедоступность, простоту обмена ссылками и открытость платформы.

Если все это так удобно для пользователей, почему каждое приложение не использует сервис-воркер?


Ссылка со слайда

Не все так просто с точки зрения разработчиков. Шутка из 2019 года все еще имеет под собой основания. Я вольно ее перевел и возьму смелость нарушить идею о том, что хорошая шутка в объяснении не нуждается все же объясню, что тут имеется в виду.


Ссылка со слайда

Дело в том, что в теории работа с сервис-воркером выглядит достаточно просто. Что такое сервис-воркер? Во-первых, это JavaScript-код. Во-вторых, это код, который исполняется в отдельном контексте относительно основного кода нашего приложения. В-третьих, это код, который исполняется только как ответ на какие-то события (events).

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

Событие install кладем нужные ресурсы в кеш: index.html, главный бандл JavaScript, главный бандл CSS, может быть, что-то еще.


Ссылка со слайда

Событие activate управляем версиями, если приложение обновилось.


Ссылка со слайда

Событие fetch тут мы немного обманываем основное приложение тем, что в ответ на все явные и неявные HTTP, a точнее HTTPS-запросы мы можем выдавать данные не из интернета, а те, что мы до этого закешировали. Это в теории.


Ссылка со слайда

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

Действительно, если мы говорим не о приложении Hello, World!, а о том, которое растет, меняет список ресурсов ведь даже те ресурсы, что у нас есть, могут добавлять в свои названия хеш-суммы, чтобы как-то работать с http-кешированием. Все это нужно предусмотреть.

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

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


Ссылка со слайда

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


Ссылка со слайда

Именно поэтому, если мы посмотрим на статистику HTTP Archive, примерно треть всех текущих сервис-воркеров в выборке адресов сделаны с помощью Workbox или его предков. Вот мой личный топ особенностей этой библиотеки, из-за которых мне она так нравится:

  • Уровень абстракций настолько комфортен для разработчика, насколько это возможно. У нас все еще есть полный контроль, полное понимание того, что именно мы делаем то есть не ждите большой кнопки сделать круто. При этом мы избавлены от копания в глубоких технических деталях HTTP-запросов, алгоритмов кеширования, если нам это не нужно. Если нужно, то можем опуститься и на этот уровень.
  • Где уместно, возвращается декларативность, применимая к идее кеширования, что тоже очень значительно упрощает и делает комфортной саму разработку.
  • Модульность помогает нам и системам сборки оптимизировать размер итогового файла сервис-воркера, неиспользуемый код не окажется в production.
  • Если нужно, мы всегда можем расширить текущие модули и текущие методы.
  • Функциональность из коробки настолько широка, что покроет, я думаю, процентов 90 всех возможных сценариев, которые потребуются при создании, в частности, офлайн-приложения.
  • Мощный инструментарий: инструменты командной строки, модули для Node, плагины для систем сборки.
  • Очень важный момент бесплатность, открытый исходный код, активная разработка и поддержка со стороны Google и сообщества разработчиков.

Я надеюсь, что убедил вас попробовать. Давайте сделаем это незамедлительно.



Помните тот самый демонстративный кусок кода, несколько десятков строчек? В Workbox это будет одна строчка вызов одного конкретного метода, который называется precacheAndRoute. Его название говорит само за себя: Workbox, закешируй этот набор ресурсов и настрой роутинг, чтобы эти ресурсы выдавались из кеша, если это возможно. Как параметр этому методу мы передаем массив, содержащий конкретный список ресурсов.



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

В первую очередь заполнить эту переменную конкретным списком файлов (и их хеш-сумм), которые нам необходимо закешировать.

Нативные импорты JavaScript в сервис-воркерах пока еще не работают, поэтому сервис-воркер будет необходимо собрать.

В конце концов нужно сообщить приложению, что теперь оно работает под управлением сервис-воркера.



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



Это модуль для Node, соответственно добавим немного JavaScript-кода.



В конфигурации мы укажем тот самый исходный сервис-воркер и адрес итогового сервис-воркера.



Самое главное мы должны объяснить Workbox, какие именно файлы составляют ту самую программную оболочку, тот самый application shell. К счастью, это делается очень удобным методом glob-шаблонов. Я гарантирую, что вы сможете настроить необходимую выборку ресурсов для вашего даже сложного по конфигурации веб-приложения. В этом конфиге есть много других свойств, которые позволяют вам совершенно точно затюнинговать этот процесс.



Вызов метода injectManifest с говорящим названием.



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



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



В общем случае нам понадобятся три плагина.


Ссылка со слайда

Как их настроить, можно посмотреть на этом слайде. Базовая настройка требуется только в плагине rollup-replace, который позволяет выбирать режим Workbox, development или production, заменой строки в исходном коде Workbox.

Чем отличаются режимы? В режиме production вся отладочная информация будет полностью исключена, в режиме development вы увидите детальный лог в консоли браузера, который позволяет точно прослеживать, что именно делает Workbox. Очень удобно.

Terser сделает минификацию, чтобы получить примерно такой вид итогового сервис-воркера.



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

Осталось интегрировать сборку сервис-воркера в общий билд приложения.


Ссылка со слайда

Последний шаг из нашего набора регистрация в приложении. Тут можно пойти путем вызова нативных методов, метод register для объекта сервис-воркер, а можно опять обратиться за помощью к нашему герою Workbox.

С использованием модуля workbox-window у нас есть удобный метод для регистрации на самом деле это синтаксический сахар для нативного метода, плюс там сразу внедрена лучшая практика по моменту регистрации сервис-воркера, а именно в событии window.onload. Чтобы не конкурировать за сетевые и процессорные ресурсы с основным потоком JavaScript, мы откладываем регистрацию сервис-воркера настолько, насколько это возможно.

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



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

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

Исходный код именно для этой части сервис-воркера есть в репозитории по адресу aka.ms/workbox6. Там находится код сегодняшнего демонстрационного приложения и ссылка на его онлайн-версию, чтобы вы могли с ним поиграть и посмотреть, какие процессы там происходят. Открывайте DevTools, вкладку Application, что я и сделаю прямо сейчас.

Итак, приложение простой блог, сделаем его офлайн-готовым, то есть работающим вне зависимости от подключения к сети. Давайте посмотрим, как это выглядит.



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

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

Что мы в итоге получили? Полностью работающую офлайн-оболочка приложения, управление версиями со стороны Workbox и детализированную информацию для разработчиков.

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



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



Если мы в блоге используем аватары из сервиса Gravatar, для этого мы можем настроить самую консервативную стратегию всегда брать их из кеша, если они там доступны с предыдущей загрузки. Как вы видите, этот метод очень декларативен, то есть это выглядит как настройка роутинга. Для адресов, который попадают под этот шаблон, в данном случае я его задаю через RegExp, мы просим Workbox использовать стратегию Cache First.



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



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

В то же время в моем примере кода вы можете увидеть, что используется плагин Broadcast Update, который дает нам возможность отправить сообщение приложению. Если данные обновились, можно спросить пользователя: Не желаете ли получить обновленную статью?



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


Ссылка со слайда

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

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


Ссылка со слайда

Аналогично с картинками. Есть хорошие практики для динамического кеширования картинок, и Workbox позволяет эти практики применять буквально одной строчкой. В данном случае я задаю, что хочу кешировать не более чем 10 элементов.



Существует еще рецепт для production-ready, стабильно работающего офлайн-фолбека всевозможных сценариев.



Кеширование самих страниц.



Кеширование статических ресурсов, а именно JavaScript- и CSS-файлов.



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


Ссылка со слайда

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

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

Итак, что нам нужно сделать? Нам нужно расширить базовый класс в стратегии, назовем его CacheNetworkRace.


Ссылка со слайда

В этом классе нужно реализовать метод с названием _handle, куда мы передаем сам HTTP-запрос и очень важно экземпляр класса StrategyHandler


Ссылка со слайда


Ссылка со слайда

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


Ссылка со слайда

Аналогично можно не обращаться напрямую к API Cache Storage, а использовать удобный метод для поиска этого ресурса.

Что нам остается? Реализовать ту самую гонку в виде: Какой промис первым выполнится успешно, тот мы и отдаем. Тут же можно сделать обработку ошибок.



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

Друзья, мне осталось показать только некоторые интересные и полезные ресурсы. Вернемся к репозиторию, где вы найдете исходный код нашего демо-приложения, как оно работает, можете сразу увидеть ссылку на онлайн-версию. Все это настроено и работает на Azure Static Web Apps. Я вам рекомендую попробовать этот способ хостинга статических веб-приложений, который максимально автоматизирует весь цикл приложения вы указываете только GitHub-репозиторий, его ветку, и через несколько мгновений ваше приложение, собранное и задеплоенное, находится уже в сети.

Продолжить общение на тему прогрессивных веб-приложений и сервис-воркеров я вам предлагаю в русскоязычном сообществе pwa_ru в телеграме. Еще одна интересная ссылка ведет на другой GitHub-репозиторий, где мы собрали максимально широкий набор ресурсов о прогрессивных веб-приложениях, сервис-воркерах, кешировании на русском языке. Там же вы найдете целую кучу примеров PWA в production и из глобального интернета, и из рунета. Я вам обещаю, многие посещаемые вами сайты, которые вы используете достаточно часто, это прогрессивные веб-приложения или, по крайней мере, они используют части сервис-воркеров для оптимизации вашего пользовательского опыта.

Друзья, прогрессивные веб-приложения это про улучшение пользовательского опыта. Workbox это про улучшение опыта разработчика. Давайте создавать быстрые и удобные веб-приложения и получать от этого удовольствие. Спасибо вам огромное за внимание.
Подробнее..

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

21.06.2021 12:17:59 | Автор: admin

image


Что такое Workbox?


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


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



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


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

Это вторая часть руководства. Вот ссылка на первую часть.


Модули, предоставляемые WB


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


  • workbox-background-sync: фоновая синхронизация, позволяющая выполнять сетевые запросы в режиме офлайн
  • workbox-broadcast-update: отправка уведомлений об обновлении кэша (через Broadcast Channel API)
  • workbox-cacheable-response: фильтрация кэшируемых запросов на основе статус-кодов или заголовков ответов
  • workbox-core: изменение уровня логгирования и названий кэша. Содержит общий код, используемый другими модулями
  • workbox-expiration: установка лимита записей в кэше и времени жизни сохраненных ресурсов
  • workbox-google-analytics: фиксация действий пользователей на странице в режиме офлайн
  • workbox-navigation-preload: предварительная загрузка запросов, связанных с навигацией
  • workbox-precaching: предварительное кэширование ресурсов и управление их обновлением
  • workbox-range-request: поддержка частичных ответов
  • workbox-recipes: общие паттерны использования WB
  • workbox-routing: обработка запросов с помощью встроенных стратегий кэширования или колбэков
  • workbox-strategies: стратегии кэширования во время выполнения, как правило, используемые совместно с workbox-routing
  • workbox-streams: формирование ответа на основе нескольких источников потоковой передачи данных
  • workbox-window: регистрация, управление обновлением и обработка событий жизненного цикла СВ

workbox-background-sync


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


Новый BackgroundSync API отличное решение для такой ситуации. Когда СВ обнаруживает провалившийся запрос, он может регистрировать возникновение события sync, отправляемого брузером при восстановлении соединения. Данное событие отправляется даже если пользователь вышел из приложения, что делает этот подход гораздо более эффективным, чем традиционные способы повторного выполнения провалившихся запросов.


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


Базовое использование


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


import { BackgroundSyncPlugin } from 'workbox-background-sync'import { registerRoute } from 'workbox-routing'import { NetworkOnly } from 'workbox-strategies'const bgSyncPlugin = new BackgroundSyncPlugin('myQueueName', {  maxRetentionTime: 24 * 60, // Попытка выполнения повторного запроса будет выполнена в течение 24 часов (в минутах)})registerRoute(  /\/api\/.*\/*.json/,  new NetworkOnly({    plugins: [bgSyncPlugin],  }),  'POST')

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


Рассматриваемый модуль предоставляет класс Queue, который, после инстанцирования, может использоваться для хранения провалившихся запросов. Такие запросы записываются в IndexedDB и извлекаются из нее при восстановлении соединения.


Создание очереди


import { Queue } from 'workbox-background-sync'const queue = new Queue('myQueueName') // название очереди должно быть уникальным

Название очереди используется как часть названия "тега", который получает register() глобального SyncManager. Оно также используется как название "объектного хранилища" IndexedDB.


Добавление запроса в очередь


import { Queue } from 'workbox-background-sync'const queue = new Queue('myQueueName')self.addEventListener('fetch', (event) => {  // Клонируем запрос для безопасного чтения  // при добавлении в очередь  const promiseChain = fetch(event.request.clone()).catch((err) => {    return queue.pushRequest({ request: event.request })  })  event.waitUntil(promiseChain)})

После добавления в очередь, запрос будет автоматически выполнен повторно при получении СВ события sync (или при следующем запуске СВ в браузерах, которые не поддерживают фоновую синхронизацию).


workbox-cacheable-response


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


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


Кэширование на основе статус-кода


import { registerRoute } from 'workbox-routing'import { CacheFirst } from 'workbox-strategies'import { CacheableResponsePlugin } from 'workbox-cacheable-response'registerRoute(  ({ url }) =>    url.origin === 'https://example.com' && url.pathname.startsWith('/images/'),  new CacheFirst({    cacheName: 'image-cache',    plugins: [      new CacheableResponsePlugin({        statuses: [0, 200]      })    ]  }))

Данная настройка указывает WB кэшировать любые ответы со статусом 0 или 200 при обработке запросов к https://example.com.


Кэширование на основе заголовка


import { registerRoute } from 'workbox-routing'import { StaleWhileRevalidate } from 'workbox-strategies'import { CacheableResponsePlugin } from 'workbox-cacheable-response'registerRoute(  ({ url }) => url.pathname.startsWith('/path/to/api/'),  new StaleWhileRevalidate({    cacheName: 'api-cache',    plugins: [      new CacheableResponsePlugin({        headers: {          'X-Is-Cacheable': 'true'        }      })    ]  }))

При обработке ответов на запросы к URL, начинающемуся с /path/to/api/, проверяется, присутствует ли в ответе заголовок X-Is-Cacheable (который добавляется сервером). Если заголовок присутствует и имеет значение true, такой ответ кэшируется.


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


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


import { registerRoute } from 'workbox-routing'import { StaleWhileRevalidate } from 'workbox-strategies'import { CacheableResponsePlugin } from 'workbox-cacheable-response'registerRoute(  ({ url }) => url.pathname.startsWith('/path/to/api/'),  new StaleWhileRevalidate({    cacheName: 'api-cache',    plugins: [      new CacheableResponsePlugin({        statuses: [200, 404],        headers: {          'X-Is-Cacheable': 'true'        }      })    ]  }))

При использовании встроенной стратегии без явного определения cacheableResponse.CacheableResponsePlugin, для проверки валидности ответа используются следющие критерии:


  • staleWhileRevalidate и networkFirst: ответы со статусом 0 (непрозрачные ответы) и 200 считаются валидными
  • cacheFirst: только ответы со статусом 200 считаются валидными

По умолчанию заголовки ответа для определения его валидности не используются.


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


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


import { CacheableResponse } from 'workbox-cacheable-response'const cacheable = new CacheableResponse({  statuses: [0, 200],  headers: {    'X-Is-Cacheable': 'true'  }})const response = await fetch('/path/to/api')if (cacheable.isResponseCacheable(response)) {  const cache = await caches.open('api-cache')  cache.put(response.url, response)} else {  // Ответ не может быть кэширован}

workbox-expiration


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


Ограничение количества записей в кэше


import { registerRoute } from 'workbox-routing'import { CacheFirst } from 'workbox-strategies'import { ExpirationPlugin } from 'workbox-expiration'registerRoute(  ({ request }) => request.destination === 'image',  new CacheFirst({    cacheName: 'image-cache',    plugins: [      new ExpirationPlugin({        // ограничиваем количество записей в кэше        maxEntries: 20      })    ]  }))

При достижении лимита удаляются самые старые записи.


Ограничение времени хранения ресурсов в кэше


import { registerRoute } from 'workbox-routing'import { CacheFirst } from 'workbox-strategies'import { ExpirationPlugin } from 'workbox-expiration'registerRoute(  ({ request }) => request.destination === 'image',  new CacheFirst({    cacheName: 'image-cache',    plugins: [      new ExpirationPlugin({        // ограничиваем время хранения ресурсов в кэше        maxAgeSeconds: 24 * 60 * 60      })    ]  }))

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


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


Класс CacheExpiration позволяет отделять логику ограничения от других модулей. Для установки ограничений создается экземпляр названного класса:


import { CacheExpiration } from 'workbox-expiration'const cacheName = 'my-cache'const expirationManager = new CacheExpiration(cacheName, {  maxAgeSeconds: 24 * 60 * 60,  maxEntries: 20})

Затем, при обновлении записи в кэше, вызывается метод updateTimestamp() для обновления "возраста" записи.


await openCache.put(request, response)await expirationManager.updateTimestamp(request.url)

Для проверки всех записей в кэше на предмет их соответствия установленным критериям вызывается метод expireEntries():


await expirationManager.expireEntries()

workbox-precaching


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


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


WB предоставляет простой и понятный API для реализации этого паттерна и эффективной загрузки ресурсов.


При первом запуске приложения workbox-precaching "смотрит" на загружаемые ресурсы, удаляет дубликаты и регистрирует соответствующие события СВ для загрузки и хранения ресурсов. URL, которые содержат информацию о версии (версионную информацию) (например, хэш контента) используются в качестве ключей кэша без дополнительной модификации. К ключам кэша URL, которые не содержат такой информации, добавляется параметр строки запроса, представляющий хэш контента, генерируемый WB во время выполнения.


workbox-precaching делает все это при обработке события install СВ.


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


Новый СВ не будет использоваться для ответов на запросы до его активации. В событии activate workbox-precaching определяет кэшированные ресурсы, отсутствующие в новом списке URL, и удаляет их из кэша.


Обработка предварительно кэшированных ответов


Вызов precacheAndRoute() или addRoute() создает маршрутизатор, который определяет совпадения запросов с предварительно кэшированными URL.


В этом маршрутизаторе используется стратегия "сначала кэш".


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


Список предварительно кэшируемых ресурсов


workbox-precaching ожидает получения массива объектов со свойствами url и revision. Данный массив иногда называют "манифестом предварительного кэширования":


import { precacheAndRoute } from 'workbox-precaching'precacheAndRoute([  { url: '/index.html', revision: '383676' },  { url: '/styles/app.0c9a31.css', revision: null },  { url: '/scripts/app.0d5770.js', revision: null },  // другие записи])

Свойства revision второго и третьего объектов имеют значения null. Это объясняется тем, что версионная информация этих объектов является частью значений их свойств url.


В отличие от JavaScript и CSS URL, указывающие на HTML-файлы, как правило, не включают в себя версионную информацию по той причине, что ссылки на такие файлы должны быть статическими.


Версионирование позволяет рассматриваемому модулю определять необходимость обновления кэшированного ресурса.


Обратите внимание: для генерации списка предварительно кэшируемых ресурсов следует использовать один из встроенных инструментов WB: workbox-build, workbox-webpack-plugin или workbox-cli. Создавать такой список вручную очень плохая идея.


Автоматическая обработка входящих запросов


При поиске совпадения входящего запроса с кэшированным ресурсом workbox-precaching автоматически выполняет некоторые манипуляции с URL.


Например, запрос к / оценивается как запрос к index.html.


Игнорирование параметров строки запроса


По умолчанию игнорируются параметры поиска, которые начинаются с utm_ или точно совпадают с fbclid. Это означает, что запрос к /about.html?utm_campaign=abcd оценивается как запрос к /about.html.


Игнорируемые параметры указываются в настройке ignoreURLParametersMatching:


import { precacheAndRoute } from 'workbox-precaching'precacheAndRoute(  [    { url: '/index.html', revision: '383676' },    { url: '/styles/app.0c9a31.css', revision: null },    { url: '/scripts/app.0d5770.js', revision: null }  ],  {    // Игнорируем все параметры    ignoreURLParametersMatching: [/.*/]  })

Основной файл директории


По умолчанию основным файлом директории считается index.html. Именно поэтому запросы к / оцениваются как запросы к /index.html. Это поведение можно изменить с помощью настройки directoryIndex:


import { precacheAndRoute } from 'workbox-precaching'precacheAndRoute(  [    { url: '/index.html', revision: '383676' },    { url: '/styles/app.0c9a31.css', revision: null },    { url: '/scripts/app.0d5770.js', revision: null },  ],  {    directoryIndex: null  })

"Чистые" URL


По умолчанию к запросу добавляется расширение .html. Например, запрос к /about оценивается как /about.html. Это можно изменить с помощью настройки cleanUrls:


import { precacheAndRoute } from 'workbox-precaching'precacheAndRoute([{ url: '/about.html', revision: 'b79cd4' }], {  cleanUrls: false})

Кастомные манипуляции


Настройка urlManipulation позволяет кастомизировать логику определения совпадений. Эта функция должна возвращать массив возможных совпадений:


import { precacheAndRoute } from 'workbox-precaching'precacheAndRoute(  [    { url: '/index.html', revision: '383676' },    { url: '/styles/app.0c9a31.css', revision: null },    { url: '/scripts/app.0d5770.js', revision: null }  ],  {    urlManipulation: ({ url }) => {      // Логика определения совпадений      return [alteredUrlOption1, alteredUrlOption2]    }  })

workbox-routing


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


workbox-routing это модуль, позволяющий "связывать" поступающие запросы с функциями, формирующими на них ответы.


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


Обратите внимание на следующее:


  • важно указывать метод запроса. По умолчанию перехватываются только GET-запросы
  • маршрутизаторы должны регистрироваться в правильном порядке. При наличии нескольких "роутов" для одного запроса, сработает только первый из них

Определение совпадений и обработка запросов


В WB "роут" это две функции: функция "определения совпадения" и функция "обработки запроса".


WB предоставляет некоторые утилиты для помощи в реализации названных функций.


Функция определения совпадения принимает ExtendableEvent, Request и объект URL. Возврат истинного значения из этой функции означает совпадение. Например, вот пример определения совпадения с конкретным URL:


const matchCb = ({ url, request, event }) => {  return (url.pathname === '/special/url')}

Функция обработки запроса принимает такие же параметры + аргумент value, который имеет значение, возвращаемое из первой функции:


const handlerCb = async ({ url, request, event, params }) => {  const response = await fetch(request)  const responseBody = await response.text()  return new Response(`${responseBody} <!-- Глядите-ка! Новый контент. -->`, {    headers: response.headers  })}

Обработчик должен возвращать промис, разрешающийся Response.


Регистрация колбэков выглядит следующим образом:


import { registerRoute } from 'workbox-routing'registerRoute(matchCb, handlerCb)

Единственным ограничением является то, что функция определения совпадения должна возвращать истинное значение синхронно. Это связано с тем, что Router должен синхронно обрабатывать событие fetch или передавать его другим обработчикам.


Как правило, в функции обработки запроса используется одна из встроенных стратегий, например:


import { registerRoute } from 'workbox-routing'import { StaleWhileRevalidate } from 'workbox-strategies'registerRoute(  matchCb,  new StaleWhileRevalidate())

Определение совпадений с помощью регулярного выражения


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


import { registerRoute } from 'workbox-routing'registerRoute(  new RegExp('/styles/.*\\.css'),  handlerCb)

Для запросов из одного источника данная "регулярка" будет регистрировать совпадения для следующих URL:



Однако, в случае с запросами к другим источникам, регулярка должна совпадать с началом URL. Поэтому совпадение для следующих URL обнаружено не будет:



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


new RegExp('https://cdn\\.third-party-site\\.com.*/styles/.*\\.css')

Для регистрации совпадений как с локальными, так и со сторонними URL можно использовать wildcard, но при этом следует проявлять особую осторожность.


Роут для навигации


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


import { createHandlerBoundToURL } from 'workbox-precaching'import { NavigationRoute, registerRoute } from 'workbox-routing'// Предположим, что страница `/app-shell.html` была предварительно кэшированаconst handler = createHandlerBoundToURL('/app-shell.html')const navigationRoute = new NavigationRoute(handler)registerRoute(navigationRoute)

При посещении пользователем вашего сайта, запрос на получение страницы будет считаться навигационным, следовательно, ответом на него будет кэшированная страница /app-shell.html.


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


import { createHandlerBoundToURL } from 'workbox-precaching'import { NavigationRoute, registerRoute } from 'workbox-routing'const handler = createHandlerBoundToURL('/app-shell.html')const navigationRoute = new NavigationRoute(handler, {  allowlist: [    new RegExp('/blog/')  ],  denylist: [    new RegExp('/blog/restricted/')  ]})registerRoute(navigationRoute)

Обратите внимание, что denyList имеет приоритет перед allowList.


Обработчик по умолчанию


import { setDefaultHandler } from 'workbox-routing'setDefaultHandler(({ url, event, params }) => {  // ...})

Обработчик ошибок


import { setCatchHandler } from 'workbox-routing'setCatchHandler(({ url, event, params }) => {  // ...})

Обработка не-GET-запросов


import { registerRoute } from 'workbox-routing'registerRoute(  matchCb,  handlerCb,  // определяем метод  'POST')registerRoute(  new RegExp('/api/.*\\.json'),  handlerCb,  // определяем метод  'POST')

workbox-strategies


Стратегия кэширования это паттерн, определяющий порядок формирования СВ ответа на запрос (после возникновения события fetch).


Вот какие стратегии предоставляет рассматриваемый модуль.


Stale-While-Revalidate


Данная стратегия возвращает ответ из кэша (при наличии ответа в кэше) или из сети (при отсутствии кэшированного ответа). Сетевой запрос используется для обновления кэша. Такой запрос выполняется независимо от возраста кэшированного ответа.


import { registerRoute } from 'workbox-routing'import { StaleWhileRevalidate } from 'workbox-strategies'registerRoute(  ({url}) => url.pathname.startsWith('/images/avatars/'),  new StaleWhileRevalidate())

Cache-Fisrt


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


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


import { registerRoute } from 'workbox-routing'import { CacheFirst } from 'workbox-strategies'registerRoute(  ({ request }) => request.destination === 'style',  new CacheFirst())

Network-First


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


import { registerRoute } from 'workbox-routing'import { NetworkFirst } from 'workbox-strategies'registerRoute(  ({ url }) => url.pathname.startsWith('/social-timeline/'),  new NetworkFirst())

Network-Only


import { registerRoute } from 'workbox-routing'import { NetworkOnly } from 'workbox-strategies'registerRoute(  ({url}) => url.pathname.startsWith('/admin/'),  new NetworkOnly())

Cache-Only


import { registerRoute } from 'workbox-routing'import { CacheOnly } from 'workbox-strategies'registerRoute(  ({ url }) => url.pathname.startsWith('/app/v2/'),  new CacheOnly())

Настройка стратегии


Каждая стратегия позволяет кастомизировать:


  • название кэша
  • лимит записей в кэше и время их "жизни"
  • плагины

Название кэша


import { registerRoute } from 'workbox-routing'import { CacheFirst } from 'workbox-strategies'registerRoute(  ({ request }) => request.destination === 'image',  new CacheFirst({    cacheName: 'image-cache',  }))

Плагины


В стратегии могут использоваться следующие плагины:


  • workbox-background-sync
  • workbox-broadcast-update
  • workbox-cacheable-response
  • workbox-expiration
  • workbox-range-requests

import { registerRoute } from 'workbox-routing'import { CacheFirst } from 'workbox-strategies'import { ExpirationPlugin } from 'workbox-expiration'registerRoute(  ({ request }) => request.destination === 'image',  new CacheFirst({    cacheName: 'image-cache',    plugins: [      new ExpirationPlugin({        // Хранить ресурсы в течение недели        maxAgeSeconds: 7 * 24 * 60 * 60,        // Хранить до 10 ресурсов        maxEntries: 10      })    ]  }))

WB также позволяет создавать и использовать собственные стратегии.


workbox-recipies


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


Рецепты


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


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


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


По умолчанию резервная страница должна иметь название offline.html.


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


Рецепт


import { offlineFallback } from 'workbox-recipes'import { setDefaultHandler } from 'workbox-routing'import { NetworkOnly } from 'workbox-strategies'setDefaultHandler(  new NetworkOnly())offlineFallback()

Паттерн


import { setCatchHandler, setDefaultHandler } from 'workbox-routing'import { NetworkOnly } from 'workbox-strategies'const pageFallback = 'offline.html'const imageFallback = falseconst fontFallback = falsesetDefaultHandler(  new NetworkOnly())self.addEventListener('install', event => {  const files = [pageFallback]  if (imageFallback) {    files.push(imageFallback)  }  if (fontFallback) {    files.push(fontFallback)  }  event.waitUntil(self.caches.open('workbox-offline-fallbacks').then(cache => cache.addAll(files)))})const handler = async (options) => {  const dest = options.request.destination  const cache = await self.caches.open('workbox-offline-fallbacks')  if (dest === 'document') {    return (await cache.match(pageFallback)) || Response.error()  }  if (dest === 'image' && imageFallback !== false) {    return (await cache.match(imageFallback)) || Response.error()  }  if (dest === 'font' && fontFallback !== false) {    return (await cache.match(fontFallback)) || Response.error()  }  return Response.error()}setCatchHandler(handler)

Подготовка кэша


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


Рецепт


import { warmStrategyCache } from 'workbox-recipes'import { CacheFirst } from 'workbox-strategies'// Здесь может испоьзоваться любая стратегияconst strategy = new CacheFirst()const urls = [  '/offline.html']warmStrategyCache({urls, strategy})

Паттерн


import { CacheFirst } from 'workbox-strategies'// Здесь может использоваться любая стратегияconst strategy = new CacheFirst()const urls = [  '/offline.html',]self.addEventListener('install', event => {  // `handleAll` возвращает два промиса, второй промис разрешается после добавления всех элементов в кэш  const done = urls.map(path => strategy.handleAll({    event,    request: new Request(path),  })[1])  event.waitUntil(Promise.all(done))})

Кэширование страницы


Данный рецепт позволяет СВ отвечать на запрос на получение HTML-страницы с помощью стратегии "сначала сеть". При этом, СВ оптимизируется таким образом, что в случае отсутствия подключения к сети, возвращает ответ из кэша менее чем за 4 секунды. По умолчанию запрос к сети выполняется в течение 3 секунд. Настройка warmCache позволяет подготовить ("разогреть") кэш к использованию.


Рецепт


import { pageCache } from 'workbox-recipes'pageCache()

Паттерн


import { registerRoute } from 'workbox-routing'import { NetworkFirst } from 'workbox-strategies'import { CacheableResponsePlugin } from 'workbox-cacheable-response'const cacheName = 'pages'const matchCallback = ({ request }) => request.mode === 'navigate'const networkTimeoutSeconds = 3registerRoute(  matchCallback,  new NetworkFirst({    networkTimeoutSeconds,    cacheName,    plugins: [      new CacheableResponsePlugin({        statuses: [0, 200]      })    ]  }))

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


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


Рецепт


import { staticResourceCache } from 'workbox-recipes'staticResourceCache()

Паттерн


import { registerRoute } from 'workbox-routing'import { StaleWhileRevalidate } from 'workbox-strategies'import { CacheableResponsePlugin } from 'workbox-cacheable-response'const cacheName = 'static-resources'const matchCallback = ({ request }) =>  // CSS  request.destination === 'style' ||  // JavaScript  request.destination === 'script' ||  // веб-воркеры  request.destination === 'worker'registerRoute(  matchCallback,  new StaleWhileRevalidate({    cacheName,    plugins: [      new CacheableResponsePlugin({        statuses: [0, 200]      })    ]  }))

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


Данный рецепт позволяет СВ отвечать на запросы на получение изображений с помощью стратегии "сначала кэш". По умолчанию кэшируется до 60 изображений в течение 30 дней. Поддерживается разогрев кэша.


Рецепт


import { imageCache } from 'workbox-recipes'imageCache()

Паттерн


import { registerRoute } from 'workbox-routing'import { CacheFirst } from 'workbox-strategies'import { CacheableResponsePlugin } from 'workbox-cacheable-response'import { ExpirationPlugin } from 'workbox-expiration'const cacheName = 'images'const matchCallback = ({ request }) => request.destination === 'image'const maxAgeSeconds = 30 * 24 * 60 * 60const maxEntries = 60registerRoute(  matchCallback,  new CacheFirst({    cacheName,    plugins: [      new CacheableResponsePlugin({        statuses: [0, 200]      }),      new ExpirationPlugin({        maxEntries,        maxAgeSeconds      })    ]  }))

Кэширование гугл-шрифтов


Данный рецепт кэширует таблицу стилей для шрифтов с помощью стратегии "считается устаревшим после запроса" и сами шрифты с помощью стратегии "сначала кэш". По умолчанию кэшируется до 30 шрифтов в течение 1 года.


Рецепт


import { googleFontsCache } from 'workbox-recipes'googleFontsCache()

Паттерн


import { registerRoute } from 'workbox-routing'import { StaleWhileRevalidate } from 'workbox-strategies'import { CacheFirst } from 'workbox-strategies'import { CacheableResponsePlugin } from 'workbox-cacheable-response'import { ExpirationPlugin } from 'workbox-expiration'const sheetCacheName = 'google-fonts-stylesheets'const fontCacheName = 'google-fonts-webfonts'const maxAgeSeconds = 60 * 60 * 24 * 365const maxEntries = 30registerRoute(  ({ url }) => url.origin === 'https://fonts.googleapis.com',  new StaleWhileRevalidate({    cacheName: sheetCacheName  }))// Кэшируем до 30 шрифтов с помощью стратегии "сначала кэш" и храним кэш в течение 1 годаregisterRoute(  ({ url }) => url.origin === 'https://fonts.gstatic.com',  new CacheFirst({    cacheName: fontCacheName,    plugins: [      new CacheableResponsePlugin({        statuses: [0, 200],      }),      new ExpirationPlugin({        maxAgeSeconds,        maxEntries      })    ]  }))

Быстрое использование


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


import {  pageCache,  imageCache,  staticResourceCache,  googleFontsCache,  offlineFallback} from 'workbox-recipes'pageCache()googleFontsCache()staticResourceCache()imageCache()offlineFallback()

workbox-window


Данный модуль выполняется в контексте window. Его основными задачами является следующее:


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

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


<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')  wb.register()}</script>

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


Установка


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

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


import { Workbox } from 'workbox-window'if ('serviceWorker' in navigator) {  const wb = new Workbox('/sw.js')  wb.register()}

Примеры


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


const wb = new Workbox('/sw.js')wb.addEventListener('activated', (event) => {  // `event.isUpdate` будет иметь значение `true`, если другая версия СВ  // управляет страницей при регистрации данной версии  if (!event.isUpdate) {    console.log('СВ был активирован в первый раз!')    // Если СВ настроен для предварительного кэширования ресурсов,    // эти ресурсы могут быть получены здесь  }})// Региструем СВ после добавления обработчиков событийwb.register()

Уведомление пользователя о том, что СВ был установлен, но ожидает активации


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


const wb = new Workbox('/sw.js')wb.addEventListener('waiting', (event) => {  console.log(    `Новый СВ был установлен, но он не может быть активирован, пока все вкладки браузера не будут закрыты или перезагружены`  )})wb.register()

Уведомление пользователя об обновлении кэша


Модуль workbox-broadcast-update позволяет информировать пользователей об обновлении контента. Для получения этой информации в браузере используется событие message с типом CACHE_UPDATED:


const wb = new Workbox('/sw.js')wb.addEventListener('message', (event) => {  if (event.data.type === 'CACHE_UPDATED') {    const { updatedURL } = event.data.payload    console.log(`Доступна новая версия ${updatedURL}!`)  }})wb.register()

Отправка СВ списка URL для кэширования


В некоторых приложениях имеет смысл кэшировать только те ресурсы, которые используются посещенной пользователем страницей. Модуль workbox-routing принимает список URL и кэширует их на основе правил, определенных в маршрутизаторе.


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


const wb = new Workbox('/sw.js')wb.addEventListener('activated', (event) => {  // Получаем `URL` текущей страницы + все загружаемые страницей ресурсы  const urlsToCache = [    location.href,    ...performance      .getEntriesByType('resource')      .map((r) => r.name)  ]  // Передаем этот список СВ  wb.messageSW({    type: 'CACHE_URLS',    payload: { urlsToCache }  })})wb.register()

Практика


В этом разделе представлено несколько сниппетов, которые можно использовать в приложениях "как есть", а также краткий обзор готовых решений для разработки PWA, предоставляемых такими фреймворками для фронтенда, как React и Vue.


Для обеспечения работы приложения в режиме офлайн требуется не только СВ и его регистрация в основной файле приложения, но и так называемый "манифест".


О том, что такое манифест можно почитать здесь, здесь и здесь.


Как правило, манифест (и СВ) размещаются на верхнем уровне (в корневой директории) проекта. Манифест может иметь расширение .json или .webmanifest (лучше использовать первый вариант).


Манифест


{  "name": "Название приложения",  "short_name": "Краткое название (будет указано под иконкой приложения при его установке)",  "scope": "/", // зона контроля СВ, разные страницы могут обслуживаться разными СВ  "start_url": ".", // начальный URL, как правило, директория, в которой находится index.html, в котором регистрируется СВ  "display": "standalone",  "orientation": "portrait",  "background_color": "#f0f0f0",  "theme_color": "#3c3c3c",  "description": "Описание приложения",  // этих иконок должно быть достаточно для большинства девайсов  "icons": [    {      "src": "./icons/64x64.png",      "sizes": "64x64",      "type": "image/png"    },    {      "src": "./icons/128x128.png",      "sizes": "128x128",      "type": "image/png"    },    {      "src": "./icons/256x256.png",      "sizes": "256x256",      "type": "image/png",      "purpose": "any maskable"    },    {      "src": "./icons/512x512.png",      "sizes": "512x512",      "type": "image/png"    }  ],  "serviceworker": {    "src": "./service-worker.js" // ссылка на файл с кодом СВ  }}

Ручная реализация СВ, использующего стратегию "сначала кэш"


// Название кэша// используется для обновления кэша// в данном случае, для этого достаточно изменить версию кэша - my-cache-v2const CACHE_NAME = 'my-cache-v1'// Критические для работы приложения ресурсыconst ASSETS_TO_CACHE = [  './index.html',  './offline.html',  './style.css',  './script.js']// Предварительное кэширование ресурсов, выполняемое во время установки СВself.addEventListener('install', (e) => {  e.waitUntil(    caches      .open(CACHE_NAME)      .then((cache) => cache.addAll(ASSETS_TO_CACHE))  )  self.skipWaiting()})// Удаление старого кэша во время активации нового СВself.addEventListener('activate', (e) => {  e.waitUntil(    caches      .keys()      .then((keys) =>        Promise.all(          keys.map((key) => {            if (key !== CACHE_NAME) {              return caches.delete(key)            }          })        )      )  )  self.clients.claim()})// Обработка сетевых запросов/*  1. Выполняется поиск совпадения  2. Если в кэше имеется ответ, он возвращается  3. Если ответа в кэше нет, выполняется сетевой запрос  4. Ответ на сетевой запрос кэшируется и возвращается  5. В кэш записываются только ответы на `GET-запросы`  6. При возникновении ошибки возвращается резервная страница*/self.addEventListener('fetch', (e) => {  e.respondWith(    caches      .match(e.request)      .then((response) =>          response || fetch(e.request)            .then((response) =>              caches.open(CACHE_NAME)                .then((cache) => {                  if (e.request.method === 'GET') {                    cache.put(e.request, response.clone())                  }                  return response                })          )      )      .catch(() => caches.match('./offline.html'))  )})

Конфигурация Webpack


Пример настройки вебпака для производственной сборки прогрессивного веб-приложения.


Предположим, что в нашем проекте имеется 4 директории:


  • public директория со статическими ресурсами, включая index.html, manifest.json и sw-reg.js
  • src директория с кодом приложения
  • build директория для сборки
  • config директория с настройками, включая .env, paths.js и webpack.config.js

В файле public/sw-reg.js содержится код регистрации СВ:


if ('serviceWorker' in navigator) {  window.addEventListener('load', () => {    navigator.serviceWorker      .register('./service-worker.js')      .then((reg) => {        console.log('СВ зарегистрирован: ', reg)      })      .catch((err) => {        console.error('Регистрация СВ провалилась: ', err)      })  })}

В файле config/paths.js осуществляется экспорт путей к директориям с файлами приложения:


const path = require('path')module.exports = {  public: path.resolve(__dirname, '../public'),  src: path.resolve(__dirname, '../src'),  build: path.resolve(__dirname, '../build')}

Допустим, что в качестве фронтенд-фреймворка мы используем React, а также, что в проекте используется TypeScript. Тогда файл webpack.config.js будет выглядеть следующим образом:


const webpack = require('webpack')// импортируем пути к директориям с файлами приложенияconst paths = require('../paths')// плагин для копирования статических ресурсов в директорию сборкиconst CopyWebpackPlugin = require('copy-webpack-plugin')// плагин для обработки `index.html` - вставки ссылок на стили и скрипты, добавления метаданных и т.д.const HtmlWebpackPlugin = require('html-webpack-plugin')// плагин для обеспечения прямого доступа к переменным среды окруженияconst Dotenv = require('dotenv-webpack')// плагин для минификации и удаления неиспользуемого CSSconst MiniCssExtractPlugin = require('mini-css-extract-plugin')// плагин для сжатия изображенийconst ImageminPlugin = require('imagemin-webpack-plugin').default// плагин для добавления блоков кодаconst AddAssetHtmlPlugin = require('add-asset-html-webpack-plugin')// Плагин для генерации СВconst { GenerateSW } = require('workbox-webpack-plugin')// настройки Babelconst babelLoader = {  loader: 'babel-loader',  options: {    presets: ['@babel/preset-env', '@babel/preset-react'],    plugins: [      '@babel/plugin-proposal-class-properties',      '@babel/plugin-syntax-dynamic-import',      '@babel/plugin-transform-runtime'    ]  }}module.exports = {  // режим сборки  mode: 'production',  // входная точка  entry: {    index: {      import: `${paths.src}/index.js`,      dependOn: ['react', 'helpers']    },    react: ['react', 'react-dom'],    helpers: ['immer', 'nanoid']  },  // отключаем логгирование  devtool: false,  // результат сборки  output: {    // директория сборки    path: paths.build,    // название файла    filename: 'js/[name].[contenthash].bundle.js',    publicPath: './',    // очистка директории при каждой сборке    clean: true,    crossOriginLoading: 'anonymous',    module: true  },  resolve: {    alias: {      '@': `${paths.src}/components`    },    extensions: ['.mjs', '.js', '.jsx', '.ts', '.tsx', '.json']  },  experiments: {    topLevelAwait: true,    outputModule: true  },  module: {    rules: [      // JavaScript, React      {        test: /\.m?jsx?$/i,        exclude: /node_modules/,        use: babelLoader      },      // TypeScript      {        test: /.tsx?$/i,        exclude: /node_modules/,        use: [babelLoader, 'ts-loader']      },      // CSS, SASS      {        test: /\.(c|sa|sc)ss$/i,        use: [          'style-loader',          {            loader: 'css-loader',            options: { importLoaders: 1 }          },          'sass-loader'        ]      },      // статические ресурсы - изображения и шрифты      {        test: /\.(jpe?g|png|gif|svg|eot|ttf|woff2?)$/i,        type: 'asset'      },      {        test: /\.(c|sa|sc)ss$/i,        use: [          MiniCssExtractPlugin.loader,          {            loader: 'css-loader',            options: { importLoaders: 1 }          },          'sass-loader'        ]      }    ]  },  plugins: [    new CopyWebpackPlugin({      patterns: [        {          from: `${paths.public}/assets`        }      ]    }),    new HtmlWebpackPlugin({      template: `${paths.public}/index.html`    }),    // это позволяет импортировать реакт только один раз    new webpack.ProvidePlugin({      React: 'react'    }),    new Dotenv({      path: './config/.env'    }),    new MiniCssExtractPlugin({      filename: 'css/[name].[contenthash].css',      chunkFilename: '[id].css'    }),    new ImageminPlugin({      test: /\.(jpe?g|png|gif|svg)$/i    }),    // Добавляем код регистрации СВ в `index.html`    new AddAssetHtmlPlugin({ filepath: `${paths.public}/sw-reg.js` }),    // Генерируем СВ    new GenerateSW({      clientsClaim: true,      skipWaiting: true    })  ],  optimization: {    runtimeChunk: 'single'  },  performance: {    hints: 'warning',    maxEntrypointSize: 512000,    maxAssetSize: 512000  }}

Здесь вы найдете шпаргалку по настройке вебпака. Пример полной конфигурации вебпака для JS/React/TS-проекта можно посмотреть здесь.


React PWA


Для того, чтобы получить готовый шаблон React-приложения с возможностями PWA, достаточно выполнить команду:


yarn create react-app my-app --template pwa# илиnpx create-react-app ...

Или, если речь идет о TypeScript-проекте:


yarn create react-app my-app --template pwa-typescript# илиnpx create-react-app ...

Кроме прочего, в директории src создаются файлы service-worker.ts и serviceWorkerRegister.ts (последний импортируется в index.tsx), а в директории public файл manifest.json.


Затем, перед сборкой проекта с помощью команды yarn build или npm run build, в файл src/index.tsx необходимо внести одно изменение:


// доserviceWorkerRegistration.unregister();// послеserviceWorkerRegistration.register();

Подробнее об этом можно прочитать здесь.


Vue PWA


С Vue дела обстоят еще проще.


Глобально устанавливаем vue-cli:


yarn global add @vue/cli# илиnpm i -g @vue/cli

Затем, при создании шаблона проекта с помощью команды vue create my-app, выбираем Manually select features и Progressive Web App (PWA) Support.


Кроме прочего, в директории src создается файл registerServiceWorker.ts, который импортируется в main.ts. Данный файл содержит ссылку на файл service-worker.js, который, как и manifest.json, автоматически создается при сборке проекта с помощью команды yarn build или npm run build. Разумеется, содержимое обоих файлов можно кастомизировать.

Подробнее..

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru