Maxim Salnikov

@webmaxru

Практический мастер-класс

Сделаем веб-приложение прогрессивным за один час!

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

  • Организатор Mobile / Web / PWA митапов в Осло и Лондоне

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

  • Ответственный за поток о веб-разработке 404fest в Самаре

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

Определение PWA

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

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

работают везде*

* но не все возможности доступны везде**

нативно

** применяем стратегию прогрессивного улучшения

=

+

Application shell

Web App Manifest

Быстрые, адаптивные, mobile-first

Работают по HTTPS

Логически

Физически

-файл(ы)

Вебсайт

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

Браузер/ОС

Event-driven worker

Кэш

fetch
push
sync

Спроектируем App shell

My App

  • Определим минимальный, но достаточный набор ресурсов, помним о версионности 

  • При первой загрузке явно поместим эти ресурсы в Cache Storage

  • При последующих запусках будем пробовать выдавать эти ресурсы из кэша (иначе – сеть)

  • В то же время проверяем, не обновилась ли версия. Если да – обновляем кэш.

Время кода!

Рабочее окружение

Инструменты:

  • Git
  • Node, npm
  • Любой статический веб-сервер, например serve
  • Редактор кода

 

Браузеры (последние стабильные версии):

  • Chrome обязательно
  • Firefox / Edge / Edge Beta опционально

 

Быстрый старт

npm install serve -g
git clone https://github.com/webmaxru/pwa-for-production
cd pwa-for-production
git checkout part-1-init
npm install
serve

Итоговое приложение (можно копировать примеры кода оттуда):

Предварительное кэширование

self.addEventListener('install', event => {
  
    // Помещаем ресурсы app shell в Cache Storage

})
self.addEventListener('activate', event => {
  
    // Удаляем из Cache Storage устаревшие версии

})

handmade-service-worker.js

В реальности...

  • Необходимо следить за актуальностью списка ресурсов

  • Для некоторых видов ответов (opaque, redirected) потребуется специальная обработка

  • Пересоздание полной версии приложения в кэше при  изменении любого из ресурсов неоптимально

  • Размеры кэша ограничены — нужен контроль

  • Придется подумать о механизмах инвалидации кэша

  • ...

Перехватываем запросы

self.addEventListener('fetch', event => {

  if (event.request.url.indexOf('/api') != -1) {
    event.respondWith(
      // Реализуем стратегию Network-First (для API?)
    )
  } else {
    event.respondWith(
      // Реализуем стратегию Cache-First (для app shell?)
    )
  }
})

handmade-service-worker.js

В реальности...

  • Реализация стратегий — не самая простая задача

  • Могут потребоваться и более сложные варианты, например, Stale-While-Revalidate

  • Даже в рамках одной стратегии для разных групп ресурсов могут потребоваться разные настройки

  • В итоге придется сделать собственную реализацию роутинга

  • Необходимо предусмотреть все варианты фолбеков

  • ...

  • Оболочка приложения

  • Кеширование данных (runtime)

  • Повторение совершенных офлайн запросов

  • Двусторонняя коммуникация с приложением

  • Офлайн-аналитика (GA)

Все это в собственном сервис-воркере

Режимы работы

  • Workbox CLI

  • Плагин Webpack

  • Модуль для NodeJS

# Устанавливаем модуль Workbox для NodeJS
$ npm install workbox-build --save-dev

Build script

// We will use injectManifest mode
const {injectManifest} = require('workbox-build')

// Sample configuration with the basic options
var workboxConfig = {...}

// Calling the method and output the result
injectManifest(workboxConfig).then(({count, size}) => {
    console.log(`Generated ${workboxConfig.swDest},
    which will precache ${count} files, ${size} bytes.`)
})

workbox-build-inject.js

Build script configuration

// Sample configuration with the basic options
var workboxConfig = {
  globDirectory: 'dist/',
  globPatterns: [
    '**/*.{txt,png,ico,html,js,json,css}'
  ],
  swSrc: 'src/workbox-service-worker.js',
  swDest: 'dist/sw.js'
}

workbox-build-inject.js

Application Shell

// Importing Workbox itself from Google CDN
importScripts('https://googleapis.com/.../workbox-sw.js');

// Precaching and setting up the routing
workbox.precaching.precacheAndRoute([])

src/workbox-service-worker.js

Caching, serving, managing versions

Precaching manifest

[
  {
    "url": "index.html",
    "revision": "34c45cdf166d266929f6b532a8e3869e"
  },
  {
    "url": "favicon.ico",
    "revision": "b9aa7c338693424aae99599bec875b5f"
  },
  ...
]

Build flow integration

{
  "scripts": {
    "build-pwa": "npm run build-app &&
                  node workbox-build-inject.js"
  }
}

package.json

Application Data

Intercepting requests

self.addEventListener('fetch', event => {

  if (event.request.url.indexOf('/api/breakingnews') != -1) {
    event.respondWith(
      // Network-First Strategy
    )
  } else if (event.request.url.indexOf('/api/archive') != -1 {
    event.respondWith(
      // Cache-First Strategy
    )
  }
})

handmade-service-worker.js

Routes and strategies

workbox.routing.registerRoute(
  new RegExp('/api/breakingnews'),
  new workbox.strategies.NetworkFirst()
);

src/workbox-service-worker.js

workbox.routing.registerRoute(
  new RegExp('/api/archive'),
  new workbox.strategies.CacheFirst({
    plugins: [...]
  })
);

Strategies

  • CacheFirst

  • CacheOnly

  • NetworkFirst

  • NetworkOnly

  • StaleWhileRevalidate

Plugins

  • Expiration

  • CacheableResponse

  • BroadcastUpdate

  • BackgroundSync

  • ...your own plugin?

Coding time

Better UX for update flow

App version updates

v1

v2

v1

v1

v2

Deployed

Displayed

v2

A new version of the app is available. Click to refresh.

const updateChannel = new BroadcastChannel('app-shell');

updateChannel.addEventListener('message', event => {
    // Inform about the new version & prompt to reload
});

Option #1: BroadcastChannel

main.js

workbox.precaching.addPlugins([
    new workbox.broadcastUpdate.Plugin('app-shell')
]);

src/workbox-service-worker.js

// Feature detection
if ('serviceWorker' in navigator) {

  // Postponing the registration for better performance
  window.addEventListener('load', () => {
    navigator.serviceWorker.register('/service-worker.js');
  });

}

Option #2: Service worker lifecycle

app.js

Requirements

  • Feature detection

  • Registration after app fully loaded and UI rendered

  • Hook into service worker lifecycle update event

  • Was the service worker updated?

  • Was the app itself updated?

Workbox v4: workbox-window

import { Workbox } from 'workbox-window';

if ('serviceWorker' in navigator) {
    const wb = new Workbox('service-worker.js');

    // Event listeners...

    wb.register();
}

main.js

$ npm install workbox-window

Alternative

<script type="module">
import {Workbox} from 'https://...cdn.com/workbox-window.mjs';

if ('serviceWorker' in navigator) {
  const wb = new Workbox('service-worker.js');

  wb.register();
}
</script>

index.html

Was service worker file updated?

wb.addEventListener('installed', event => {
  if (event.isUpdate) {
  // Show "Newer version is available. Refresh?" prompt
  } else {
  // Optionally: show "The app is offline-ready" toast
  }
});

main.js

workbox.core.skipWaiting()
workbox.core.clientsClaim()

service-worker.js

Must have!

Other useful events

wb.addEventListener('activated', (event) => {
  // Service worker activated
});
wb.addEventListener('waiting', (event) => {
  // Service worker was installed but can't be activated
});

Same for the "external" service workers:

externalinstalled, externalwaiting, externalactivated

Что нового?

Фоновая загрузка

  • Приостановка / возобновление загрузки при нестабильном подключении

  • Доступ к загруженным ресурсам и статусу загрузки из приложения

  • Продолжение загрузки после закрытия приложения

  • Нативные элементы интерфейса браузера / ОС для управления и получения статуса загрузки

Подробнее и с примерами

Native File System API

Badging API

Contact Picker API

Почти 100 новых API

  • 2000+ разработчиков

  • Представители основных браузеров, библиотек, фреймворков

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

Спасибо!

@webmaxru

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

Есть вопрос?

@webmaxru

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

Thank you!

Maxim Salnikov

@webmaxru

Questions?

Maxim Salnikov

@webmaxru

Практический мастер-класс по PWA: сделаем веб-приложение прогрессивным за один час!

By Maxim Salnikov

Практический мастер-класс по PWA: сделаем веб-приложение прогрессивным за один час!

1. Определяем общую цель и конкретные задачи, что нужно, чтобы наше приложение стало PWA. 2. Устанавливаем и настраиваем наше рабочее окружение. 3. Создаем и регистрируем наш первый сервис-воркер. Изучаем возможности Dev Tools браузера касательно PWA. 4. Отправляем наше приложение в оффлайн - на практике знакомимся с библиотекой Workbox. 5. Оптимизируем работу с сетью - кешируем запросы к API, используя разные стратегии. 6. Отдыхаем от кодинга - составляем Web App Manifest, чтобы наше приложение стало устанавливаемым. 7. Проверка итоговой “прогрессивности” нашего приложения, подведение итогов. 8. Пара слов о других интересных возможностях сервис воркеров и о будущем PWA.

  • 3,473