Maxim Salnikov
@webmaxru
What is an offline-ready web application
Developer Audience Lead at Microsoft
My App
App
Service worker
Browser/OS
Event-driven worker
Cache
fetch
push
sync
self.addEventListener('install', event => {
// Putting resources into the Cache Storage
})
self.addEventListener('activate', event => {
// Managing versions
})
self.addEventListener('fetch', event => {
// Exctracting from the cache and serving
})
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;
});
});
});
})
);
}
}
});
import { precacheAndRoute } from "workbox-precaching";
// Cache and serve resources from __WB_MANIFEST array
precacheAndRoute(self.__WB_MANIFEST);
# Using Workbox as a Node module
$ npm install workbox-build
On every app build
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}`);
});
import { precacheAndRoute } from "workbox-precaching";
precacheAndRoute([
{ revision: "866bcc582589b8920dbc", url: "index.html" },
{ revision: "c2761edff7776e1e48a3", url: "styles.css" },
{ revision: "3469613435532733abd9", url: "main.js" }
]);
import { precacheAndRoute } from "workbox-precaching";
precacheAndRoute(self.__WB_MANIFEST);
"build-pwa":
"npm run build-app &&
node build-sw.js &&
npx rollup -c"
import { Workbox, messageSW } from 'workbox-window';
if ('serviceWorker' in navigator) {
const wb = new Workbox('/sw.js');
wb.register();
// Interactive update flow with messageSW
// code sample is at https://aka.ms/workbox6
}
New version is available. Click to reload.
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
)
}
})
import { registerRoute } from "workbox-routing";
import {
CacheFirst,
NetworkFirst,
StaleWhileRevalidate,
} from "workbox-strategies";
// Avatars can always be taken from the cache
registerRoute(
new RegExp("https://www.gravatar.com/avatar/.*"),
new CacheFirst()
);
// Keeping article list always fresh
registerRoute(
({url}) => url.pathname.startsWith('/api/articles/'),
new NetworkFirst()
);
// Retrieving article from the cache and checking for updates
import { BroadcastUpdatePlugin } from 'workbox-broadcast-update';
registerRoute(
({url}) => url.pathname.startsWith('/api/article/'),
new StaleWhileRevalidate({
plugins: [
new BroadcastUpdatePlugin()
],
})
);
Maxim Salnikov
@webmaxru
No active app tab required
One-off event coming when sync is possible
To periodically synchronize data
async function postTweet(post) {
const registration = await navigator.serviceWorker.ready;
await savePost(post); // Storing data in IndexedDB
await registration.sync.register('post-tweet');
}
self.addEventListener('sync', event => {
if (event.tag == 'postTweet') {
event.waitUntil(
// Extract stored data and repeat sending
);
}
});
import {BackgroundSyncPlugin} from 'workbox-background-sync';
const postTweetPlugin =
new BackgroundSyncPlugin('tweetsQueue', {
maxRetentionTime: 24 * 60 // Max retry period
})
registerRoute(
({url}) => url.pathname.startsWith('/api/post-tweet/'),
new NetworkOnly({plugins: [postTweetPlugin]}),
'POST'
)
Maxim Salnikov
@webmaxru
Maxim Salnikov
@webmaxru