Максим Сальников
@webmaxru
Сервис-воркеры:
используем накопленный опыт и смотрим в будущее
Как правильно использовать то, что уже умеет сервис-воркер
И чему он еще научится
Максим Сальников

@webmaxru
-
Google Dev Expert, Microsoft MVP
-
Организатор Mobile / Web / PWA митапов в Осло и Лондоне
-
Организатор конференций Mobile Era и ngVikings
Full-stack разработчик "приложений из будущего" в ForgeRock



Предсказуемое кеширование
Отложенные действия в оффлайне
Получение и показ уведомлений
Service Worker API
Что действительно нового?
JIT добавленный метод платежа
Полноценный оффлайн-режим
Оптимизация работы с сетью
install, activate, fetch, backgroundfetchsuccess, backgroundfetchfail, backgroundfetchclick
sync
push, notificationclick
paymentrequest
Логически
Физически
-файл(ы)
Вебсайт
Сервис-воркер
Браузер/ОС
Event-driven воркер
Похож на SharedWorker
-
Работает в собственном глобальном контексте
-
Работает в отдельном потоке
-
Не привязан к конкретной странице
-
Нет доступа к DOM
Не похож на SharedWorker
-
Может запускаться без страницы
-
Работает только под HTTPS (localhost - исключение)
-
Может быть завершен браузером в любое время
-
Имеет заданную модель жизненного цикла
Непростой жизненный цикл
'install'
Загрузка
Установка
Активация
Удален
'activate'
[Ожидание]
Активен
Сервис-воркер

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

Кросс-платформенные?
Браузеры
Настольные
Мобильные








За флагом

OS





Построим App Shell
My App
-
Определиться с минимально необходимым набором ресурсов
Доступна новая версия.
Обновить?
Сервис-воркер
-
install: поместить ресурсы в Cache Storage
-
activate: очистить Cache от ресурсов предыдущей версии приложения
-
fetch: если ресурс в Cache Storage, выдать его оттуда, иначе — загрузить из сети (и закешировать)
Build time
-
Зарегистрировать сервис-воркер и проверять, не обновился ли он
Вебсайт
Прогрессивное улучшение
Определяйте наличие функциональности
Совет #1








Поддержка: ваш браузер
PWA Feature Detector
Поддержка: точно

Web API Confluence
Регистрация
if ('serviceWorker' in navigator) {
// Регистрируем сервис-воркер
}
Фоновая синхронизация
if ('SyncManager' in window) {
// Реализуем функциональность для оффлайн-режима
}
Подписка на Push-уведомления
if (!('PushManager' in window)) {
// Прячем интерфейс подписки на push-уведомления
}
Действия в уведомлениях
if ('actions' in Notification.prototype) {
// Можем использовать кнопки с разными действиями
}




Правильный момент для регистрации
Лучше — позже (или никогда)
Совет #2
Улучшать
Не мешать
Не сломать
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/sw-workbox.js')
.then(...);
}

if ('serviceWorker' in navigator) {
window.addEventListener('load', () => {
navigator.serviceWorker.register('/sw-workbox.js')
.then(...);
)};
}

platformBrowserDynamic()
.bootstrapModule(AppModule)
.then(() => {
// Регистрация сервис-воркера
});
main.ts

Неприятная правда #1
-
Сервис-воркер не улучшит первый запуск вашего вебсайта
-
При первом запуске пользователь скачает ресурсы из набора Application Shell дважды
-
В некоторых случаях сервис-воркер не только не ускорит, но и замедлит повторные открытия
Предварительное кеширование
Знайте ваши ресурсы, следите за чистотой и не прощайте ошибок
Совет #3
Инструментарий сервис-воркера
-
Service Worker API
-
Cache API
-
IndexedDB
-
Fetch
-
Clients API
-
Broadcast Channel API
-
postMessage
-
Push API
-
Notifications API
-
Local Storage
-
Session Storage
-
XMLHttpRequest
-
DOM
const appShellFilesToCache = [
...
'./non-existing.html'
]
sw-handmade.js
self.addEventListener('install', (event) => {
event.waitUntil(
caches.open('appshell').then((cache) => {
return cache.addAll(appShellFilesToCache)
})
)
})

-
Ошибки HTTP
-
Время работы сервис-воркера
-
Ошибки хранилищ

if ('storage' in navigator && 'estimate' in navigator.storage) {
navigator.storage.estimate().then(({usage, quota}) => {
console.log(`Using ${usage} out of ${quota} bytes.`);
});
}
Chrome | <6% своб. пространства |
Firefox | <10% своб. пространства |
Safari | <50MB |
IE10 | <250MB |
Edge | Зависит от размера диска |
Объем хранилищ ограничен
const appShellFilesToCache = [
'./styles.css',
...
'./styles.css'
]
Дубликаты ресурсов в addAll()


event.waitUntil(
caches
.open('appshell').then(cache => {
return cache.addAll(['./bad-res.html'])
.catch(err => {
console.log(err)
})
})
);
Обработка ошибок

event.waitUntil(
caches
.open('appshell').then(cache => {
return cache.addAll(['./bad-res.html'])
.catch(err => {
console.log(err)
throw err
})
})
);
Обработка ошибок


Кеширование ресурсов с других адресов
Приготовьтесь
к непрозрачности
Совет #4
* = origins
*
2 варианта
-
Добавить заголовки CORS на удаленной стороне
-
Обрабатывать opaque ответы
Ограничения opaque ответов
-
Свойство status всегда равно нулю и не зависит от того, успешен запрос или нет
-
Методы Cache API add()/addAll() сработают аварийно, если статус хотя бы одного из ответов не находится в диапазоне 2XX
const appShellFilesToCache = [
...
'https://workboxjs.org/offline-ga.min.svg'
]
sw-handmade.js
self.addEventListener('install', (event) => {
event.waitUntil(
caches.open('appshell').then((cache) => {
return cache.addAll(appShellFilesToCache)
})
)
})

Решение для no-cors
fetch(event.request).then( response => {
if (response.ok) {
let copy = response.clone();
caches.open('runtime').then( cache => {
cache.put(request, copy);
});
return response;
}
})
Решение для no-cors
fetch(event.request).then( response => {
if (response.ok || response.status === 0) {
let copy = response.clone();
caches.open('runtime').then( cache => {
cache.put(request, copy);
});
return response;
}
})
Проблемы
-
Мы не знаем, что к нам пришло в качестве ответа, так что есть вероятность закешировать 404, 500 и т.д.
-
Каждый закешированный ресурс занимает как минимум 7Мб в Cache Storage
Не относитесь к App Shell формально
Помните о пользователях
Совет #5
Обновление вебсайта
v1
v2
v1
v1
v2
На сервере
В браузере
v2


Отслеживание обновления
-
На самой странице, через статус регистрации сервис-воркера
navigator.serviceWorker.register('sw-handmade.js')
.then(registration => {
if (registration.waiting) {
// Показываем приглашение обновить страницу
}
})
-
В сервис-воркере, после отправив уведомление клиентам через BroadcastChannel API или postMessage
Неприятная правда #2
-
Архитектура Application Shell идет вразрез с идеей веба про "всегда последней версии"
-
Мы можем лишь слегка улучшить пользовательский опыт
Я прогрессивное веб-приложение, и я always fresh. Только это уже устаревшая версия. Кликни здесь чтобы обновить
Сервис-воркеру нужно время на запуск
Не тратьте его впустую!
Совет #6
Проблема при "холодном" запуске
SW Boot
Запрос навигации
SW Boot
Запрос навигации
-
Если сервис-воркер выгружен из памяти
-
Если результат этого запроса не закеширован
-
Если в сервис-воркере есть событие fetch
addEventListener('activate', event => {
event.waitUntil(async function() {
// Feature-detect
if (self.registration.navigationPreload) {
// Включаем!
await self.registration.navigationPreload.enable();
}
}());
});
Предзагрузка запроса навигации
addEventListener('fetch', event => {
event.respondWith(async function() {
// Лучший вариант: отвечаем из кеша
const cachedResponse = await caches.match(event.request);
if (cachedResponse) return cachedResponse;
// OK вариант: отвечаем результатом предзагрузки
const response = await event.preloadResponse;
if (response) return response;
// Худший вариант: идем в сеть :(
return fetch(event.request);
}());
});
Используем ее результат
Правильные инструменты
Доверяй И проверяй
Совет #7
Инструменты помогают
-
Реализовывать сложные алгоритмы
-
Перенимать лучшие практики
-
Фокусироваться на ВАШЕЙ задаче
-
Следить за обновлениями спецификаций
-
Обрабатывать пограничные случаи
Фреймворки
-
sw-precache / sw-toolbox
-
Workbox
-
offline-plugin для Webpack
-
PWABuilder.com
-
create-react-app
-
preact-cli
-
polymer-cli
-
vue-cli
-
angular-cli
Генераторы
-
Lighthouse
-
Sonarwhal
Аудит
App shell
Динамическое кеширование
Оффлайн GA
Повторение неуспешных запросов
Уведомления об обновлениях
Build-интеграции
Возможность расширять функциональность собственного сервис-воркера

npm install -g sonarwhal
sonarwhal --init
sonarwhal https://airhorner.com
npm install -g lighthouse
lighthouse https://airhorner.com


Неприятная правда #3
-
Даже широко известные, хорошо поддерживаемые библиотеки с открытым кодом могут содержать ошибки
-
И даже они не всегда успевают отражать изменения в спецификациях

testingjavascript.com
- Загрузили страницу ./index.html
- А там редирект 301 на ./ и Content-Type: text/plain
- Загрузили и закешировали содержимое ./ (text/html), а Content-Type взяли от первого запроса
-
Обновите Workbox до 3.6.3
-
Сбросьте кеш явной установкой его имени
workbox.core.setCacheNameDetails({precache: 'new-name'});
Если что-то пошло не так
Нужен kill switch
Совет #8


-
Разрегистрировать сервис-воркер?
-
Разместить исправленный сервис-воркер (или no-op)
-
Удостовериться, что сервис-воркер не берется из кеша HTTP
План спасения
No-op
self.addEventListener('install', () => {
self.skipWaiting();
});
self.addEventListener('activate', () => {
self.clients.matchAll({type: 'window'}).then(tabs => {
tabs.forEach(tab => {
tab.navigate(tab.url);
});
});
});
Крайняя мера для UX
Обновление и обход кеша HTTP
Cache-Control: no-cache
Ресурсы, импортированные через importScripts()
Сервис-воркер
Спецификация обновлена
Побайтное сравнение, нужно использовать версионность
importScripts(`/sw-lib.js?v=${VERSION}`);
Контент не проверяется, нужно менять название ресурса
updateViaCache
index.html
navigator.serviceWorker.register('/sw.js', {
updateViaCache: 'none'
})
Значения: "imports", "all" или "none"
Оптимизации importScripts()
-
Заранее известные скрипты извлекаются и запускаются еще до вызова importScripts()
-
Они хранятся в виде V8 байткода
Проблема
Вызов importScripts() в произвольном месте сервис-воркера
Решение
Теперь — только до достижения состояния installed
Не только оффлайн и уведомления
Используйте весь потенциал сервис-воркера
Совет #9
Балансировщик нагрузки
-
Перехватываем запросы и выбираем определенный сервер
-
Например, наименее нагруженный или для A/B тестирования
Поддержка WebP (с WASM)
service-worker.js / событие fetch
event.respondWith(async function() {
const response = await fetch(event.request);
const buffer = await response.arrayBuffer();
const WebPDecoder = await fetchWebPDecoder();
const decoder = new WebPDecoder(buffer);
const blob = await decoder.decodeToBMP();
return new Response(blob, { headers: { "content-type": "image/bmp",
"status": 200 } });
}());
Новые возможности
Начинайте эксперименты уже сейчас!
Совет #10
Quiz time!
-
Нет, в стандарте четко сказано: same origin
-
Да, ведь есть экспериментальный Foreign Fetch
-
Да, наверное, есть какие-то варианты...
Может ли сервис-воркер быть установлен без посещения его вебсайта (с другого адреса)?
Payment handler
-
Вспомогательный инструмент Payment Request API для платежных систем
-
Регистрирует платежные инструменты (методы): платежи с карт, криптовалютные, банковские переводы и т.д.
-
Если метод зарегистрирован (в сервис-воркере) и есть в списке PaymentRequest сайта продавца, он будет показан покупателю
const swReg = await navigator.serviceWorker.register("/sw.js");
await swReg.paymentManager.instruments.set(
"My Pay's Method ID",
{
name: "My Pay's Method",
method: "https://my.pay/my-method",
}
);
self.addEventListener("paymentrequest", event => {
// Открыть окно с интерфейсом данного платежного метода
event.openWindow('https://my.pay/checkout')
});
main.js / payment app
sw.js / payment app
Just-In-Time установка
-
Метод (url) задан как supportedMethods в PaymentRequest сайта продавца и в момент оплаты этот метод выбран
-
На HEAD-запрос по этому адресу сайт платежной системы возвращает 200 + Link:<адрес Payment Method Manifest>, где в default_applications должен присутствовать Web App Manifest метода
-
В Web App Manifest метода присутствует секция serviceworker с src и scope
-
Сервис-воркер с этого адреса будет установлен!
-
Будет вызвано его событие paymentrequest
Фоновая загрузка
-
Контролируемые, предсказуемые, при необходимости возобновляемые скачивания и закачивания файлов без зависимости от состояния вебсайта
-
Все возможности для уведомления пользователя о состоянии скачивания
const registration = await navigator.serviceWorker.ready;
await registration.backgroundFetch.fetch(
'my-series',
['s01e01.mpg', 's01e02.mpg'],
{
title: 'Downloading My Series',
downloadTotal: 1000000000
}
);
index.html
const bgFetches =
await registration.backgroundFetch.getIds();
console.log(bgFetches);
addEventListener('backgroundfetchsuccess', event => {
event.waitUntil(
(async function() {
try {
// Копируем результаты в Cache Storage
...
await event.updateUI({ title: `Downloaded!` });
} catch (err) {
await event.updateUI({ title: `Fail: ${err}` });
}
})()
);
});
service-worker.js
addEventListener('backgroundfetchfail', event => {
...
});
addEventListener('backgroundfetchclick', event => {
...
});
service-worker.js

Проект Фугу
-
Writable Files API
-
WebHID API
-
Scheduled Task API
-
Web Share Target API
-
Wake Lock API
-
Cookie Store API
-
User Idle Detection API
-
...

periodicsync
-
1800+ разработчиков
-
Представители основных браузеров, библиотек, фреймворков
Совет #11
-
Все о PWA на русском языке
Спасибо!
@webmaxru
Максим Сальников
Есть вопрос?
@webmaxru
Максим Сальников
Сервис-воркеры: используем накопленный опыт и смотрим в будущее
By Maxim Salnikov
Сервис-воркеры: используем накопленный опыт и смотрим в будущее
Service Worker API — это фундамент концепта прогрессивных веб-приложений, отвечающий за возможность работы оффлайн, оптимизацию сетевых запросов, push-уведомления и массу других полезных вещей. Формально определяемый как программируемый сетевой прокси, сервис-воркер дает нам возможность реализовать целый слой логики приложения и содержит массу нюансов в своем поведении, которые и будут представлены в рамках сессии. Основываясь на накопленном сообществом опыте разработки и использования сервис-воркеров в реальных проектах, мы обсудим: - лучшие практики с примерами кода для всего жизненного цикла сервис-воркера, от регистрации до экстренного удаления; - возможные проблемы и особые случаи при работе с HTTP-запросами из сервис-воркера; - последние новости о поддержке отдельных частей спецификации разными браузерами; - рекомендуемые инструменты для автоматизации некоторых сетевых задач; - планируемые добавления в Service Worker API: новые интересные возможности.
- 4,714