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

@webmaxru

Автоматизируем

сервис-воркер

с Workbox 6

Как сделать приложение быстрее и удобнее,

работая с удовольствием

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

  • Организатор Mobile-/Web-/PWA-митапов в Норвегии

  • Организатор конференций Mobile Era и ngVikings в Скандинавии

  • Спикер, тренер, автор публикаций о современном вебе

Ответственный за успех Azure-разработчиков в Microsoft

Обсудим сегодня:

  • Состояние PWA

  • Cервис-воркер это непросто

  • Типовые задачи с Workbox

  • Рецепты

  • Расширяемость

  • Полезные ресурсы

Что с PWA?

Приватность

Функциональность

Веб форкнут?

  • Стандарты

  • Прогрессивное улучшение

}

Сервис-воркер

Работа с сетью

Кеширование

  • Установка

  • Возможности

Под управлением сервис-воркеров

Страницы

0.9%

на мобильных устройствах

1%

на десктопах

Под управлением сервис-воркеров

Просмотры

18.2%

Удобно для пользователей

  • Само приложение

  • Потребляемые данные

  • Действия офлайн

  • Ошибки соединения

  • Обновления

  • Возможности платформы

  • Всегда доступно

  • Осмысленно сохраняются

  • Не пропадают

  • Не прерывают задачу

  • Явные и фоновые

  • Используются на всю катушку!

Сохраняя преимущества веба!

Шутка из 2019

В теории

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

Осталось сделать:

  1. Заполнить __WB_MANIFEST

  2. Собрать (бандлинг)

  3. Зарегистрировать в приложении

 

# Используем 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}`);
});

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: [ /* Следующий слайд */ ]
}

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()
    ],
  })
);

src/service-worker.js

Стратегии

  • CacheFirst

  • CacheOnly

  • NetworkFirst

  • NetworkOnly

  • StaleWhileRevalidate

  • ...своя стратегия?

Плагины

  • Expiration

  • CacheableResponse

  • BroadcastUpdate

  • BackgroundSync

  • ...свой плагин?

Рецепты

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);

Пишем свою стратегию

import {Strategy} from 'workbox-strategies';

class CacheNetworkRace extends Strategy {
  async _handle(request, handler /* Экз. StrategyHandler */) {
    // Запросы, обработка, возврат результатов
  }
}
  • Если нужно изменить логику запросов

  • Можно применять в registerRoute()

  • Можно использовать стандартные плагины

Методы StrategyHandler

_handle(request, handler) {
  const fetchDone = handler.fetchAndCachePut(request);
  const matchDone = handler.cacheMatch(request);

  return new Promise((resolve, reject) => {
    fetchDone.then(resolve);
    matchDone.then((response) => response && resolve(response));

    Promise.allSettled([fetchDone, matchDone]).then(/* reject */)
  });
}

Варианты использования WB

Гибкость

Автоматизация

Модули и их методы

Рецепты

Генерация сервис-воркера

через CLI

Собственные плагины

Собственные стратегии

  • Демо-приложение

  • Исходный код

  • Хостинг на Azure Static Web Apps

  • Русскоязычное сообщество PWA

  • Ресурсы на русском языке

  • Примеры в продакшн

Спасибо!

@webmaxru

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

Made with Slides.com