Максим Сальников
@webmaxru
Автоматизируем
сервис-воркер
с Workbox 6
Как сделать приложение быстрее и удобнее,
работая с удовольствием
Максим Сальников
-
Организатор Mobile-/Web-/PWA-митапов в Норвегии
-
Организатор конференций Mobile Era и ngVikings в Скандинавии
-
Спикер, тренер, автор публикаций о современном вебе
Ответственный за успех Azure-разработчиков в Microsoft


Сервис-воркер
Работа с сетью
Кеширование
-
Установка
-
Возможности
Под управлением сервис-воркеров
Страницы

0.9%
на мобильных устройствах
1%
на десктопах
Под управлением сервис-воркеров
Просмотры

18.2%
Удобно для пользователей
-
Само приложение
-
Потребляемые данные
-
Действия офлайн
-
Ошибки соединения
-
Обновления
-
Возможности платформы
-
Всегда доступно
-
Осмысленно сохраняются
-
Не пропадают
-
Не прерывают задачу
-
Явные и фоновые
-
Используются на всю катушку!
Сохраняя преимущества веба!
Шутка из 2019
Вольный перевод https://twitter.com/sanketsahu/status/1133223194888773632

В теории
self.addEventListener('install', event => { // Помещаем ресурсы в Cache Storage }) self.addEventListener('activate', event => { // Управляем версиями }) self.addEventListener('fetch', event => { // Извлекаем из кеша и отдаем })
self.addEventListener('install', event => { // Помещаем ресурсы в Cache Storage }) self.addEventListener('activate', event => { // Управляем версиями }) self.addEventListener('fetch', event => { // Извлекаем из кеша и отдаем })
self.addEventListener('install', event => { // Помещаем ресурсы в Cache Storage }) self.addEventListener('activate', event => { // Управляем версиями }) self.addEventListener('fetch', event => { // Извлекаем из кеша и отдаем })
handmade-service-worker.js
В руководстве
const PRECACHE = 'precache-v1'; const RUNTIME = 'runtime'; const PRECACHE_URLS = [ 'index.html', './', 'styles.css', '../../styles/main.css', 'demo.js' ]; self.addEventListener('install', event => { event.waitUntil( caches.open(PRECACHE) .then(cache => cache.addAll(PRECACHE_URLS)) .then(self.skipWaiting()) ); }); self.addEventListener('activate', event => { const currentCaches = [PRECACHE, RUNTIME]; event.waitUntil( caches.keys().then(cacheNames => { return cacheNames.filter(cacheName => !currentCaches.includes(cacheName)); }).then(cachesToDelete => { return Promise.all(cachesToDelete.map(cacheToDelete => { return caches.delete(cacheToDelete); })); }).then(() => self.clients.claim()) ); }); self.addEventListener('fetch', event => { if (event.request.url.startsWith(self.location.origin)) { if (event.request.url.indexOf('api/') != -1) { event.respondWith( caches.match(event.request.clone()).then((response) => { return response || fetch(event.request.clone()).then((r2) => { return caches.open(RUNTIME).then((cache) => { cache.put(event.request.url, r2.clone()); return r2.clone(); }); }); }) ); } else { event.respondWith( caches.match(event.request).then(cachedResponse => { if (cachedResponse) { return cachedResponse; } return caches.open(RUNTIME).then(cache => { return fetch(event.request).then(response => { return cache.put(event.request, response.clone()).then(() => { return response; }); }); }); }) ); } } });
handmade-service-worker.js
− Автоматизация при билде
− Точные настройки
− Расширяемость
− Умное кеширование
− Полный набор фолбэков
− Связь с приложением
− Отладочная информация
− ...
В продакшн
Редиректы?
Fallback?
Opaque response?
Версионность?
Инвалидация кеша?
Обновление спецификаций?
Размер локального хранилища?
Переменные имена ресурсов?
Feature detection?
Минимально необходимое обновление кеша?
Стратегии кеширования?
Роутинг?
Точные настройки стратегий?
Модульность?
Я вижу старую версию!!!
-
Продуманный уровень абстракций
-
Декларативность, где уместно
-
Модульность и расширяемость
-
Богатая функциональность «из коробки»
-
Мощный инструментарий
~30% сервис-воркеров — это...
Open source, активная разработка и поддержка
Настроим офлайн-доступность
import { precacheAndRoute } from "workbox-precaching"; // Закешировать и выдавать ресурсы из массива __WB_MANIFEST precacheAndRoute(self.__WB_MANIFEST);
src/service-worker.js
Осталось сделать:
-
Заполнить __WB_MANIFEST
-
Собрать (бандлинг)
-
Зарегистрировать в приложении
# Используем Workbox как модуль для Node
$ npm install workbox-build --save-dev
}
При каждом билде
Билд-скрипт
const { injectManifest } = require("workbox-build"); let workboxConfig = { swSrc: "src/service-worker.js", swDest: "dist/sw.js", globPatterns: ["index.html", "*.css", "*.js"] }; injectManifest(workboxConfig).then(() => { console.log(`Generated ${workboxConfig.swDest}`); });
const { injectManifest } = require("workbox-build"); let workboxConfig = { swSrc: "src/service-worker.js", swDest: "dist/sw.js", globPatterns: ["index.html", "*.css", "*.js"] }; injectManifest(workboxConfig).then(() => { console.log(`Generated ${workboxConfig.swDest}`); });
const { injectManifest } = require("workbox-build"); let workboxConfig = { swSrc: "src/service-worker.js", swDest: "dist/sw.js", globPatterns: ["index.html", "*.css", "*.js"] }; injectManifest(workboxConfig).then(() => { console.log(`Generated ${workboxConfig.swDest}`); });
const { injectManifest } = require("workbox-build"); let workboxConfig = { swSrc: "src/service-worker.js", swDest: "dist/sw.js", globPatterns: ["index.html", "*.css", "*.js"] }; injectManifest(workboxConfig).then(() => { console.log(`Generated ${workboxConfig.swDest}`); });
sw-build.js
[Почти] готовый сервис-воркер
import { precacheAndRoute } from "workbox-precaching"; precacheAndRoute([ { revision: "866bcc582589b8920dbc", url: "index.html" }, { revision: "c2761edff7776e1e48a3", url: "styles.css" }, { revision: "3469613435532733abd9", url: "main.js" } ]);
dist/sw.js
import { precacheAndRoute } from "workbox-precaching"; precacheAndRoute(self.__WB_MANIFEST);
src/service-worker.js
Бандлинг и минификация
import resolve from 'rollup-plugin-node-resolve' import replace from 'rollup-plugin-replace' import { terser } from 'rollup-plugin-terser' export default { input: 'dist/sw.js', output: { file: 'dist/sw.js', format: 'iife' }, plugins: [ /* Следующий слайд */ ] }
import resolve from 'rollup-plugin-node-resolve' import replace from 'rollup-plugin-replace' import { terser } from 'rollup-plugin-terser' export default { input: 'dist/sw.js', output: { file: 'dist/sw.js', format: 'iife' }, plugins: [ /* Следующий слайд */ ] }
rollup.config.js
Конфигурируем плагины
plugins: [
resolve(),
replace({
'process.env.NODE_ENV': JSON.stringify('production')
}),
terser()
]
rollup.config.js
Интегрируем в билд приложения
"build-pwa": "npm run build-app && node build-sw.js && npx rollup -c"
package.json / scripts

Регистрация в приложении
import { Workbox, messageSW } from 'workbox-window'; if ('serviceWorker' in navigator) { const wb = new Workbox('/sw.js'); wb.register(); // Интерактивный процесс обновления с messageSW // См. пример кода на https://aka.ms/workbox6 }
src/main.js
Доступна новая версия приложения. Загрузить

-
Работает офлайн (оболочка приложения)
-
Управление версиями
Динамическое кеширование
import { registerRoute } from "workbox-routing";
import {
CacheFirst,
NetworkFirst,
StaleWhileRevalidate,
} from "workbox-strategies";
src/service-worker.js
// Аватары можно брать всегда из кеша
registerRoute(
new RegExp("https://www.gravatar.com/avatar/.*"),
new CacheFirst()
);
Кеширование ответов API
// Список статей должен быть всегда свежим registerRoute( ({url}) => url.pathname.startsWith('/api/articles/'), new NetworkFirst() ); // Статью берем из кеша и проверяем на обновление import { BroadcastUpdatePlugin } from 'workbox-broadcast-update'; registerRoute( ({url}) => url.pathname.startsWith('/api/article/'), new StaleWhileRevalidate({ plugins: [ new BroadcastUpdatePlugin() ], }) );
// Список статей должен быть всегда свежим registerRoute( ({url}) => url.pathname.startsWith('/api/articles/'), new NetworkFirst() ); // Статью берем из кеша и проверяем на обновление import { BroadcastUpdatePlugin } from 'workbox-broadcast-update'; registerRoute( ({url}) => url.pathname.startsWith('/api/article/'), new StaleWhileRevalidate({ plugins: [ new BroadcastUpdatePlugin() ], }) );
src/service-worker.js
-
Единовременное событие при подключении к сети
-
Для организации переотправки запросов, сделанных офлайн
-
После закрытия вкладки с приложением и/или браузера
Фоновая синхронизация
import { BackgroundSyncPlugin } from "workbox-background-sync";
const bgSyncPlugin = new BackgroundSyncPlugin("feedbackQueue", {
maxRetentionTime: 24 * 60, // Период попыток повтора в минутах
});
registerRoute(
({url}) => url.pathname.startsWith('/feedback'),
new NetworkFirst({
plugins: [bgSyncPlugin],
}),
"POST"
);
Готовность формы к оффлайн
src/service-worker.js
Стратегии
-
CacheFirst
-
CacheOnly
-
NetworkFirst
-
NetworkOnly
-
StaleWhileRevalidate
-
...своя стратегия?
Плагины
-
Expiration
-
CacheableResponse
-
BroadcastUpdate
-
BackgroundSync
-
...свой плагин?
Своя стратегия?
-
Если нужно изменить логику запросов
-
Можно применять в registerRoute()
-
Можно использовать встроенные и собственные плагины
Свой плагин?
-
Если нужно настроить или расширить стратегию
-
Можно использовать во встроенных и собственных стратегиях
Рецепты
import { googleFontsCache, imageCache } from "workbox-recipes"; // Кешируем Google Fonts googleFontsCache({ cachePrefix: "wb6-gfonts" }); // Кешируем изображения imageCache({ maxEntries: 10 });
import { googleFontsCache, imageCache } from "workbox-recipes"; // Кешируем Google Fonts googleFontsCache({ cachePrefix: "wb6-gfonts" }); // Кешируем изображения imageCache({ maxEntries: 10 });
src/service-worker.js
...и еще однострочники для:
// Фолбэка для страниц и изображений offlineFallback(); // Кеширования страниц pageCache(); // Кеширования статических ресурсов staticResourceCache(); // Разогрева кеша warmStrategyCache(urls, strategy);
// Фолбэка для страниц и изображений offlineFallback(); // Кеширования страниц pageCache(); // Кеширования статических ресурсов staticResourceCache(); // Разогрева кеша warmStrategyCache(urls, strategy);
// Фолбэка для страниц и изображений offlineFallback(); // Кеширования страниц pageCache(); // Кеширования статических ресурсов staticResourceCache(); // Разогрева кеша warmStrategyCache(urls, strategy);
// Фолбэка для страниц и изображений offlineFallback(); // Кеширования страниц pageCache(); // Кеширования статических ресурсов staticResourceCache(); // Разогрева кеша warmStrategyCache(urls, strategy);
Варианты использования WB
Гибкость
Автоматизация
Модули и их методы
Рецепты
Генерация сервис-воркера
через CLI
Собственные плагины
Собственные стратегии
-
Демо-приложение
-
Исходный код
-
Хостинг на Azure Static Web Apps
-
Русскоязычное сообщество PWA
-
Ресурсы на русском языке
-
Примеры в продакшн
Спасибо!
@webmaxru
Максим Сальников
Автоматизируем сервис-воркер с Workbox 6
By Maxim Salnikov
Автоматизируем сервис-воркер с Workbox 6
«Задеплоил сервис-воркер — нужно покупать новый домен» — известная шутка о том, как сложно писать собственную логику кеширования. С приходом шестой версии библиотеки Workbox для прогрессивных веб-приложений (PWA) больше не нужен компромисс между гибкостью и удобством автоматизации сетевых задач. Я расскажу, как начать работу с Workbox 6, реализовать типовую функциональность для офлайнового веб-приложения и пойти дальше, добавив собственную логику кеширования.
- 3,337