Максим Сальников

@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

  1. Загрузили страницу ./index.html
  2. А там редирект 301 на ./ и Content-Type: text/plain
  3. Загрузили и закешировали содержимое ./ (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

Максим Сальников

Made with Slides.com