Максим Сальников
@webmaxru
Что значит готовность веб-приложения к офлайн
Ответственный за успех Azure-разработчиков в Microsoft
Браузеры
JavaScript
Браузеры
JavaScript
Движки JS
Браузеры
JavaScript
Движки JS
Интерфейсы
Браузеры
Сообщество
JavaScript
Движки JS
Интерфейсы
Браузеры
Сообщество
JavaScript
Движки JS
Интерфейсы
Браузеры
Проблемы?
(по историческим причинам)
Такие приложения запускаются везде и обладают рядом характеристик, обеспечивающих пользователей преимуществами, аналогичными тем, что доступны в нативных решениях.
* но не все возможности доступны везде**
** применяем стратегию прогрессивного улучшения
My App
Вебсайт
Сервис-воркер
Браузер/ОС
Event-driven worker
Кэш
fetch
push
sync
self.addEventListener('install', event => {
// Помещаем ресурсы app shell в Cache Storage
})
self.addEventListener('activate', event => {
// Удаляем из Cache Storage устаревшие версии
})
self.addEventListener('fetch', event => {
event.respondWith(
caches.match(event.request).then(response => {
return response || fetch(event.request)
})
)
})
# Будем использовать как модуль Node
$ npm install workbox-build --save-dev
var workboxConfig = {
globDirectory: 'dist/',
globPatterns: [
'**/*.{txt,png,ico,html,js,json,css}'
],
swSrc: 'src/workbox-service-worker.js',
swDest: 'dist/sw.js'
}
// Загрузим с Google CDN (но лучше иметь локально)
importScripts('https://googleapis.com/.../workbox-sw.js')
// Все остальное
workbox.precaching.precacheAndRoute([])
Кэширование, выдача, управление версиями
const {injectManifest} = require('workbox-build')
var workboxConfig = {...}
// Наполняем сервис-воркер информацией о ресурсах
injectManifest(workboxConfig).then(({count, size}) => {
console.log(`Generated ${workboxConfig.swDest},
which will precache ${count} files, ${size} bytes.`)
})
{
"scripts": {
"build-pwa": "npm run build-app &&
node workbox-build.js"
}
}
self.addEventListener('fetch', event => {
if (event.request.url.indexOf('/api/breakingnews') != -1) {
event.respondWith(
// Стратегия Network-First
)
} else if (event.request.url.indexOf('/api/archive') != -1 {
event.respondWith(
// Стратегия Cache-First
)
}
})
workbox.routing.registerRoute(
new RegExp('/api/breakingnews'),
new workbox.strategies.NetworkFirst()
)
workbox.routing.registerRoute(
new RegExp('/api/archive'),
new workbox.strategies.CacheFirst({
plugins: [...]
})
)
const swReg = await navigator.serviceWorker.ready
if ('sync' in swReg) {
// Сохраняем данные в IndexedDB и...
swReg.sync.register('postTweet')
}
self.addEventListener('sync', event => {
if (event.tag == 'postTweet') {
event.waitUntil(
// Извлекаем из IndexedDB и отправляем на сервер
)
}
})
const postTweetPlugin =
new workbox.backgroundSync.Plugin('tweetsQueue', {
maxRetentionTime: 24*60 // Продолжительность попыток
})
workbox.routing.registerRoute(
/(http[s]?:\/\/)?([^\/\s]+\/)post-tweet/,
new workbox.strategies.NetworkOnly({
plugins: [postTweetPlugin]
}),
'POST'
)
const swReg = await navigator.serviceWorker.ready
await swReg.backgroundFetch.fetch(
'my-series',
['s01e01.mpg', 's01e02.mpg'],
{
title: 'Первый сезон',
downloadTotal: 600 * 1024 * 1024
}
)
self.addEventListener('backgroundfetchsuccess', event => {
event.waitUntil(
(async function() {
try {
// Помещаем ресурс в Cache Storage
...
await event.updateUI({ title: `Загружено!` })
} catch (err) {
await event.updateUI({ title: `Ошибка: ${err}` })
}
})()
)
})
const swReg = await navigator.serviceWorker.ready
if ('periodicSync' in swReg) {
swReg.periodicSync.register('refreshTweets', {
// Минимальный период - 24 часа
minInterval: 24 * 60 * 60 * 1000,
})
}
self.addEventListener('periodicsync', event => {
if (event.tag === 'refreshTweets') {
event.waitUntil(
// [Может быть] Выполняем полезные действия
)
}
})
chrome://flags/#enable-devtools-experim