PWA для SSR приложения в 5 строк на Workbox 6.
В начале хочу поблагодарить и выразить свою признательность компании Яндекс за миссионерскую деятельность в области образования, обмена знаниями и опытом в сфере IT технологий и особенно, за последнюю онлайн-конференцию “Я люблю фронтенд”, которая и дала толчок к написанию данной статьи.
В статье хочу затронуть вопросы:
- что такое PWA
- для чего нашему приложению нужен сервис-воркер
- почему решили переписать сервис-воркер на Workbox 6
- вопрос целостности кэша и некоторые особенности Workbox 6 при обновлении версий
PWA (progressive web app) — технология в web-разработке, которая визуально и функционально трансформирует сайт в приложение.
Пожалуй самым главным свойством нативного приложения является мгновенный старт и отображение оболочки приложения — обычные веб-приложения, как вы знаете, не обладают такой функциональностью — в зависимости от местоположения пользователя и качества мобильного интернета первая страница приложения может загружаться несколько секунд, отображая белый экран.
Наше приложение является React Server Side Rendering приложением. Для своей работы оно требует загрузки некоторого количества скриптов, стилей и изображений. Ресурсам указываются атрибуты, которые предполагают длительное их хранение в кэше браузера пользователя (для последующего более быстрого старта). Но к сожалению, современный пользователь в час просматривает такое количество ресурсов и загружает такое количество контента, которое просто не оставляет шанса остаться в кэше нашим ресурсам до следующего захода пользователя в наше приложение. Поэтому наш сервер вынужден при каждом соединении отдавать десятки ресурсов снова и снова, нагружая каналы связи, пожирая процессорное время сервера и замедляя отображение нашего приложения пользователю.
(сегодня технологическими компаниями ведутся исследования и разработки по хранению кэша не в общей куче, а для каждого домена отдельно, что в будущем возможно решит проблему вымывания кэша)
Решить данную проблему коренным образом сегодня позволяет кэширующий сервис-воркер, который надежно сохраняет ресурсы в браузере и таким образом превращает наше приложение в PWA, позволяя практически мгновенно стартовать нашему приложению и отображать свой первоначальный контент.
Пару лет назад нами был написан такой сервис-воркер, который содержал порядка 200 строк, выполнял свои функции и в принципе нас устраивал. Он при первом посещении сайта сохраняет в кэш все необходимые ресурсы, а так же специальную страницу, которая служит оболочкой приложения и загружается из кэша каждый раз когда пользователь открывает наш сайт, реализуя App Shell модель приложения.
До этого я несколько раз присматривался к Workbox ранних версий и примерял его к нашему проекту, но каждый раз что-то не складывалось и наверное к лучшему.
Собственно подтолкнул меня к окончательному переходу на Workbox доклад Максима Сальникова — “Автоматизируем сервис-воркер с Workbox 6”. Максим в докладе показал код для новой версии Workbox и сказал пару правильных мыслей о том, что код сервис-воркера, написанный руками по многим аспектам плох:
- спецификации сервис-воркеров подвержены изменениям
- реализация сервис-воркеров в разных браузерах может отличаться
- до конца нет полной уверенности, что код написан верно
- автоматизацию сборки сервис-воркера нужно делать самостоятельно
- код сервис-воркера слабо структурирован и не рассчитан на будущие изменения
- он громоздок и плохо читаем
- он, как правило, не декларативен
- да и со временем код устаревает и за ним нужно постоянно следить, а на это как правило не хватает времени
и еще много много чего плохого может оказаться в самописном коде для сервис-воркера. Почему бы не переложить весь груз ответственности за написание и поддержку кода на тех, кто занимается этим профессионально и использовать готовый код в своем приложении?
В общем окунулся я в изучение Workbox и в течении 2х дней наш 200-строчный код превратился в несколько строк!
import { skipWaiting, clientsClaim } from 'workbox-core';
import { registerRoute, NavigationRoute } from 'workbox-routing';
import { precacheAndRoute, createHandlerBoundToURL } from 'workbox-precaching';
precacheAndRoute(self.__WB_MANIFEST);
registerRoute(
new NavigationRoute(createHandlerBoundToURL('/app-shell.html'))
);
skipWaiting();
clientsClaim();
Вот и весь код нашего сервис-воркера!
На старте он кэширует все ресурсы, сгенерированные нашим бандлером, в кэш — self.__WB_MANIFEST
содержит в себе массив url ресурсов. Затем он кэширует и регистрирует app-shell.html
в качестве ресурса, который будет загружаться вместо страниц с сервера. Последние 2 инструкции переводят наш сервис-воркер в активное состояние управлением страницей сразу же после своей загрузки и окончания кэширования ресурсов.
Для того чтобы инжектировать self.__WB_MANIFEST
в код сервис-воркера — нужно использовать подходящий плагин для вашего бандлера — делается это в пару строк (см документацию).
При 1-й загрузке страницы Workbox скачивает все необходимые для кэширования ресурсы с сервера, не смотря на то что возможно они уже были загружены и находятся в кэше браузера.
При изменении ресурсов на сервере — Workbox загружает и обновляет в кэше только измененные (раньше на всех поголовно ресурсах и описаниях сервис-воркеров предлагался код который удалял весь старый кэш и заново загружал все ресурсы, что было глупо и расточительно — мы этот момент у себя исправляли более аккуратной загрузкой только изменившихся ресурсов)
Расскажу о маленьком ЧП с нашим приложением, чтобы описать проблему целостности кэша и как она решается в Workbox.
Однажды к нам в техподдержку обратился пользователь со странным багом — приложение не загружалось. Мы долго долго ломали голову в чем проблема, до тех пор пока не решили исследовать состояние кэша нашего приложения в его браузере — оказалось, что там не достает нескольких ресурсов. То-ли сам пользователь баловался и удалил их, то-ли какое-то расширение поработало, но приложение оставалось не рабочим и усилиями пользователя восстановлено не могло быть.
Мы озаботились этой проблемой и написали код который при навигации смотрел в кэш и если не находил там ресурс из манифеста — шел за ним на сервер, отдавал его браузеру и кэшировал его снова.
Это решало проблему целостности кэша при его повреждении.
При переходе на Workbox обнаружилось, что если удалить весь кэш — Workbox будет делать вид, что все в порядке и просто будет ходить за всеми ресурсами молча на сервер, не помещая снова их в кэш, сводя на 0 все усилия которые были приложены при написании сервис-воркера, чтобы избегать такой ситуации.
Я описал ситуацию разработчикам Workbox, и хотя они со мной не согласились в том , что восстанавливать кэш необходимо, все же предложили вариант исправления данной ситуации — нужно в начало кода добавить описание плагина, который будет выполнять работу по загрузке и сохранению в кэш ресурсов из манифеста, в случае их отсутствия в кэше
import { addPlugins } from 'workbox-precaching';
const repopulatePrecachePlugin = {
cachedResponseWillBeUsed: async ({
cacheName,
request,
cachedResponse
}) => {
// If there's a cache hit, we're good.
if (cachedResponse) { return cachedResponse; }
// Otherwise, repopulate the precache.
const cache = await caches.open(cacheName);
const response = await fetch(request);
// Optional: check the responses status as appropriate.
if (!response.ok) { return null; }
await cache.put(request, response.clone());
return response;
},
};
addPlugins([repopulatePrecachePlugin]);
Добавив этот код в начало нашего сервис-воркера мы обеспечим целостность кэша, гарантируя то, что если загруженный ресурс из манифеста отсутствует в кэше мы его туда поместим снова при очередном к нему обращении.
Данный плагин восстанавливает только те потерянные ресурсы в кэше, к которым произошло обращение. Плагин можно доработать так, чтобы он проверял сразу все ресурсы и восстанавливал сразу все отсутствующие.
Собственно вот и вся история перевода сервис-воркера на Workbox.
Спасибо Яндексу за конференцию и Максу за полезный доклад! :)