SlideShare ist ein Scribd-Unternehmen logo
1 von 179
Downloaden Sie, um offline zu lesen
Getting started with
Aaron Gustafson
@AaronGustafson
noti.st/AaronGustafson
@AaronGustafson
Follow along
๏ Slides: https://aka.ms/btconf-pwa-slides
๏ Work files: https://aka.ms/btconf-pwa-code
๏ Final Demo: https://aka.ms/btconf-pwa-live
What exactly is a
PWA?
Progressive Web App?
What exactly is a
What exactly is a
Progressive Web App?
“Progressive Web App”
is a marketing term
Progressive Web App
Progressive Web App
Progressive Web App
Game
Gallery
Book
Newspaper
Art Project
Tool
Progressive Web App
Game
Gallery
Book
Newspaper
Art Project
Tool
Progressive Web Site
Who’s behind PWAs?
@AaronGustafson
What’s a PWA, technically?
HTTPS
@AaronGustafson
What’s a PWA, technically?
HTTPS Web App
Manifest
@AaronGustafson
What’s a PWA, technically?
HTTPS Web App
Manifest
Service
Worker
Should I believe
the hype?
Maybe?
Starbucks:
2x increase in daily
active users
aka.ms/google-io-2018
Tinder:
Core experience
with 90% less code
aka.ms/tinder-pwa-2017
Trivago:
97% increase in
click-outs to
hotel offers
aka.ms/trivago-pwa-2017
West Elm:
15% increase in
time on site
9% increase in
revenue per visit
aka.ms/west-elm-pwa-2017
Getting Started with Progressive Web Apps [Beyond Tellerrand 2019]
PWAs start with a great web
experience and then enhance
that experience for performance,
resilience, installation,
and engagement
PWAs start with a great web
experience and then enhance
that experience for performance,
resilience, installation,
and engagement
Progressive Web App
Progressive Web AppProgressive
Enhancement
Progressive
/prəˈɡresiv/
happening or developing
gradually or in stages;
proceeding step by step
@AaronGustafson
Enhance the experience
Capabilities
UserExperience
@AaronGustafson
What’s a PWA, technically?
HTTPS Web App
Manifest
Service
Worker
@AaronGustafson
Let’s talk about HTTPS
@AaronGustafson
HTTPS is simple (& free) now
๏ Many hosts include it
๏ GitHub
๏ Netlify
๏ AWS
๏ etc.
๏ LetsEnrcypt & Certbot for everything else
https://letsencrypt.org/
@AaronGustafson
What’s a PWA, technically?
HTTPS Web App
Manifest
Service
Worker
@AaronGustafson
Let’s talk about the Manifest
@AaronGustafson
Manifest files are JSON files
{
"property_a": "value",
"property_b": ["value_1", "value_2"],
"property_c": {
"nested_property": "nested_value"
}
}
@AaronGustafson
Minimum Viable Manifest
{
"lang": "en-US",
"name": "Aaron Gustafson",
"short_name": "AaronG",
"description": "The online home and work of Aaron Gustafson."
}
@AaronGustafson
Minimum Viable Manifest
{
"lang": "en-US",
"name": "Aaron Gustafson",
"short_name": "AaronG",
"description": "The online home and work of Aaron Gustafson."
}
@AaronGustafson
Minimum Viable Manifest
{
"lang": "en-US",
"dir": "ltr",
"name": "Aaron Gustafson",
"short_name": "AaronG",
"description": "The online home and work of Aaron Gustafson."
}
@AaronGustafson
Minimum Viable Manifest
{
"lang": "en-US",
"dir": "auto", // default
"name": "Aaron Gustafson",
"short_name": "AaronG",
"description": "The online home and work of Aaron Gustafson."
}
@AaronGustafson
Minimum Viable Manifest
{
"lang": "en-US",
"name": "Aaron Gustafson",
"short_name": "AaronG",
"description": "The online home and work of Aaron Gustafson."
}
@AaronGustafson
Minimum Viable Manifest
{
"lang": "en-US",
"name": "Aaron Gustafson",
"short_name": "AaronG",
"description": "The online home and work of Aaron Gustafson."
}
@AaronGustafson
Minimum Viable Manifest
{
"lang": "en-US",
"name": "Aaron Gustafson",
"short_name": "AaronG",
"description": "The online home and work of Aaron Gustafson."
}
@AaronGustafson
Minimum Viable Manifest
{
"lang": "en-US",
"name": "Aaron Gustafson",
"short_name": "AaronG",
"description": "The online home and work of Aaron Gustafson."
}
@AaronGustafson
Reference in the head
<link rel="manifest" href="/manifest.json">
@AaronGustafson
Reference in the head
<link rel="manifest" href="/manifest.json">
@AaronGustafson
Reference in the head
<link rel="manifest" href="/manifest.json">
@AaronGustafson
Let‘s make a manifest!
๏ Open a new text document in your site
and name it manifest.json
๏ Create a basic JSON object inside, including the
following Manifest members:
1. lang,
2. name,
3. short_name, and
4. description.
@AaronGustafson
Yours should look like this
{
"lang": "en-US",
"name": "Aaron Gustafson",
"short_name": "AaronG",
"description": "The online home and work of Aaron Gustafson."
}
@AaronGustafson
Let’s prep for install…
{
"lang": "en-US",
"name": "Aaron Gustafson",
"short_name": "AaronG",
"description": "The online home and work of Aaron Gustafson."
}
@AaronGustafson
Where do we begin?
{
…
"start_url": "/"
}
@AaronGustafson
Where does this apply?
{
…
"start_url": "/",
"scope": "/" // defaults to the start_url value
}
@AaronGustafson
What should it look like?
{
…
"start_url": "/",
"display": "minimal-ui"
}
@AaronGustafson
Display Modes
"display": "browser" "display": "standalone" "display": "minimal-ui""display": "fullscreen"
@AaronGustafson
Need to lock orientation?
{
…
"start_url": "/",
"display": "minimal-ui",
"orientation": "any"
}
@AaronGustafson
Orientation options
๏ “any “– no preference
๏ “natural” – the default orientation of the device
๏ “portrait”
๏ “portrait-primary”
๏ “portrait-secondary”
๏ “landscape”
๏ “landscape-primary”
๏ “landscape-secondary”
Details: https://www.w3.org/TR/screen-orientation/
@AaronGustafson
A little color…
{
…
"start_url": "/",
"display": "minimal-ui",
"orientation": "any",
"theme_color": "#27831B"
}
@AaronGustafson
A little color…
{
…
"start_url": "/",
"display": "minimal-ui",
"orientation": "any",
"theme_color": "#27831B",
"background_color": "#fffcf4"
}
@AaronGustafson
Adding icons
{
…
"icons": [
{ "src": "/i/og-logo.png",
"type": "image/png",
"sizes": "800x600" },
{ "src": "/i/notification-icon.png",
"type": "image/png",
"sizes": "256x256" },
{ "src": "/favicon.png",
"type": "image/png",
"sizes": "16x16" }
]
}
@AaronGustafson
Adding icons
{
…
"icons": [
{ "src": "/i/og-logo.png",
"type": "image/png",
"sizes": "800x600" },
{ "src": "/i/notification-icon.png",
"type": "image/png",
"sizes": "256x256" },
{ "src": "/favicon.png",
"type": "image/png",
"sizes": "16x16" }
]
}
@AaronGustafson
Anatomy of an ImageResource
{
"src": "/i/og-logo.png",
"type": "image/png",
"sizes": "800x600"
}
@AaronGustafson
Anatomy of an ImageResource
{
"src": "/i/og-logo.png",
"type": "image/png",
"sizes": "800x600"
}
@AaronGustafson
Anatomy of an ImageResource
{
"src": "/i/og-logo.png",
"type": "image/png",
"sizes": "800x600"
}
@AaronGustafson
Anatomy of an ImageResource
{
"src": "/i/og-logo.png",
"type": "image/png",
"sizes": "800x600"
}
@AaronGustafson
Anatomy of an ImageResource
{
"src": "/i/og-logo.png",
"type": "image/png",
"sizes": "800x600"
}
@AaronGustafson
Anatomy of an ImageResource
{
"src": "/i/og-logo.png",
"type": "image/png",
"sizes": "800x600",
"purpose": "badge"
}
@AaronGustafson
Anatomy of an ImageResource
{
"src": "/i/og-logo.png",
"type": "image/png",
"sizes": "800x600",
"purpose": "maskable"
}
@AaronGustafson
Let‘s improve our manifest!
๏ Add the following Manifest members:
1. start_url,
2. display,
3. theme_color,
4. background_color, and
5. icons
Suggested sizes: 48x48, 72x72, 96x96, 144x144,
192x192, and 512x512
@AaronGustafson
Enhance the experience
Capabilities
UserExperience
@AaronGustafson
Enhance the experience
Capabilities
UserExperience
How’d it go?
Any questions?
Let’s take a break.
@AaronGustafson
What’s a PWA, technically?
HTTPS Web App
Manifest
Service
Worker
@AaronGustafson
Let’s talk about Service Worker
@AaronGustafson
Registering a Service Worker
if ( "serviceWorker" in navigator ) {
navigator.serviceWorker.register( "/serviceworker.min.js" );
}
@AaronGustafson
Registering a Service Worker
if ( "serviceWorker" in navigator ) {
navigator.serviceWorker.register( "/serviceworker.min.js" );
}
@AaronGustafson
Registering a Service Worker
if ( "serviceWorker" in navigator ) {
navigator.serviceWorker.register( "/serviceworker.min.js" );
}
@AaronGustafson
Registering a Service Worker
if ( "serviceWorker" in navigator ) {
navigator.serviceWorker.register( "/serviceworker.min.js" );
}
@AaronGustafson
Registering a Service Worker
if ( "serviceWorker" in navigator ) {
navigator.serviceWorker.register( "/serviceworker.min.js" )
.then(function( registration ){
console.log( "Success!", registration.scope );
})
.catch(function( error ){
console.error( "Failure!" , error );
});
}
@AaronGustafson
Enhance the experience
Capabilities
UserExperience
@AaronGustafson
The Service Worker Lifecycle
Browser
Install Activation Ready
aka.ms/pwa-lifecycle
@AaronGustafson
Listening for these events
self.addEventListener( "install", function( event ){
console.log( "installing" );
});
@AaronGustafson
Listening for these events
self.addEventListener( "install", function( event ){
console.log( "installing" );
});
@AaronGustafson
Listening for these events
self.addEventListener( "install", function( event ){
console.log( "installing" );
});
@AaronGustafson
Listening for these events
self.addEventListener( "install", function( event ){
console.log( "installing" );
});
@AaronGustafson
Let‘s make a Service Worker!
๏ Create a new file for your service worker
๏ Register the Service Worker
navigator.serviceWorker.register( path )
๏ Log to the console from the following events:
๏ install
๏ activate
@AaronGustafson
Yours should look similar
self.addEventListener( "install", function( event ){
console.log( "installing" );
});
self.addEventListener( "activate", function( event ){
console.log( "activating" );
});
@AaronGustafson
The Service Worker Lifecycle
Browser
Install Activation Ready
aka.ms/pwa-lifecycle
@AaronGustafson
Preloading assets
self.addEventListener( "install", function( event ){
event.waitUntil(
caches.open("v1").then(function(cache) {
return cache.addAll([
"/css/main.css",
"/js/main.js"
]);
})
);
});
@AaronGustafson
Preloading assets
self.addEventListener( "install", function( event ){
event.waitUntil(
caches.open("v1").then(function(cache) {
return cache.addAll([
"/css/main.css",
"/js/main.js"
]);
})
);
});
@AaronGustafson
Preloading assets
self.addEventListener( "install", function( event ){
event.waitUntil(
caches.open("v1").then(function(cache) {
return cache.addAll([
"/css/main.css",
"/js/main.js"
]);
})
);
});
@AaronGustafson
Preloading assets
self.addEventListener( "install", function( event ){
event.waitUntil(
caches.open("v1").then(function(cache) {
return cache.addAll([
"/css/main.css",
"/js/main.js"
]);
})
);
});
@AaronGustafson
Preloading assets
self.addEventListener( "install", function( event ){
event.waitUntil(
caches.open("v1").then(function(cache) {
return cache.addAll([
"/css/main.css",
"/js/main.js"
]);
})
);
});
@AaronGustafson
Let’s refactor
self.addEventListener( "install", function( event ){
event.waitUntil(
caches.open("v1").then(function(cache) {
return cache.addAll([
"/css/main.css",
"/js/main.js"
]);
})
);
});
@AaronGustafson
Let’s refactor
const VERSION = "v1";
self.addEventListener( "install", function( event ){
event.waitUntil(
caches.open( VERSION ).then(function(cache) {
return cache.addAll([
"/css/main.css",
"/js/main.js"
]);
})
);
});
@AaronGustafson
Let‘s preload assets
๏ Leverage the install event to pre-load some assets
๏ Load your page in a browser and see that the assets
are loaded
๏ Bump your version number and reload the page
๏ What happened?
@AaronGustafson
We need to clean up
const VERSION = "v1";
// install event
self.addEventListener( "activate", event => {
// clean up stale caches
event.waitUntil(
caches.keys()
.then( keys => {
return Promise.all(
keys.filter( key => {
return ! key.startsWith( VERSION );
})
.map( key => {
return caches.delete( key );
})
);
})
);
});
@AaronGustafson
We need to clean up
const VERSION = "v1";
// install event
self.addEventListener( "activate", event => {
// clean up stale caches
event.waitUntil(
caches.keys()
.then( keys => {
return Promise.all(
keys.filter( key => {
return ! key.startsWith( VERSION );
})
.map( key => {
return caches.delete( key );
})
);
})
);
});
@AaronGustafson
We need to clean up
const VERSION = "v1";
// install event
self.addEventListener( "activate", event => {
// clean up stale caches
event.waitUntil(
caches.keys()
.then( keys => {
return Promise.all(
keys.filter( key => {
return ! key.startsWith( VERSION );
})
.map( key => {
return caches.delete( key );
})
);
})
);
});
@AaronGustafson
We need to clean up
const VERSION = "v1";
// install event
self.addEventListener( "activate", event => {
// clean up stale caches
event.waitUntil(
caches.keys()
.then( keys => {
return Promise.all(
keys.filter( key => {
return ! key.startsWith( VERSION );
})
.map( key => {
return caches.delete( key );
})
);
})
);
});
@AaronGustafson
We need to clean up
const VERSION = "v1";
// install event
self.addEventListener( "activate", event => {
// clean up stale caches
event.waitUntil(
caches.keys()
.then( keys => {
return Promise.all(
keys.filter( key => {
return ! key.startsWith( VERSION );
})
.map( key => {
return caches.delete( key );
})
);
})
);
});
@AaronGustafson
We need to clean up
const VERSION = "v1";
// install event
self.addEventListener( "activate", event => {
// clean up stale caches
event.waitUntil(
caches.keys()
.then( keys => {
return Promise.all(
keys.filter( key => {
return ! key.startsWith( VERSION );
})
.map( key => {
return caches.delete( key );
})
);
})
);
});
@AaronGustafson
Let‘s clean up
๏ Leverage the activate event to clear stale caches
๏ Load your page in a browser
๏ What happened?
@AaronGustafson
Use the new SW immediately
const VERSION = "v1";
self.addEventListener( "install", function( event ){
event.waitUntil(
caches.open( VERSION ).then(function(cache) {
return cache.addAll([
"/css/main.css",
"/js/main.js"
]);
})
);
self.skipWaiting();
});
@AaronGustafson
Take over any active clients
self.addEventListener( "activate", event => {
// clean up stale caches
event.waitUntil(
caches.keys()
.then( keys => {
return Promise.all(
keys.filter( key => {
return ! key.startsWith( VERSION );
})
.map( key => {
return caches.delete( key );
})
);
})
);
clients.claim();
});
@AaronGustafson
Look what happens
๏ Add skipWaiting() to your install event
๏ Look at the DevTools and observe how the state of
the Service Worker changes with and without this
line of code.
๏ What happened?
๏ Open your site in two tabs and add clients.claim() to
your activate event
๏ Look at the DevTools in each.
๏ What happened?
Let’s get
some lunch
@AaronGustafson
Back to Service Worker
@AaronGustafson
How requests are made
Browser
Internet
@AaronGustafson
Along comes Service Worker
Browser
Internet
@AaronGustafson
Along comes Service Worker
Browser
Internet
Cache
@AaronGustafson
Along comes Service Worker
Browser
Internet
Cache
👎🏻
@AaronGustafson
Intercepting requests
self.addEventListener( "fetch", function( event ){
console.log( "fetching" );
});
@AaronGustafson
Let‘s try it out
๏ Add a fetch event handler
๏ Load your page in a browser
๏ What happened?
๏ Instead of a string, log event.request.url
๏ Load your page in a browser
๏ What do you see?
@AaronGustafson
We can issue our own fetch
self.addEventListener( "fetch", function( event ){
event.respondWith(
fetch( event.request )
);
});
@AaronGustafson
We can issue our own fetch
self.addEventListener( "fetch", function( event ){
event.respondWith(
fetch( event.request )
);
});
What if the
request fails?
@AaronGustafson
What if the request fails?
const VERSION = "v1",
OFFLINE_PAGE = "offline.html";
@AaronGustafson
What if the request fails?
self.addEventListener( "install", function( event ){
event.waitUntil(
caches.open("v1").then(function(cache) {
return cache.addAll([
"/css/main.css",
"/js/main.js",
OFFLINE_PAGE
]);
})
);
});
@AaronGustafson
What if the request fails?
self.addEventListener( "fetch", function( event ){
if ( event.request.mode === "navigate" ) {
event.respondWith(
fetch(event.request)
.catch(error => {
console.log( "Fetch failed; returning offline page." );
return caches.match( OFFLINE_PAGE );
})
);
}
});
@AaronGustafson
What if the request fails?
self.addEventListener( "fetch", function( event ){
if ( event.request.mode === "navigate" ) {
event.respondWith(
fetch(event.request)
.catch(error => {
console.log( "Fetch failed; returning offline page." );
return caches.match( OFFLINE_PAGE );
})
);
}
});
@AaronGustafson
What if the request fails?
self.addEventListener( "fetch", function( event ){
if ( event.request.mode === "navigate" ) {
event.respondWith(
fetch(event.request)
.catch(error => {
console.log( "Fetch failed; returning offline page." );
return caches.match( OFFLINE_PAGE );
})
);
}
});
@AaronGustafson
What if the request fails?
self.addEventListener( "fetch", function( event ){
if ( event.request.mode === "navigate" ) {
event.respondWith(
fetch(event.request)
.catch(error => {
console.log( "Fetch failed; returning offline page." );
return caches.match( OFFLINE_PAGE );
})
);
}
});
@AaronGustafson
Let‘s try it out
๏ Add an offline.html page
๏ Pre-cache it during install
๏ Remember to rev VERSION
๏ Add a fetch handler for navigations, providing the
offline page as a fallback
๏ Turn off the network (once you know the SW is
running) and see what happens
@AaronGustafson
Caching strategies
๏ Network → cache → offline
๏ Cache → network → offline
๏ Cache vs network race → cache → offline
๏ etc.
@AaronGustafson
Network, then cache
if ( event.request.mode === "navigate" ) {
event.respondWith(
fetch( event.request )
.catch(error => {
return caches.match( OFFLINE_PAGE );
})
);
}
@AaronGustafson
Network, then cache
self.addEventListener( "fetch", function( event ){
let request = event.request,
url = request.url;
// all the rest of the code
}
@AaronGustafson
Network, then cache
if ( event.request.mode === "navigate" ) {
event.respondWith(
fetch( event.request )
.catch(error => {
return caches.match( OFFLINE_PAGE );
})
);
}
@AaronGustafson
Network, then cache
if ( request.mode === "navigate" ) {
event.respondWith(
fetch( request )
.catch(error => {
return caches.match( OFFLINE_PAGE );
})
);
}
@AaronGustafson
Network, then cache
if ( request.mode === "navigate" ) {
event.respondWith(
fetch( request )
.then( response => {
event.waitUntil(
caches.open( VERSION ).then( cache => {
return cache.put( request, response );
})
);
return response.clone();
})
.catch(error => {
return caches.match( OFFLINE_PAGE );
})
);
}
@AaronGustafson
Network, then cache
if ( request.mode === "navigate" ) {
event.respondWith(
fetch( request )
.then( response => {
event.waitUntil(
caches.open( VERSION ).then( cache => {
return cache.put( request, response );
})
);
return response.clone();
})
.catch(error => {
return caches.match( OFFLINE_PAGE );
})
);
}
@AaronGustafson
Network, then cache
if ( request.mode === "navigate" ) {
event.respondWith(
fetch( request )
.then( response => {
event.waitUntil(
caches.open( VERSION ).then( cache => {
return cache.put( request, response );
})
);
return response.clone();
})
.catch(error => {
return caches.match( OFFLINE_PAGE );
})
);
}
@AaronGustafson
Network, then cache
if ( request.mode === "navigate" ) {
event.respondWith(
fetch( request )
.then( response => {
event.waitUntil(
caches.open( VERSION ).then( cache => {
return cache.put( request, response );
})
);
return response.clone();
})
.catch(error => {
return caches.match( OFFLINE_PAGE );
})
);
}
@AaronGustafson
Network, then cache
.catch(error => {
return caches.match( OFFLINE_PAGE );
})
@AaronGustafson
Network, then cache
.catch(error => {
return caches.match( request ).then( cached_result => {
if ( cached_result ) {
return cached_result;
}
return caches.match( OFFLINE_PAGE );
});
})
@AaronGustafson
Network, then cache
.catch(error => {
return caches.match( request ).then( cached_result => {
if ( cached_result ) {
return cached_result;
}
return caches.match( OFFLINE_PAGE );
});
})
@AaronGustafson
Network, then cache
.catch(error => {
return caches.match( request ).then( cached_result => {
if ( cached_result ) {
return cached_result;
}
return caches.match( OFFLINE_PAGE );
});
})
@AaronGustafson
Network, then cache
if ( request.mode === "navigate" ) {
event.respondWith(
fetch( request ).then( response => {
event.waitUntil(
caches.open( VERSION ).then( cache => {
return cache.put( request, response );
})
);
return response.clone();
})
.catch(error => {
return caches.match( request ).then( cached_result => {
if ( cached_result ) { return cached_result; }
return caches.match( OFFLINE_PAGE );
});
})
);
}
@AaronGustafson
Caching strategies
✓ Network → cache → offline
๏ Cache → network → offline
๏ Cache vs network race → cache → offline
๏ etc.
@AaronGustafson
Cache first, then network
if ( /.css$/.test(url) || /.js$/.test(url) ) {
event.respondWith(
caches.match( request )
.then( cached_result => {
if ( cached_result ) { return cached_result; }
return fetch( request ).then( response => {
event.waitUntil(
caches.open( VERSION ).then( cache => {
return cache.put( request, reponse );
})
);
return response.clone();
})
.catch( new Response( "", {
status: 408,
statusText: "The server appears to be offline."
})
);
})
);
}
@AaronGustafson
Cache first, then network
if ( /.css$/.test(url) || /.js$/.test(url) ) {
event.respondWith(
caches.match( request )
.then( cached_result => {
if ( cached_result ) { return cached_result; }
return fetch( request ).then( response => {
event.waitUntil(
caches.open( VERSION ).then( cache => {
return cache.put( request, reponse );
})
);
return response.clone();
})
.catch( new Response( "", {
status: 408,
statusText: "The server appears to be offline."
})
);
})
);
}
@AaronGustafson
Cache first, then network
if ( /.css$/.test(url) || /.js$/.test(url) ) {
event.respondWith(
caches.match( request )
.then( cached_result => {
if ( cached_result ) { return cached_result; }
return fetch( request ).then( response => {
event.waitUntil(
caches.open( VERSION ).then( cache => {
return cache.put( request, reponse );
})
);
return response.clone();
})
.catch( new Response( "", {
status: 408,
statusText: "The server appears to be offline."
})
);
})
);
}
@AaronGustafson
Cache first, then network
if ( /.css$/.test(url) || /.js$/.test(url) ) {
event.respondWith(
caches.match( request )
.then( cached_result => {
if ( cached_result ) { return cached_result; }
return fetch( request ).then( response => {
event.waitUntil(
caches.open( VERSION ).then( cache => {
return cache.put( request, reponse );
})
);
return response.clone();
})
.catch( new Response( "", {
status: 408,
statusText: "The server appears to be offline."
})
);
})
);
}
@AaronGustafson
Cache first, then network
if ( /.css$/.test(url) || /.js$/.test(url) ) {
event.respondWith(
caches.match( request )
.then( cached_result => {
if ( cached_result ) { return cached_result; }
return fetch( request ).then( response => {
event.waitUntil(
caches.open( VERSION ).then( cache => {
return cache.put( request, response );
})
);
return response.clone();
})
.catch( new Response( "", {
status: 408,
statusText: "The server appears to be offline."
})
);
})
);
}
@AaronGustafson
Cache first, then network
if ( /.css$/.test(url) || /.js$/.test(url) ) {
event.respondWith(
caches.match( request )
.then( cached_result => {
if ( cached_result ) { return cached_result; }
return fetch( request ).then( response => {
event.waitUntil(
caches.open( VERSION ).then( cache => {
return cache.put( request, reponse );
})
);
return response.clone();
})
.catch( new Response( "", {
status: 408,
statusText: "The server appears to be offline."
})
);
})
);
}
Anyone see
an opportunity
for refactoring?
@AaronGustafson
We cached a fetch twice
return fetch( request ).then( response => {
event.waitUntil(
caches.open( VERSION ).then( cache => {
return cache.put( request, response );
})
);
return response.clone();
})
@AaronGustafson
Let’s make it a function
function cacheResponse( response ) {
event.waitUntil(
caches.open( VERSION ).then( cache => {
return cache.put( request, response );
})
);
return response.clone();
}
@AaronGustafson
Let’s make it a function
function cacheResponse( response, event ) {
event.waitUntil(
caches.open( VERSION ).then( cache => {
return cache.put( event.request, response );
})
);
return response.clone();
}
@AaronGustafson
And we can use it like this
return fetch( request )
.then( response => cacheResponse( response, event ) )
@AaronGustafson
Network, then cache
if ( request.mode === "navigate" ) {
event.respondWith(
fetch( request )
.then( response => cacheResponse( response, event ) )
.catch(error => {
return caches.match( request ).then( cached_result => {
if ( cached_result ) { return cached_result; }
return caches.match( OFFLINE_PAGE );
});
})
);
}
@AaronGustafson
Cache first, then network
if ( /.css$/.test(url) || /.js$/.test(url) ) {
event.respondWith(
caches.match( request )
.then( cached_result => {
if ( cached_result ) { return cached_result; }
return fetch( request )
.then( response => cacheResponse( response, event ) )
.catch( new Response( "", {
status: 408,
statusText: "The server appears to be offline."
})
);
})
);
}
@AaronGustafson
Caching strategies
✓ Network → cache → offline
✓ Cache → network → offline
๏ Cache vs network race → cache → offline
๏ etc.
@AaronGustafson
Who will win?
document.addEventListener('DOMContentLoaded', function(event) {
var networkDone = false;
var networkRequest = fetch('weather.json').then(function(response) {
return response.json();
})
.then(function(json) {
networkDone = true;
updatePage(json);
});
caches.match('weather.json').then(function(response) {
if ( ! response) throw Error('No data');
return response.json();
})
.then(function(json) {
if (!networkDone) updatePage(json);
})
.catch(function() { return networkRequest; })
.catch(function() { console.log('We have nothing.'); })
.then(hideLoading);
});
https://git.io/v56s4
@AaronGustafson
Who will win?
document.addEventListener('DOMContentLoaded', function(event) {
var networkDone = false;
var networkRequest = fetch('weather.json').then(function(response) {
return response.json();
})
.then(function(json) {
networkDone = true;
updatePage(json);
});
caches.match('weather.json').then(function(response) {
if ( ! response) throw Error('No data');
return response.json();
})
.then(function(json) {
if (!networkDone) updatePage(json);
})
.catch(function() { return networkRequest; })
.catch(function() { console.log('We have nothing.'); })
.then(hideLoading);
});
https://git.io/v56s4
@AaronGustafson
Who will win?
document.addEventListener('DOMContentLoaded', function(event) {
var networkDone = false;
var networkRequest = fetch('weather.json').then(function(response) {
return response.json();
})
.then(function(json) {
networkDone = true;
updatePage(json);
});
caches.match('weather.json').then(function(response) {
if ( ! response) throw Error('No data');
return response.json();
})
.then(function(json) {
if (!networkDone) updatePage(json);
})
.catch(function() { return networkRequest; })
.catch(function() { console.log('We have nothing.'); })
.then(hideLoading);
});
https://git.io/v56s4
@AaronGustafson
Who will win?
document.addEventListener('DOMContentLoaded', function(event) {
var networkDone = false;
var networkRequest = fetch('weather.json').then(function(response) {
return response.json();
})
.then(function(json) {
networkDone = true;
updatePage(json);
});
caches.match('weather.json').then(function(response) {
if ( ! response) throw Error('No data');
return response.json();
})
.then(function(json) {
if (!networkDone) updatePage(json);
})
.catch(function() { return networkRequest; })
.catch(function() { console.log('We have nothing.'); })
.then(hideLoading);
});
https://git.io/v56s4
@AaronGustafson
Let‘s discuss
๏ In what scenarios would these different caching
strategies be most appropriate?
๏ Are there other strategies you’d like to discuss?
๏ Do you want to add these caching strategies to
your site now?
Let’s take
another break
Could we save data?
@AaronGustafson
Look at the connection
var slow_connection = false,
save_data = false;
function testConnection() {
// only test every minute
if ( last_tested &&
Date.now() < last_tested + ( 60 * 1000 ) ) { return; }
if ( 'connection' in navigator ) {
slow_connection = ( navigator.connection.downlink < 0.5 );
save_data = navigator.connection.saveData;
last_tested = Date.now();
}
}
@AaronGustafson
Look at the connection
var slow_connection = false,
save_data = false;
function testConnection() {
// only test every minute
if ( last_tested &&
Date.now() < last_tested + ( 60 * 1000 ) ) { return; }
if ( 'connection' in navigator ) {
slow_connection = ( navigator.connection.downlink < 0.5 );
save_data = navigator.connection.saveData;
last_tested = Date.now();
}
}
@AaronGustafson
Look at the connection
var slow_connection = false,
save_data = false;
function testConnection() {
// only test every minute
if ( last_tested &&
Date.now() < last_tested + ( 60 * 1000 ) ) { return; }
if ( 'connection' in navigator ) {
slow_connection = ( navigator.connection.downlink < 0.5 );
save_data = navigator.connection.saveData;
last_tested = Date.now();
}
}
@AaronGustafson
Look at the connection
var slow_connection = false,
save_data = false;
function testConnection() {
// only test every minute
if ( last_tested &&
Date.now() < last_tested + ( 60 * 1000 ) ) { return; }
if ( 'connection' in navigator ) {
slow_connection = ( navigator.connection.downlink < 0.5 );
save_data = navigator.connection.saveData;
last_tested = Date.now();
}
}
@AaronGustafson
Look at the connection
self.addEventListener( "fetch", function( event ){
testConnection();
let request = event.request,
url = request.url;
…
});
@AaronGustafson
Use that information
else if ( request.headers.get("Accept").includes("image") ) {
event.respondWith(
caches.match( request ).then( cached_result => {
// cached first
if ( cached_result ) {
return cached_result;
}
// fallback to network
if ( ! slow_connection && ! save_data ) {
return fetch( request )
.then( response => cacheResponse( response, event ) )
// fail
.catch(
// Respond with an “offline” image
);
} else {
// Respond with a “saving data” image
}
})
);
}
@AaronGustafson
Use that information
else if ( request.headers.get("Accept").includes("image") ) {
event.respondWith(
caches.match( request ).then( cached_result => {
// cached first
if ( cached_result ) {
return cached_result;
}
// fallback to network
if ( ! slow_connection && ! save_data ) {
return fetch( request )
.then( response => cacheResponse( response, event ) )
// fail
.catch(
// Respond with an “offline” image
);
} else {
// Respond with a “saving data” image
}
})
);
}
@AaronGustafson
Use that information
else if ( request.headers.get("Accept").includes("image") ) {
event.respondWith(
caches.match( request ).then( cached_result => {
// cached first
if ( cached_result ) {
return cached_result;
}
// fallback to network
if ( ! slow_connection && ! save_data ) {
return fetch( request )
.then( response => cacheResponse( response, event ) )
// fail
.catch(
// Respond with an “offline” image
);
} else {
// Respond with a “saving data” image
}
})
);
}
@AaronGustafson
Use that information
else if ( request.headers.get("Accept").includes("image") ) {
event.respondWith(
caches.match( request ).then( cached_result => {
// cached first
if ( cached_result ) {
return cached_result;
}
// fallback to network
if ( ! slow_connection && ! save_data ) {
return fetch( request )
.then( response => cacheResponse( response, event ) )
// fail
.catch(
// Respond with an “offline” image
);
} else {
// Respond with a “saving data” image
}
})
);
}
@AaronGustafson
Use that information
else if ( request.headers.get("Accept").includes("image") ) {
event.respondWith(
caches.match( request ).then( cached_result => {
// cached first
if ( cached_result ) {
return cached_result;
}
// fallback to network
if ( ! slow_connection && ! save_data ) {
return fetch( request )
.then( response => cacheResponse( response, event ) )
// fail
.catch(
// Respond with an “offline” image
);
} else {
// Respond with a “saving data” image
}
})
);
}
@AaronGustafson
Use that information
else if ( request.headers.get("Accept").includes("image") ) {
event.respondWith(
caches.match( request ).then( cached_result => {
// cached first
if ( cached_result ) {
return cached_result;
}
// fallback to network
if ( ! slow_connection && ! save_data ) {
return fetch( request )
.then( response => cacheResponse( response, event ) )
// fail
.catch(
// Respond with an “offline” image
);
} else {
// Respond with a “saving data” image
}
})
);
}
@AaronGustafson
Dynamic images? Yes please!
const VERSION = "v2",
OFFLINE_PAGE = "offline.html",
SVG_OFFLINE = '<svg …></svg>',
SVG_SLOW = '<svg …></svg>';
@AaronGustafson
Dynamic images? Yes please!
function newImageResponse( svg ) {
return new Response( svg, {
headers: { 'Content-Type': 'image/svg+xml' }
});
}
@AaronGustafson
An SVG for your troubles
else if ( request.headers.get("Accept").includes("image") ) {
event.respondWith(
caches.match( request ).then( cached_result => {
// cached first
if ( cached_result ) {
return cached_result;
}
// fallback to network
if ( ! slow_connection && ! save_data ) {
return fetch( request )
.then( response => cacheResponse( response, event ) )
// fail
.catch(
// Respond with an “offline” image
);
} else {
// Respond with a “saving data” image
}
})
);
}
@AaronGustafson
An SVG for your troubles
else if ( request.headers.get("Accept").includes("image") ) {
event.respondWith(
caches.match( request ).then( cached_result => {
// cached first
if ( cached_result ) {
return cached_result;
}
// fallback to network
if ( ! slow_connection && ! save_data ) {
return fetch( request )
.then( response => cacheResponse( response, event ) )
// fail
.catch(
() => newImageResponse( SVG_OFFLINE )
);
} else {
return newImageResponse( SVG_SLOW );
}
})
);
}
@AaronGustafson
Let‘s discuss
๏ What other ways we could use Service Workers to
improve the user experience?
@AaronGustafson
Moar enhancements!
Capabilities
UserExperience
@AaronGustafson
Bonus content!
๏ Share Target
๏ Shortcuts (coming soon)
PWAs start with a great web
experience and then enhance
that experience for performance,
resilience, installation,
and engagement
PWAs start with a great web
experience and then enhance
that experience for performance,
resilience, installation,
and engagement
@AaronGustafson
Congrats, you made a PWA!
HTTPS Web App
Manifest
Service
Worker
Thank you!
@AaronGustafson
aaron-gustafson.com
noti.st/AaronGustafson

Weitere ähnliche Inhalte

Was ist angesagt?

Elasticsearch Query DSL - Not just for wizards...
Elasticsearch Query DSL - Not just for wizards...Elasticsearch Query DSL - Not just for wizards...
Elasticsearch Query DSL - Not just for wizards...clintongormley
 
Scaling real-time search and analytics with Elasticsearch
Scaling real-time search and analytics with ElasticsearchScaling real-time search and analytics with Elasticsearch
Scaling real-time search and analytics with Elasticsearchclintongormley
 
London seo master - feb 2020
London seo master - feb 2020London seo master - feb 2020
London seo master - feb 2020Matt Williamson
 
다양한 업무에 적합한 AWS의 스토리지 서비스 알아보기 – 김상현, AWS 솔루션즈 아키텍트:: AWS Builders Online Ser...
다양한 업무에 적합한 AWS의 스토리지 서비스 알아보기 – 김상현, AWS 솔루션즈 아키텍트:: AWS Builders Online Ser...다양한 업무에 적합한 AWS의 스토리지 서비스 알아보기 – 김상현, AWS 솔루션즈 아키텍트:: AWS Builders Online Ser...
다양한 업무에 적합한 AWS의 스토리지 서비스 알아보기 – 김상현, AWS 솔루션즈 아키텍트:: AWS Builders Online Ser...Amazon Web Services Korea
 
Clinton Gormley – Elasticsearch Query DSL – Not just for wizards…- NoSQL matt...
Clinton Gormley – Elasticsearch Query DSL – Not just for wizards…- NoSQL matt...Clinton Gormley – Elasticsearch Query DSL – Not just for wizards…- NoSQL matt...
Clinton Gormley – Elasticsearch Query DSL – Not just for wizards…- NoSQL matt...NoSQLmatters
 
Redefining technical SEO & how we should be thinking about it as an industry ...
Redefining technical SEO & how we should be thinking about it as an industry ...Redefining technical SEO & how we should be thinking about it as an industry ...
Redefining technical SEO & how we should be thinking about it as an industry ...WeLoveSEO
 
Google search secrets
Google search secretsGoogle search secrets
Google search secretsSaurabh Gupta
 
Alexis max-Creating a bot experience as good as your user experience - Alexis...
Alexis max-Creating a bot experience as good as your user experience - Alexis...Alexis max-Creating a bot experience as good as your user experience - Alexis...
Alexis max-Creating a bot experience as good as your user experience - Alexis...WeLoveSEO
 
Enfuse_2016_Internet_of Things_Rajewski
Enfuse_2016_Internet_of Things_RajewskiEnfuse_2016_Internet_of Things_Rajewski
Enfuse_2016_Internet_of Things_RajewskiMary Braden Murphy
 
ReadingSEO - Technical SEO at Scale
ReadingSEO - Technical SEO at ScaleReadingSEO - Technical SEO at Scale
ReadingSEO - Technical SEO at ScaleHayden Roche
 
AWS 서비스로 웹 애플리케이션 만들기 – 김주영, AWS 솔루션즈 아키텍트:: AWS Builders Online Series
AWS 서비스로 웹 애플리케이션 만들기 – 김주영, AWS 솔루션즈 아키텍트:: AWS Builders Online Series AWS 서비스로 웹 애플리케이션 만들기 – 김주영, AWS 솔루션즈 아키텍트:: AWS Builders Online Series
AWS 서비스로 웹 애플리케이션 만들기 – 김주영, AWS 솔루션즈 아키텍트:: AWS Builders Online Series Amazon Web Services Korea
 
AWS Migration - General
AWS Migration - GeneralAWS Migration - General
AWS Migration - GeneralKenji Morooka
 
AWS 클라우드 환경에서의 VDI 환경 및 콜 센터 구축하기 – 조상만, AWS 솔루션즈 아키텍트:: AWS Builders Online...
 AWS 클라우드 환경에서의 VDI 환경 및 콜 센터 구축하기 – 조상만, AWS 솔루션즈 아키텍트:: AWS Builders Online... AWS 클라우드 환경에서의 VDI 환경 및 콜 센터 구축하기 – 조상만, AWS 솔루션즈 아키텍트:: AWS Builders Online...
AWS 클라우드 환경에서의 VDI 환경 및 콜 센터 구축하기 – 조상만, AWS 솔루션즈 아키텍트:: AWS Builders Online...Amazon Web Services Korea
 
스타트업에서 데이터 분석 시작하기 – 박진우, AWS 솔루션즈 아키텍트:: AWS Builders Online Series
스타트업에서 데이터 분석 시작하기 – 박진우, AWS 솔루션즈 아키텍트:: AWS Builders Online Series 스타트업에서 데이터 분석 시작하기 – 박진우, AWS 솔루션즈 아키텍트:: AWS Builders Online Series
스타트업에서 데이터 분석 시작하기 – 박진우, AWS 솔루션즈 아키텍트:: AWS Builders Online Series Amazon Web Services Korea
 
Love Can't Wait! Optimizing PageLoad Time of SPAs at Zoosk [FutureStack16]
Love Can't Wait!  Optimizing PageLoad Time of SPAs at Zoosk [FutureStack16]Love Can't Wait!  Optimizing PageLoad Time of SPAs at Zoosk [FutureStack16]
Love Can't Wait! Optimizing PageLoad Time of SPAs at Zoosk [FutureStack16]New Relic
 
ゲーム開発で活用するAWSの機械学習サービスの紹介
ゲーム開発で活用するAWSの機械学習サービスの紹介ゲーム開発で活用するAWSの機械学習サービスの紹介
ゲーム開発で活用するAWSの機械学習サービスの紹介Amazon Web Services Japan
 

Was ist angesagt? (19)

Elasticsearch Query DSL - Not just for wizards...
Elasticsearch Query DSL - Not just for wizards...Elasticsearch Query DSL - Not just for wizards...
Elasticsearch Query DSL - Not just for wizards...
 
Scaling real-time search and analytics with Elasticsearch
Scaling real-time search and analytics with ElasticsearchScaling real-time search and analytics with Elasticsearch
Scaling real-time search and analytics with Elasticsearch
 
London seo master - feb 2020
London seo master - feb 2020London seo master - feb 2020
London seo master - feb 2020
 
다양한 업무에 적합한 AWS의 스토리지 서비스 알아보기 – 김상현, AWS 솔루션즈 아키텍트:: AWS Builders Online Ser...
다양한 업무에 적합한 AWS의 스토리지 서비스 알아보기 – 김상현, AWS 솔루션즈 아키텍트:: AWS Builders Online Ser...다양한 업무에 적합한 AWS의 스토리지 서비스 알아보기 – 김상현, AWS 솔루션즈 아키텍트:: AWS Builders Online Ser...
다양한 업무에 적합한 AWS의 스토리지 서비스 알아보기 – 김상현, AWS 솔루션즈 아키텍트:: AWS Builders Online Ser...
 
Clinton Gormley – Elasticsearch Query DSL – Not just for wizards…- NoSQL matt...
Clinton Gormley – Elasticsearch Query DSL – Not just for wizards…- NoSQL matt...Clinton Gormley – Elasticsearch Query DSL – Not just for wizards…- NoSQL matt...
Clinton Gormley – Elasticsearch Query DSL – Not just for wizards…- NoSQL matt...
 
Redefining technical SEO & how we should be thinking about it as an industry ...
Redefining technical SEO & how we should be thinking about it as an industry ...Redefining technical SEO & how we should be thinking about it as an industry ...
Redefining technical SEO & how we should be thinking about it as an industry ...
 
Google search secrets
Google search secretsGoogle search secrets
Google search secrets
 
Alexis max-Creating a bot experience as good as your user experience - Alexis...
Alexis max-Creating a bot experience as good as your user experience - Alexis...Alexis max-Creating a bot experience as good as your user experience - Alexis...
Alexis max-Creating a bot experience as good as your user experience - Alexis...
 
Enfuse_2016_Internet_of Things_Rajewski
Enfuse_2016_Internet_of Things_RajewskiEnfuse_2016_Internet_of Things_Rajewski
Enfuse_2016_Internet_of Things_Rajewski
 
AWS re:Invent 2020 IoT Update - 20201223
AWS re:Invent 2020 IoT Update - 20201223AWS re:Invent 2020 IoT Update - 20201223
AWS re:Invent 2020 IoT Update - 20201223
 
Owasp austin
Owasp austinOwasp austin
Owasp austin
 
Searching 101
Searching 101Searching 101
Searching 101
 
ReadingSEO - Technical SEO at Scale
ReadingSEO - Technical SEO at ScaleReadingSEO - Technical SEO at Scale
ReadingSEO - Technical SEO at Scale
 
AWS 서비스로 웹 애플리케이션 만들기 – 김주영, AWS 솔루션즈 아키텍트:: AWS Builders Online Series
AWS 서비스로 웹 애플리케이션 만들기 – 김주영, AWS 솔루션즈 아키텍트:: AWS Builders Online Series AWS 서비스로 웹 애플리케이션 만들기 – 김주영, AWS 솔루션즈 아키텍트:: AWS Builders Online Series
AWS 서비스로 웹 애플리케이션 만들기 – 김주영, AWS 솔루션즈 아키텍트:: AWS Builders Online Series
 
AWS Migration - General
AWS Migration - GeneralAWS Migration - General
AWS Migration - General
 
AWS 클라우드 환경에서의 VDI 환경 및 콜 센터 구축하기 – 조상만, AWS 솔루션즈 아키텍트:: AWS Builders Online...
 AWS 클라우드 환경에서의 VDI 환경 및 콜 센터 구축하기 – 조상만, AWS 솔루션즈 아키텍트:: AWS Builders Online... AWS 클라우드 환경에서의 VDI 환경 및 콜 센터 구축하기 – 조상만, AWS 솔루션즈 아키텍트:: AWS Builders Online...
AWS 클라우드 환경에서의 VDI 환경 및 콜 센터 구축하기 – 조상만, AWS 솔루션즈 아키텍트:: AWS Builders Online...
 
스타트업에서 데이터 분석 시작하기 – 박진우, AWS 솔루션즈 아키텍트:: AWS Builders Online Series
스타트업에서 데이터 분석 시작하기 – 박진우, AWS 솔루션즈 아키텍트:: AWS Builders Online Series 스타트업에서 데이터 분석 시작하기 – 박진우, AWS 솔루션즈 아키텍트:: AWS Builders Online Series
스타트업에서 데이터 분석 시작하기 – 박진우, AWS 솔루션즈 아키텍트:: AWS Builders Online Series
 
Love Can't Wait! Optimizing PageLoad Time of SPAs at Zoosk [FutureStack16]
Love Can't Wait!  Optimizing PageLoad Time of SPAs at Zoosk [FutureStack16]Love Can't Wait!  Optimizing PageLoad Time of SPAs at Zoosk [FutureStack16]
Love Can't Wait! Optimizing PageLoad Time of SPAs at Zoosk [FutureStack16]
 
ゲーム開発で活用するAWSの機械学習サービスの紹介
ゲーム開発で活用するAWSの機械学習サービスの紹介ゲーム開発で活用するAWSの機械学習サービスの紹介
ゲーム開発で活用するAWSの機械学習サービスの紹介
 

Ähnlich wie Getting Started with Progressive Web Apps [Beyond Tellerrand 2019]

Media in the Age of PWAs [ImageCon 2019]
Media in the Age of PWAs [ImageCon 2019]Media in the Age of PWAs [ImageCon 2019]
Media in the Age of PWAs [ImageCon 2019]Aaron Gustafson
 
Effectively Testing Services on Rails - Railsconf 2014
Effectively Testing Services on Rails - Railsconf 2014Effectively Testing Services on Rails - Railsconf 2014
Effectively Testing Services on Rails - Railsconf 2014neal_kemp
 
Designing the Conversation [Beyond Tellerrand 2019]
Designing the Conversation [Beyond Tellerrand 2019]Designing the Conversation [Beyond Tellerrand 2019]
Designing the Conversation [Beyond Tellerrand 2019]Aaron Gustafson
 
ゼロから始める
セマンティックSEO
&
構造化データ at CSS Nite LP36
ゼロから始める
セマンティックSEO
&
構造化データ at CSS Nite LP36ゼロから始める
セマンティックSEO
&
構造化データ at CSS Nite LP36
ゼロから始める
セマンティックSEO
&
構造化データ at CSS Nite LP36Kenichi Suzuki
 
Interactive Visualization With Bokeh (SF Python Meetup)
Interactive Visualization With Bokeh (SF Python Meetup)Interactive Visualization With Bokeh (SF Python Meetup)
Interactive Visualization With Bokeh (SF Python Meetup)Peter Wang
 
Designing the Conversation [Concatenate 2018]
Designing the Conversation [Concatenate 2018]Designing the Conversation [Concatenate 2018]
Designing the Conversation [Concatenate 2018]Aaron Gustafson
 
Advanced Structured Data: Beyond Rich Snippets
Advanced Structured Data: Beyond Rich SnippetsAdvanced Structured Data: Beyond Rich Snippets
Advanced Structured Data: Beyond Rich SnippetsJustin Briggs
 
Conversational Semantics for the Web [CascadiaJS 2018]
Conversational Semantics for the Web [CascadiaJS 2018]Conversational Semantics for the Web [CascadiaJS 2018]
Conversational Semantics for the Web [CascadiaJS 2018]Aaron Gustafson
 
Modelagem de Dados Semiestruturados com ISIS-DM
Modelagem de Dados Semiestruturados com ISIS-DMModelagem de Dados Semiestruturados com ISIS-DM
Modelagem de Dados Semiestruturados com ISIS-DMGustavo Fonseca
 
SMX West 2020 - Leveraging Structured Data for Maximum Effect
SMX West  2020 - Leveraging Structured Data for Maximum EffectSMX West  2020 - Leveraging Structured Data for Maximum Effect
SMX West 2020 - Leveraging Structured Data for Maximum EffectAbby Hamilton
 
Sphinx 1.1 i18n 機能紹介
Sphinx 1.1 i18n 機能紹介Sphinx 1.1 i18n 機能紹介
Sphinx 1.1 i18n 機能紹介Ian Lewis
 
Smart mobile storytelling christy robinson - denver news train - april 11-1...
Smart mobile storytelling   christy robinson - denver news train - april 11-1...Smart mobile storytelling   christy robinson - denver news train - april 11-1...
Smart mobile storytelling christy robinson - denver news train - april 11-1...News Leaders Association's NewsTrain
 
Copy & Pest - A case-study on the clipboard, blind trust and invisible cross-...
Copy & Pest - A case-study on the clipboard, blind trust and invisible cross-...Copy & Pest - A case-study on the clipboard, blind trust and invisible cross-...
Copy & Pest - A case-study on the clipboard, blind trust and invisible cross-...Mario Heiderich
 
Embracing Capybara
Embracing CapybaraEmbracing Capybara
Embracing CapybaraTim Moore
 
AWS re:Invent 2016: State of the Union: Amazon Alexa and Recent Advances in C...
AWS re:Invent 2016: State of the Union: Amazon Alexa and Recent Advances in C...AWS re:Invent 2016: State of the Union: Amazon Alexa and Recent Advances in C...
AWS re:Invent 2016: State of the Union: Amazon Alexa and Recent Advances in C...Amazon Web Services
 
GraphQL & Relay - 串起前後端世界的橋樑
GraphQL & Relay - 串起前後端世界的橋樑GraphQL & Relay - 串起前後端世界的橋樑
GraphQL & Relay - 串起前後端世界的橋樑Pokai Chang
 
Linked Data in Use: Schema.org, JSON-LD and hypermedia APIs - Front in Bahia...
Linked Data in Use: Schema.org, JSON-LD and hypermedia APIs  - Front in Bahia...Linked Data in Use: Schema.org, JSON-LD and hypermedia APIs  - Front in Bahia...
Linked Data in Use: Schema.org, JSON-LD and hypermedia APIs - Front in Bahia...Ícaro Medeiros
 
Activity streams lightning talk, DjangoCon 2011 Day 3
Activity streams lightning talk, DjangoCon 2011 Day 3Activity streams lightning talk, DjangoCon 2011 Day 3
Activity streams lightning talk, DjangoCon 2011 Day 3Steve Ivy
 

Ähnlich wie Getting Started with Progressive Web Apps [Beyond Tellerrand 2019] (20)

Media in the Age of PWAs [ImageCon 2019]
Media in the Age of PWAs [ImageCon 2019]Media in the Age of PWAs [ImageCon 2019]
Media in the Age of PWAs [ImageCon 2019]
 
Effectively Testing Services on Rails - Railsconf 2014
Effectively Testing Services on Rails - Railsconf 2014Effectively Testing Services on Rails - Railsconf 2014
Effectively Testing Services on Rails - Railsconf 2014
 
Designing the Conversation [Beyond Tellerrand 2019]
Designing the Conversation [Beyond Tellerrand 2019]Designing the Conversation [Beyond Tellerrand 2019]
Designing the Conversation [Beyond Tellerrand 2019]
 
ゼロから始める
セマンティックSEO
&
構造化データ at CSS Nite LP36
ゼロから始める
セマンティックSEO
&
構造化データ at CSS Nite LP36ゼロから始める
セマンティックSEO
&
構造化データ at CSS Nite LP36
ゼロから始める
セマンティックSEO
&
構造化データ at CSS Nite LP36
 
Interactive Visualization With Bokeh (SF Python Meetup)
Interactive Visualization With Bokeh (SF Python Meetup)Interactive Visualization With Bokeh (SF Python Meetup)
Interactive Visualization With Bokeh (SF Python Meetup)
 
Designing the Conversation [Concatenate 2018]
Designing the Conversation [Concatenate 2018]Designing the Conversation [Concatenate 2018]
Designing the Conversation [Concatenate 2018]
 
Advanced Structured Data: Beyond Rich Snippets
Advanced Structured Data: Beyond Rich SnippetsAdvanced Structured Data: Beyond Rich Snippets
Advanced Structured Data: Beyond Rich Snippets
 
Conversational Semantics for the Web [CascadiaJS 2018]
Conversational Semantics for the Web [CascadiaJS 2018]Conversational Semantics for the Web [CascadiaJS 2018]
Conversational Semantics for the Web [CascadiaJS 2018]
 
Modelagem de Dados Semiestruturados com ISIS-DM
Modelagem de Dados Semiestruturados com ISIS-DMModelagem de Dados Semiestruturados com ISIS-DM
Modelagem de Dados Semiestruturados com ISIS-DM
 
SMX West 2020 - Leveraging Structured Data for Maximum Effect
SMX West  2020 - Leveraging Structured Data for Maximum EffectSMX West  2020 - Leveraging Structured Data for Maximum Effect
SMX West 2020 - Leveraging Structured Data for Maximum Effect
 
Sphinx 1.1 i18n 機能紹介
Sphinx 1.1 i18n 機能紹介Sphinx 1.1 i18n 機能紹介
Sphinx 1.1 i18n 機能紹介
 
Smart mobile storytelling christy robinson - denver news train - april 11-1...
Smart mobile storytelling   christy robinson - denver news train - april 11-1...Smart mobile storytelling   christy robinson - denver news train - april 11-1...
Smart mobile storytelling christy robinson - denver news train - april 11-1...
 
Copy & Pest - A case-study on the clipboard, blind trust and invisible cross-...
Copy & Pest - A case-study on the clipboard, blind trust and invisible cross-...Copy & Pest - A case-study on the clipboard, blind trust and invisible cross-...
Copy & Pest - A case-study on the clipboard, blind trust and invisible cross-...
 
Embracing Capybara
Embracing CapybaraEmbracing Capybara
Embracing Capybara
 
Embedding Linked Data Invisibly into Web Pages: Strategies and Workflows for ...
Embedding Linked Data Invisibly into Web Pages: Strategies and Workflows for ...Embedding Linked Data Invisibly into Web Pages: Strategies and Workflows for ...
Embedding Linked Data Invisibly into Web Pages: Strategies and Workflows for ...
 
AWS re:Invent 2016: State of the Union: Amazon Alexa and Recent Advances in C...
AWS re:Invent 2016: State of the Union: Amazon Alexa and Recent Advances in C...AWS re:Invent 2016: State of the Union: Amazon Alexa and Recent Advances in C...
AWS re:Invent 2016: State of the Union: Amazon Alexa and Recent Advances in C...
 
GraphQL & Relay - 串起前後端世界的橋樑
GraphQL & Relay - 串起前後端世界的橋樑GraphQL & Relay - 串起前後端世界的橋樑
GraphQL & Relay - 串起前後端世界的橋樑
 
Linked Data in Use: Schema.org, JSON-LD and hypermedia APIs - Front in Bahia...
Linked Data in Use: Schema.org, JSON-LD and hypermedia APIs  - Front in Bahia...Linked Data in Use: Schema.org, JSON-LD and hypermedia APIs  - Front in Bahia...
Linked Data in Use: Schema.org, JSON-LD and hypermedia APIs - Front in Bahia...
 
Reification
ReificationReification
Reification
 
Activity streams lightning talk, DjangoCon 2011 Day 3
Activity streams lightning talk, DjangoCon 2011 Day 3Activity streams lightning talk, DjangoCon 2011 Day 3
Activity streams lightning talk, DjangoCon 2011 Day 3
 

Mehr von Aaron Gustafson

Delivering Critical Information and Services [JavaScript & Friends 2021]
Delivering Critical Information and Services [JavaScript & Friends 2021]Delivering Critical Information and Services [JavaScript & Friends 2021]
Delivering Critical Information and Services [JavaScript & Friends 2021]Aaron Gustafson
 
Adapting to Reality [Guest Lecture, March 2021]
Adapting to Reality [Guest Lecture, March 2021]Adapting to Reality [Guest Lecture, March 2021]
Adapting to Reality [Guest Lecture, March 2021]Aaron Gustafson
 
Progressive Web Apps: Where Do I Begin?
Progressive Web Apps: Where Do I Begin?Progressive Web Apps: Where Do I Begin?
Progressive Web Apps: Where Do I Begin?Aaron Gustafson
 
Adapting to Reality [Starbucks Lunch & Learn]
Adapting to Reality [Starbucks Lunch & Learn]Adapting to Reality [Starbucks Lunch & Learn]
Adapting to Reality [Starbucks Lunch & Learn]Aaron Gustafson
 
Better Performance === Greater Accessibility [Inclusive Design 24 2018]
Better Performance === Greater Accessibility [Inclusive Design 24 2018]Better Performance === Greater Accessibility [Inclusive Design 24 2018]
Better Performance === Greater Accessibility [Inclusive Design 24 2018]Aaron Gustafson
 
PWA: Where Do I Begin? [Microsoft Ignite 2018]
PWA: Where Do I Begin? [Microsoft Ignite 2018]PWA: Where Do I Begin? [Microsoft Ignite 2018]
PWA: Where Do I Begin? [Microsoft Ignite 2018]Aaron Gustafson
 
Designing the Conversation [Accessibility DC 2018]
Designing the Conversation [Accessibility DC 2018]Designing the Conversation [Accessibility DC 2018]
Designing the Conversation [Accessibility DC 2018]Aaron Gustafson
 
Performance as User Experience [AEADC 2018]
Performance as User Experience [AEADC 2018]Performance as User Experience [AEADC 2018]
Performance as User Experience [AEADC 2018]Aaron Gustafson
 
The Web Should Just Work for Everyone
The Web Should Just Work for EveryoneThe Web Should Just Work for Everyone
The Web Should Just Work for EveryoneAaron Gustafson
 
Performance as User Experience [AEA SEA 2018]
Performance as User Experience [AEA SEA 2018]Performance as User Experience [AEA SEA 2018]
Performance as User Experience [AEA SEA 2018]Aaron Gustafson
 
Performance as User Experience [An Event Apart Denver 2017]
Performance as User Experience [An Event Apart Denver 2017]Performance as User Experience [An Event Apart Denver 2017]
Performance as User Experience [An Event Apart Denver 2017]Aaron Gustafson
 
Advanced Design Methods 1, Day 2
Advanced Design Methods 1, Day 2Advanced Design Methods 1, Day 2
Advanced Design Methods 1, Day 2Aaron Gustafson
 
Advanced Design Methods 1, Day 1
Advanced Design Methods 1, Day 1Advanced Design Methods 1, Day 1
Advanced Design Methods 1, Day 1Aaron Gustafson
 
Designing the Conversation [Paris Web 2017]
Designing the Conversation [Paris Web 2017]Designing the Conversation [Paris Web 2017]
Designing the Conversation [Paris Web 2017]Aaron Gustafson
 
Exploring Adaptive Interfaces [Generate 2017]
Exploring Adaptive Interfaces [Generate 2017]Exploring Adaptive Interfaces [Generate 2017]
Exploring Adaptive Interfaces [Generate 2017]Aaron Gustafson
 
Progressive Web Apps and the Windows Ecosystem [Build 2017]
Progressive Web Apps and the Windows Ecosystem [Build 2017]Progressive Web Apps and the Windows Ecosystem [Build 2017]
Progressive Web Apps and the Windows Ecosystem [Build 2017]Aaron Gustafson
 
Writing for Engagement [TechReady 22]
Writing for Engagement [TechReady 22]Writing for Engagement [TechReady 22]
Writing for Engagement [TechReady 22]Aaron Gustafson
 
Designing the Conversation [SmashingConf 2016]
Designing the Conversation [SmashingConf 2016]Designing the Conversation [SmashingConf 2016]
Designing the Conversation [SmashingConf 2016]Aaron Gustafson
 
The Features of Highly Effective Forms [SmashingConf NYC 2016]
The Features of Highly Effective Forms [SmashingConf NYC 2016]The Features of Highly Effective Forms [SmashingConf NYC 2016]
The Features of Highly Effective Forms [SmashingConf NYC 2016]Aaron Gustafson
 
Designing the Conversation [SpeechTek 2016]
Designing the Conversation [SpeechTek 2016]Designing the Conversation [SpeechTek 2016]
Designing the Conversation [SpeechTek 2016]Aaron Gustafson
 

Mehr von Aaron Gustafson (20)

Delivering Critical Information and Services [JavaScript & Friends 2021]
Delivering Critical Information and Services [JavaScript & Friends 2021]Delivering Critical Information and Services [JavaScript & Friends 2021]
Delivering Critical Information and Services [JavaScript & Friends 2021]
 
Adapting to Reality [Guest Lecture, March 2021]
Adapting to Reality [Guest Lecture, March 2021]Adapting to Reality [Guest Lecture, March 2021]
Adapting to Reality [Guest Lecture, March 2021]
 
Progressive Web Apps: Where Do I Begin?
Progressive Web Apps: Where Do I Begin?Progressive Web Apps: Where Do I Begin?
Progressive Web Apps: Where Do I Begin?
 
Adapting to Reality [Starbucks Lunch & Learn]
Adapting to Reality [Starbucks Lunch & Learn]Adapting to Reality [Starbucks Lunch & Learn]
Adapting to Reality [Starbucks Lunch & Learn]
 
Better Performance === Greater Accessibility [Inclusive Design 24 2018]
Better Performance === Greater Accessibility [Inclusive Design 24 2018]Better Performance === Greater Accessibility [Inclusive Design 24 2018]
Better Performance === Greater Accessibility [Inclusive Design 24 2018]
 
PWA: Where Do I Begin? [Microsoft Ignite 2018]
PWA: Where Do I Begin? [Microsoft Ignite 2018]PWA: Where Do I Begin? [Microsoft Ignite 2018]
PWA: Where Do I Begin? [Microsoft Ignite 2018]
 
Designing the Conversation [Accessibility DC 2018]
Designing the Conversation [Accessibility DC 2018]Designing the Conversation [Accessibility DC 2018]
Designing the Conversation [Accessibility DC 2018]
 
Performance as User Experience [AEADC 2018]
Performance as User Experience [AEADC 2018]Performance as User Experience [AEADC 2018]
Performance as User Experience [AEADC 2018]
 
The Web Should Just Work for Everyone
The Web Should Just Work for EveryoneThe Web Should Just Work for Everyone
The Web Should Just Work for Everyone
 
Performance as User Experience [AEA SEA 2018]
Performance as User Experience [AEA SEA 2018]Performance as User Experience [AEA SEA 2018]
Performance as User Experience [AEA SEA 2018]
 
Performance as User Experience [An Event Apart Denver 2017]
Performance as User Experience [An Event Apart Denver 2017]Performance as User Experience [An Event Apart Denver 2017]
Performance as User Experience [An Event Apart Denver 2017]
 
Advanced Design Methods 1, Day 2
Advanced Design Methods 1, Day 2Advanced Design Methods 1, Day 2
Advanced Design Methods 1, Day 2
 
Advanced Design Methods 1, Day 1
Advanced Design Methods 1, Day 1Advanced Design Methods 1, Day 1
Advanced Design Methods 1, Day 1
 
Designing the Conversation [Paris Web 2017]
Designing the Conversation [Paris Web 2017]Designing the Conversation [Paris Web 2017]
Designing the Conversation [Paris Web 2017]
 
Exploring Adaptive Interfaces [Generate 2017]
Exploring Adaptive Interfaces [Generate 2017]Exploring Adaptive Interfaces [Generate 2017]
Exploring Adaptive Interfaces [Generate 2017]
 
Progressive Web Apps and the Windows Ecosystem [Build 2017]
Progressive Web Apps and the Windows Ecosystem [Build 2017]Progressive Web Apps and the Windows Ecosystem [Build 2017]
Progressive Web Apps and the Windows Ecosystem [Build 2017]
 
Writing for Engagement [TechReady 22]
Writing for Engagement [TechReady 22]Writing for Engagement [TechReady 22]
Writing for Engagement [TechReady 22]
 
Designing the Conversation [SmashingConf 2016]
Designing the Conversation [SmashingConf 2016]Designing the Conversation [SmashingConf 2016]
Designing the Conversation [SmashingConf 2016]
 
The Features of Highly Effective Forms [SmashingConf NYC 2016]
The Features of Highly Effective Forms [SmashingConf NYC 2016]The Features of Highly Effective Forms [SmashingConf NYC 2016]
The Features of Highly Effective Forms [SmashingConf NYC 2016]
 
Designing the Conversation [SpeechTek 2016]
Designing the Conversation [SpeechTek 2016]Designing the Conversation [SpeechTek 2016]
Designing the Conversation [SpeechTek 2016]
 

Kürzlich hochgeladen

KubeConEU24-Monitoring Kubernetes and Cloud Spend with OpenCost
KubeConEU24-Monitoring Kubernetes and Cloud Spend with OpenCostKubeConEU24-Monitoring Kubernetes and Cloud Spend with OpenCost
KubeConEU24-Monitoring Kubernetes and Cloud Spend with OpenCostMatt Ray
 
Building Your Own AI Instance (TBLC AI )
Building Your Own AI Instance (TBLC AI )Building Your Own AI Instance (TBLC AI )
Building Your Own AI Instance (TBLC AI )Brian Pichman
 
Using IESVE for Loads, Sizing and Heat Pump Modeling to Achieve Decarbonization
Using IESVE for Loads, Sizing and Heat Pump Modeling to Achieve DecarbonizationUsing IESVE for Loads, Sizing and Heat Pump Modeling to Achieve Decarbonization
Using IESVE for Loads, Sizing and Heat Pump Modeling to Achieve DecarbonizationIES VE
 
NIST Cybersecurity Framework (CSF) 2.0 Workshop
NIST Cybersecurity Framework (CSF) 2.0 WorkshopNIST Cybersecurity Framework (CSF) 2.0 Workshop
NIST Cybersecurity Framework (CSF) 2.0 WorkshopBachir Benyammi
 
9 Steps For Building Winning Founding Team
9 Steps For Building Winning Founding Team9 Steps For Building Winning Founding Team
9 Steps For Building Winning Founding TeamAdam Moalla
 
Anypoint Code Builder , Google Pub sub connector and MuleSoft RPA
Anypoint Code Builder , Google Pub sub connector and MuleSoft RPAAnypoint Code Builder , Google Pub sub connector and MuleSoft RPA
Anypoint Code Builder , Google Pub sub connector and MuleSoft RPAshyamraj55
 
Salesforce Miami User Group Event - 1st Quarter 2024
Salesforce Miami User Group Event - 1st Quarter 2024Salesforce Miami User Group Event - 1st Quarter 2024
Salesforce Miami User Group Event - 1st Quarter 2024SkyPlanner
 
Machine Learning Model Validation (Aijun Zhang 2024).pdf
Machine Learning Model Validation (Aijun Zhang 2024).pdfMachine Learning Model Validation (Aijun Zhang 2024).pdf
Machine Learning Model Validation (Aijun Zhang 2024).pdfAijun Zhang
 
Apres-Cyber - The Data Dilemma: Bridging Offensive Operations and Machine Lea...
Apres-Cyber - The Data Dilemma: Bridging Offensive Operations and Machine Lea...Apres-Cyber - The Data Dilemma: Bridging Offensive Operations and Machine Lea...
Apres-Cyber - The Data Dilemma: Bridging Offensive Operations and Machine Lea...Will Schroeder
 
VoIP Service and Marketing using Odoo and Asterisk PBX
VoIP Service and Marketing using Odoo and Asterisk PBXVoIP Service and Marketing using Odoo and Asterisk PBX
VoIP Service and Marketing using Odoo and Asterisk PBXTarek Kalaji
 
Introduction to Matsuo Laboratory (ENG).pptx
Introduction to Matsuo Laboratory (ENG).pptxIntroduction to Matsuo Laboratory (ENG).pptx
Introduction to Matsuo Laboratory (ENG).pptxMatsuo Lab
 
COMPUTER 10 Lesson 8 - Building a Website
COMPUTER 10 Lesson 8 - Building a WebsiteCOMPUTER 10 Lesson 8 - Building a Website
COMPUTER 10 Lesson 8 - Building a Websitedgelyza
 
activity_diagram_combine_v4_20190827.pdfactivity_diagram_combine_v4_20190827.pdf
activity_diagram_combine_v4_20190827.pdfactivity_diagram_combine_v4_20190827.pdfactivity_diagram_combine_v4_20190827.pdfactivity_diagram_combine_v4_20190827.pdf
activity_diagram_combine_v4_20190827.pdfactivity_diagram_combine_v4_20190827.pdfJamie (Taka) Wang
 
UiPath Studio Web workshop series - Day 8
UiPath Studio Web workshop series - Day 8UiPath Studio Web workshop series - Day 8
UiPath Studio Web workshop series - Day 8DianaGray10
 
OpenShift Commons Paris - Choose Your Own Observability Adventure
OpenShift Commons Paris - Choose Your Own Observability AdventureOpenShift Commons Paris - Choose Your Own Observability Adventure
OpenShift Commons Paris - Choose Your Own Observability AdventureEric D. Schabell
 
AI You Can Trust - Ensuring Success with Data Integrity Webinar
AI You Can Trust - Ensuring Success with Data Integrity WebinarAI You Can Trust - Ensuring Success with Data Integrity Webinar
AI You Can Trust - Ensuring Success with Data Integrity WebinarPrecisely
 
Igniting Next Level Productivity with AI-Infused Data Integration Workflows
Igniting Next Level Productivity with AI-Infused Data Integration WorkflowsIgniting Next Level Productivity with AI-Infused Data Integration Workflows
Igniting Next Level Productivity with AI-Infused Data Integration WorkflowsSafe Software
 
The Data Metaverse: Unpacking the Roles, Use Cases, and Tech Trends in Data a...
The Data Metaverse: Unpacking the Roles, Use Cases, and Tech Trends in Data a...The Data Metaverse: Unpacking the Roles, Use Cases, and Tech Trends in Data a...
The Data Metaverse: Unpacking the Roles, Use Cases, and Tech Trends in Data a...Aggregage
 
IESVE Software for Florida Code Compliance Using ASHRAE 90.1-2019
IESVE Software for Florida Code Compliance Using ASHRAE 90.1-2019IESVE Software for Florida Code Compliance Using ASHRAE 90.1-2019
IESVE Software for Florida Code Compliance Using ASHRAE 90.1-2019IES VE
 

Kürzlich hochgeladen (20)

KubeConEU24-Monitoring Kubernetes and Cloud Spend with OpenCost
KubeConEU24-Monitoring Kubernetes and Cloud Spend with OpenCostKubeConEU24-Monitoring Kubernetes and Cloud Spend with OpenCost
KubeConEU24-Monitoring Kubernetes and Cloud Spend with OpenCost
 
Building Your Own AI Instance (TBLC AI )
Building Your Own AI Instance (TBLC AI )Building Your Own AI Instance (TBLC AI )
Building Your Own AI Instance (TBLC AI )
 
201610817 - edge part1
201610817 - edge part1201610817 - edge part1
201610817 - edge part1
 
Using IESVE for Loads, Sizing and Heat Pump Modeling to Achieve Decarbonization
Using IESVE for Loads, Sizing and Heat Pump Modeling to Achieve DecarbonizationUsing IESVE for Loads, Sizing and Heat Pump Modeling to Achieve Decarbonization
Using IESVE for Loads, Sizing and Heat Pump Modeling to Achieve Decarbonization
 
NIST Cybersecurity Framework (CSF) 2.0 Workshop
NIST Cybersecurity Framework (CSF) 2.0 WorkshopNIST Cybersecurity Framework (CSF) 2.0 Workshop
NIST Cybersecurity Framework (CSF) 2.0 Workshop
 
9 Steps For Building Winning Founding Team
9 Steps For Building Winning Founding Team9 Steps For Building Winning Founding Team
9 Steps For Building Winning Founding Team
 
Anypoint Code Builder , Google Pub sub connector and MuleSoft RPA
Anypoint Code Builder , Google Pub sub connector and MuleSoft RPAAnypoint Code Builder , Google Pub sub connector and MuleSoft RPA
Anypoint Code Builder , Google Pub sub connector and MuleSoft RPA
 
Salesforce Miami User Group Event - 1st Quarter 2024
Salesforce Miami User Group Event - 1st Quarter 2024Salesforce Miami User Group Event - 1st Quarter 2024
Salesforce Miami User Group Event - 1st Quarter 2024
 
Machine Learning Model Validation (Aijun Zhang 2024).pdf
Machine Learning Model Validation (Aijun Zhang 2024).pdfMachine Learning Model Validation (Aijun Zhang 2024).pdf
Machine Learning Model Validation (Aijun Zhang 2024).pdf
 
Apres-Cyber - The Data Dilemma: Bridging Offensive Operations and Machine Lea...
Apres-Cyber - The Data Dilemma: Bridging Offensive Operations and Machine Lea...Apres-Cyber - The Data Dilemma: Bridging Offensive Operations and Machine Lea...
Apres-Cyber - The Data Dilemma: Bridging Offensive Operations and Machine Lea...
 
VoIP Service and Marketing using Odoo and Asterisk PBX
VoIP Service and Marketing using Odoo and Asterisk PBXVoIP Service and Marketing using Odoo and Asterisk PBX
VoIP Service and Marketing using Odoo and Asterisk PBX
 
Introduction to Matsuo Laboratory (ENG).pptx
Introduction to Matsuo Laboratory (ENG).pptxIntroduction to Matsuo Laboratory (ENG).pptx
Introduction to Matsuo Laboratory (ENG).pptx
 
COMPUTER 10 Lesson 8 - Building a Website
COMPUTER 10 Lesson 8 - Building a WebsiteCOMPUTER 10 Lesson 8 - Building a Website
COMPUTER 10 Lesson 8 - Building a Website
 
activity_diagram_combine_v4_20190827.pdfactivity_diagram_combine_v4_20190827.pdf
activity_diagram_combine_v4_20190827.pdfactivity_diagram_combine_v4_20190827.pdfactivity_diagram_combine_v4_20190827.pdfactivity_diagram_combine_v4_20190827.pdf
activity_diagram_combine_v4_20190827.pdfactivity_diagram_combine_v4_20190827.pdf
 
UiPath Studio Web workshop series - Day 8
UiPath Studio Web workshop series - Day 8UiPath Studio Web workshop series - Day 8
UiPath Studio Web workshop series - Day 8
 
OpenShift Commons Paris - Choose Your Own Observability Adventure
OpenShift Commons Paris - Choose Your Own Observability AdventureOpenShift Commons Paris - Choose Your Own Observability Adventure
OpenShift Commons Paris - Choose Your Own Observability Adventure
 
AI You Can Trust - Ensuring Success with Data Integrity Webinar
AI You Can Trust - Ensuring Success with Data Integrity WebinarAI You Can Trust - Ensuring Success with Data Integrity Webinar
AI You Can Trust - Ensuring Success with Data Integrity Webinar
 
Igniting Next Level Productivity with AI-Infused Data Integration Workflows
Igniting Next Level Productivity with AI-Infused Data Integration WorkflowsIgniting Next Level Productivity with AI-Infused Data Integration Workflows
Igniting Next Level Productivity with AI-Infused Data Integration Workflows
 
The Data Metaverse: Unpacking the Roles, Use Cases, and Tech Trends in Data a...
The Data Metaverse: Unpacking the Roles, Use Cases, and Tech Trends in Data a...The Data Metaverse: Unpacking the Roles, Use Cases, and Tech Trends in Data a...
The Data Metaverse: Unpacking the Roles, Use Cases, and Tech Trends in Data a...
 
IESVE Software for Florida Code Compliance Using ASHRAE 90.1-2019
IESVE Software for Florida Code Compliance Using ASHRAE 90.1-2019IESVE Software for Florida Code Compliance Using ASHRAE 90.1-2019
IESVE Software for Florida Code Compliance Using ASHRAE 90.1-2019
 

Getting Started with Progressive Web Apps [Beyond Tellerrand 2019]

  • 1. Getting started with Aaron Gustafson @AaronGustafson noti.st/AaronGustafson
  • 2. @AaronGustafson Follow along ๏ Slides: https://aka.ms/btconf-pwa-slides ๏ Work files: https://aka.ms/btconf-pwa-code ๏ Final Demo: https://aka.ms/btconf-pwa-live
  • 5. What exactly is a Progressive Web App?
  • 6. “Progressive Web App” is a marketing term Progressive Web App
  • 12. @AaronGustafson What’s a PWA, technically? HTTPS
  • 13. @AaronGustafson What’s a PWA, technically? HTTPS Web App Manifest
  • 14. @AaronGustafson What’s a PWA, technically? HTTPS Web App Manifest Service Worker
  • 17. Starbucks: 2x increase in daily active users aka.ms/google-io-2018
  • 18. Tinder: Core experience with 90% less code aka.ms/tinder-pwa-2017
  • 19. Trivago: 97% increase in click-outs to hotel offers aka.ms/trivago-pwa-2017
  • 20. West Elm: 15% increase in time on site 9% increase in revenue per visit aka.ms/west-elm-pwa-2017
  • 22. PWAs start with a great web experience and then enhance that experience for performance, resilience, installation, and engagement
  • 23. PWAs start with a great web experience and then enhance that experience for performance, resilience, installation, and engagement
  • 26. Progressive /prəˈɡresiv/ happening or developing gradually or in stages; proceeding step by step
  • 28. @AaronGustafson What’s a PWA, technically? HTTPS Web App Manifest Service Worker
  • 30. @AaronGustafson HTTPS is simple (& free) now ๏ Many hosts include it ๏ GitHub ๏ Netlify ๏ AWS ๏ etc. ๏ LetsEnrcypt & Certbot for everything else https://letsencrypt.org/
  • 31. @AaronGustafson What’s a PWA, technically? HTTPS Web App Manifest Service Worker
  • 33. @AaronGustafson Manifest files are JSON files { "property_a": "value", "property_b": ["value_1", "value_2"], "property_c": { "nested_property": "nested_value" } }
  • 34. @AaronGustafson Minimum Viable Manifest { "lang": "en-US", "name": "Aaron Gustafson", "short_name": "AaronG", "description": "The online home and work of Aaron Gustafson." }
  • 35. @AaronGustafson Minimum Viable Manifest { "lang": "en-US", "name": "Aaron Gustafson", "short_name": "AaronG", "description": "The online home and work of Aaron Gustafson." }
  • 36. @AaronGustafson Minimum Viable Manifest { "lang": "en-US", "dir": "ltr", "name": "Aaron Gustafson", "short_name": "AaronG", "description": "The online home and work of Aaron Gustafson." }
  • 37. @AaronGustafson Minimum Viable Manifest { "lang": "en-US", "dir": "auto", // default "name": "Aaron Gustafson", "short_name": "AaronG", "description": "The online home and work of Aaron Gustafson." }
  • 38. @AaronGustafson Minimum Viable Manifest { "lang": "en-US", "name": "Aaron Gustafson", "short_name": "AaronG", "description": "The online home and work of Aaron Gustafson." }
  • 39. @AaronGustafson Minimum Viable Manifest { "lang": "en-US", "name": "Aaron Gustafson", "short_name": "AaronG", "description": "The online home and work of Aaron Gustafson." }
  • 40. @AaronGustafson Minimum Viable Manifest { "lang": "en-US", "name": "Aaron Gustafson", "short_name": "AaronG", "description": "The online home and work of Aaron Gustafson." }
  • 41. @AaronGustafson Minimum Viable Manifest { "lang": "en-US", "name": "Aaron Gustafson", "short_name": "AaronG", "description": "The online home and work of Aaron Gustafson." }
  • 42. @AaronGustafson Reference in the head <link rel="manifest" href="/manifest.json">
  • 43. @AaronGustafson Reference in the head <link rel="manifest" href="/manifest.json">
  • 44. @AaronGustafson Reference in the head <link rel="manifest" href="/manifest.json">
  • 45. @AaronGustafson Let‘s make a manifest! ๏ Open a new text document in your site and name it manifest.json ๏ Create a basic JSON object inside, including the following Manifest members: 1. lang, 2. name, 3. short_name, and 4. description.
  • 46. @AaronGustafson Yours should look like this { "lang": "en-US", "name": "Aaron Gustafson", "short_name": "AaronG", "description": "The online home and work of Aaron Gustafson." }
  • 47. @AaronGustafson Let’s prep for install… { "lang": "en-US", "name": "Aaron Gustafson", "short_name": "AaronG", "description": "The online home and work of Aaron Gustafson." }
  • 48. @AaronGustafson Where do we begin? { … "start_url": "/" }
  • 49. @AaronGustafson Where does this apply? { … "start_url": "/", "scope": "/" // defaults to the start_url value }
  • 50. @AaronGustafson What should it look like? { … "start_url": "/", "display": "minimal-ui" }
  • 51. @AaronGustafson Display Modes "display": "browser" "display": "standalone" "display": "minimal-ui""display": "fullscreen"
  • 52. @AaronGustafson Need to lock orientation? { … "start_url": "/", "display": "minimal-ui", "orientation": "any" }
  • 53. @AaronGustafson Orientation options ๏ “any “– no preference ๏ “natural” – the default orientation of the device ๏ “portrait” ๏ “portrait-primary” ๏ “portrait-secondary” ๏ “landscape” ๏ “landscape-primary” ๏ “landscape-secondary” Details: https://www.w3.org/TR/screen-orientation/
  • 54. @AaronGustafson A little color… { … "start_url": "/", "display": "minimal-ui", "orientation": "any", "theme_color": "#27831B" }
  • 55. @AaronGustafson A little color… { … "start_url": "/", "display": "minimal-ui", "orientation": "any", "theme_color": "#27831B", "background_color": "#fffcf4" }
  • 56. @AaronGustafson Adding icons { … "icons": [ { "src": "/i/og-logo.png", "type": "image/png", "sizes": "800x600" }, { "src": "/i/notification-icon.png", "type": "image/png", "sizes": "256x256" }, { "src": "/favicon.png", "type": "image/png", "sizes": "16x16" } ] }
  • 57. @AaronGustafson Adding icons { … "icons": [ { "src": "/i/og-logo.png", "type": "image/png", "sizes": "800x600" }, { "src": "/i/notification-icon.png", "type": "image/png", "sizes": "256x256" }, { "src": "/favicon.png", "type": "image/png", "sizes": "16x16" } ] }
  • 58. @AaronGustafson Anatomy of an ImageResource { "src": "/i/og-logo.png", "type": "image/png", "sizes": "800x600" }
  • 59. @AaronGustafson Anatomy of an ImageResource { "src": "/i/og-logo.png", "type": "image/png", "sizes": "800x600" }
  • 60. @AaronGustafson Anatomy of an ImageResource { "src": "/i/og-logo.png", "type": "image/png", "sizes": "800x600" }
  • 61. @AaronGustafson Anatomy of an ImageResource { "src": "/i/og-logo.png", "type": "image/png", "sizes": "800x600" }
  • 62. @AaronGustafson Anatomy of an ImageResource { "src": "/i/og-logo.png", "type": "image/png", "sizes": "800x600" }
  • 63. @AaronGustafson Anatomy of an ImageResource { "src": "/i/og-logo.png", "type": "image/png", "sizes": "800x600", "purpose": "badge" }
  • 64. @AaronGustafson Anatomy of an ImageResource { "src": "/i/og-logo.png", "type": "image/png", "sizes": "800x600", "purpose": "maskable" }
  • 65. @AaronGustafson Let‘s improve our manifest! ๏ Add the following Manifest members: 1. start_url, 2. display, 3. theme_color, 4. background_color, and 5. icons Suggested sizes: 48x48, 72x72, 96x96, 144x144, 192x192, and 512x512
  • 68. How’d it go? Any questions?
  • 69. Let’s take a break.
  • 70. @AaronGustafson What’s a PWA, technically? HTTPS Web App Manifest Service Worker
  • 72. @AaronGustafson Registering a Service Worker if ( "serviceWorker" in navigator ) { navigator.serviceWorker.register( "/serviceworker.min.js" ); }
  • 73. @AaronGustafson Registering a Service Worker if ( "serviceWorker" in navigator ) { navigator.serviceWorker.register( "/serviceworker.min.js" ); }
  • 74. @AaronGustafson Registering a Service Worker if ( "serviceWorker" in navigator ) { navigator.serviceWorker.register( "/serviceworker.min.js" ); }
  • 75. @AaronGustafson Registering a Service Worker if ( "serviceWorker" in navigator ) { navigator.serviceWorker.register( "/serviceworker.min.js" ); }
  • 76. @AaronGustafson Registering a Service Worker if ( "serviceWorker" in navigator ) { navigator.serviceWorker.register( "/serviceworker.min.js" ) .then(function( registration ){ console.log( "Success!", registration.scope ); }) .catch(function( error ){ console.error( "Failure!" , error ); }); }
  • 78. @AaronGustafson The Service Worker Lifecycle Browser Install Activation Ready aka.ms/pwa-lifecycle
  • 79. @AaronGustafson Listening for these events self.addEventListener( "install", function( event ){ console.log( "installing" ); });
  • 80. @AaronGustafson Listening for these events self.addEventListener( "install", function( event ){ console.log( "installing" ); });
  • 81. @AaronGustafson Listening for these events self.addEventListener( "install", function( event ){ console.log( "installing" ); });
  • 82. @AaronGustafson Listening for these events self.addEventListener( "install", function( event ){ console.log( "installing" ); });
  • 83. @AaronGustafson Let‘s make a Service Worker! ๏ Create a new file for your service worker ๏ Register the Service Worker navigator.serviceWorker.register( path ) ๏ Log to the console from the following events: ๏ install ๏ activate
  • 84. @AaronGustafson Yours should look similar self.addEventListener( "install", function( event ){ console.log( "installing" ); }); self.addEventListener( "activate", function( event ){ console.log( "activating" ); });
  • 85. @AaronGustafson The Service Worker Lifecycle Browser Install Activation Ready aka.ms/pwa-lifecycle
  • 86. @AaronGustafson Preloading assets self.addEventListener( "install", function( event ){ event.waitUntil( caches.open("v1").then(function(cache) { return cache.addAll([ "/css/main.css", "/js/main.js" ]); }) ); });
  • 87. @AaronGustafson Preloading assets self.addEventListener( "install", function( event ){ event.waitUntil( caches.open("v1").then(function(cache) { return cache.addAll([ "/css/main.css", "/js/main.js" ]); }) ); });
  • 88. @AaronGustafson Preloading assets self.addEventListener( "install", function( event ){ event.waitUntil( caches.open("v1").then(function(cache) { return cache.addAll([ "/css/main.css", "/js/main.js" ]); }) ); });
  • 89. @AaronGustafson Preloading assets self.addEventListener( "install", function( event ){ event.waitUntil( caches.open("v1").then(function(cache) { return cache.addAll([ "/css/main.css", "/js/main.js" ]); }) ); });
  • 90. @AaronGustafson Preloading assets self.addEventListener( "install", function( event ){ event.waitUntil( caches.open("v1").then(function(cache) { return cache.addAll([ "/css/main.css", "/js/main.js" ]); }) ); });
  • 91. @AaronGustafson Let’s refactor self.addEventListener( "install", function( event ){ event.waitUntil( caches.open("v1").then(function(cache) { return cache.addAll([ "/css/main.css", "/js/main.js" ]); }) ); });
  • 92. @AaronGustafson Let’s refactor const VERSION = "v1"; self.addEventListener( "install", function( event ){ event.waitUntil( caches.open( VERSION ).then(function(cache) { return cache.addAll([ "/css/main.css", "/js/main.js" ]); }) ); });
  • 93. @AaronGustafson Let‘s preload assets ๏ Leverage the install event to pre-load some assets ๏ Load your page in a browser and see that the assets are loaded ๏ Bump your version number and reload the page ๏ What happened?
  • 94. @AaronGustafson We need to clean up const VERSION = "v1"; // install event self.addEventListener( "activate", event => { // clean up stale caches event.waitUntil( caches.keys() .then( keys => { return Promise.all( keys.filter( key => { return ! key.startsWith( VERSION ); }) .map( key => { return caches.delete( key ); }) ); }) ); });
  • 95. @AaronGustafson We need to clean up const VERSION = "v1"; // install event self.addEventListener( "activate", event => { // clean up stale caches event.waitUntil( caches.keys() .then( keys => { return Promise.all( keys.filter( key => { return ! key.startsWith( VERSION ); }) .map( key => { return caches.delete( key ); }) ); }) ); });
  • 96. @AaronGustafson We need to clean up const VERSION = "v1"; // install event self.addEventListener( "activate", event => { // clean up stale caches event.waitUntil( caches.keys() .then( keys => { return Promise.all( keys.filter( key => { return ! key.startsWith( VERSION ); }) .map( key => { return caches.delete( key ); }) ); }) ); });
  • 97. @AaronGustafson We need to clean up const VERSION = "v1"; // install event self.addEventListener( "activate", event => { // clean up stale caches event.waitUntil( caches.keys() .then( keys => { return Promise.all( keys.filter( key => { return ! key.startsWith( VERSION ); }) .map( key => { return caches.delete( key ); }) ); }) ); });
  • 98. @AaronGustafson We need to clean up const VERSION = "v1"; // install event self.addEventListener( "activate", event => { // clean up stale caches event.waitUntil( caches.keys() .then( keys => { return Promise.all( keys.filter( key => { return ! key.startsWith( VERSION ); }) .map( key => { return caches.delete( key ); }) ); }) ); });
  • 99. @AaronGustafson We need to clean up const VERSION = "v1"; // install event self.addEventListener( "activate", event => { // clean up stale caches event.waitUntil( caches.keys() .then( keys => { return Promise.all( keys.filter( key => { return ! key.startsWith( VERSION ); }) .map( key => { return caches.delete( key ); }) ); }) ); });
  • 100. @AaronGustafson Let‘s clean up ๏ Leverage the activate event to clear stale caches ๏ Load your page in a browser ๏ What happened?
  • 101. @AaronGustafson Use the new SW immediately const VERSION = "v1"; self.addEventListener( "install", function( event ){ event.waitUntil( caches.open( VERSION ).then(function(cache) { return cache.addAll([ "/css/main.css", "/js/main.js" ]); }) ); self.skipWaiting(); });
  • 102. @AaronGustafson Take over any active clients self.addEventListener( "activate", event => { // clean up stale caches event.waitUntil( caches.keys() .then( keys => { return Promise.all( keys.filter( key => { return ! key.startsWith( VERSION ); }) .map( key => { return caches.delete( key ); }) ); }) ); clients.claim(); });
  • 103. @AaronGustafson Look what happens ๏ Add skipWaiting() to your install event ๏ Look at the DevTools and observe how the state of the Service Worker changes with and without this line of code. ๏ What happened? ๏ Open your site in two tabs and add clients.claim() to your activate event ๏ Look at the DevTools in each. ๏ What happened?
  • 106. @AaronGustafson How requests are made Browser Internet
  • 107. @AaronGustafson Along comes Service Worker Browser Internet
  • 108. @AaronGustafson Along comes Service Worker Browser Internet Cache
  • 109. @AaronGustafson Along comes Service Worker Browser Internet Cache 👎🏻
  • 110. @AaronGustafson Intercepting requests self.addEventListener( "fetch", function( event ){ console.log( "fetching" ); });
  • 111. @AaronGustafson Let‘s try it out ๏ Add a fetch event handler ๏ Load your page in a browser ๏ What happened? ๏ Instead of a string, log event.request.url ๏ Load your page in a browser ๏ What do you see?
  • 112. @AaronGustafson We can issue our own fetch self.addEventListener( "fetch", function( event ){ event.respondWith( fetch( event.request ) ); });
  • 113. @AaronGustafson We can issue our own fetch self.addEventListener( "fetch", function( event ){ event.respondWith( fetch( event.request ) ); });
  • 115. @AaronGustafson What if the request fails? const VERSION = "v1", OFFLINE_PAGE = "offline.html";
  • 116. @AaronGustafson What if the request fails? self.addEventListener( "install", function( event ){ event.waitUntil( caches.open("v1").then(function(cache) { return cache.addAll([ "/css/main.css", "/js/main.js", OFFLINE_PAGE ]); }) ); });
  • 117. @AaronGustafson What if the request fails? self.addEventListener( "fetch", function( event ){ if ( event.request.mode === "navigate" ) { event.respondWith( fetch(event.request) .catch(error => { console.log( "Fetch failed; returning offline page." ); return caches.match( OFFLINE_PAGE ); }) ); } });
  • 118. @AaronGustafson What if the request fails? self.addEventListener( "fetch", function( event ){ if ( event.request.mode === "navigate" ) { event.respondWith( fetch(event.request) .catch(error => { console.log( "Fetch failed; returning offline page." ); return caches.match( OFFLINE_PAGE ); }) ); } });
  • 119. @AaronGustafson What if the request fails? self.addEventListener( "fetch", function( event ){ if ( event.request.mode === "navigate" ) { event.respondWith( fetch(event.request) .catch(error => { console.log( "Fetch failed; returning offline page." ); return caches.match( OFFLINE_PAGE ); }) ); } });
  • 120. @AaronGustafson What if the request fails? self.addEventListener( "fetch", function( event ){ if ( event.request.mode === "navigate" ) { event.respondWith( fetch(event.request) .catch(error => { console.log( "Fetch failed; returning offline page." ); return caches.match( OFFLINE_PAGE ); }) ); } });
  • 121. @AaronGustafson Let‘s try it out ๏ Add an offline.html page ๏ Pre-cache it during install ๏ Remember to rev VERSION ๏ Add a fetch handler for navigations, providing the offline page as a fallback ๏ Turn off the network (once you know the SW is running) and see what happens
  • 122. @AaronGustafson Caching strategies ๏ Network → cache → offline ๏ Cache → network → offline ๏ Cache vs network race → cache → offline ๏ etc.
  • 123. @AaronGustafson Network, then cache if ( event.request.mode === "navigate" ) { event.respondWith( fetch( event.request ) .catch(error => { return caches.match( OFFLINE_PAGE ); }) ); }
  • 124. @AaronGustafson Network, then cache self.addEventListener( "fetch", function( event ){ let request = event.request, url = request.url; // all the rest of the code }
  • 125. @AaronGustafson Network, then cache if ( event.request.mode === "navigate" ) { event.respondWith( fetch( event.request ) .catch(error => { return caches.match( OFFLINE_PAGE ); }) ); }
  • 126. @AaronGustafson Network, then cache if ( request.mode === "navigate" ) { event.respondWith( fetch( request ) .catch(error => { return caches.match( OFFLINE_PAGE ); }) ); }
  • 127. @AaronGustafson Network, then cache if ( request.mode === "navigate" ) { event.respondWith( fetch( request ) .then( response => { event.waitUntil( caches.open( VERSION ).then( cache => { return cache.put( request, response ); }) ); return response.clone(); }) .catch(error => { return caches.match( OFFLINE_PAGE ); }) ); }
  • 128. @AaronGustafson Network, then cache if ( request.mode === "navigate" ) { event.respondWith( fetch( request ) .then( response => { event.waitUntil( caches.open( VERSION ).then( cache => { return cache.put( request, response ); }) ); return response.clone(); }) .catch(error => { return caches.match( OFFLINE_PAGE ); }) ); }
  • 129. @AaronGustafson Network, then cache if ( request.mode === "navigate" ) { event.respondWith( fetch( request ) .then( response => { event.waitUntil( caches.open( VERSION ).then( cache => { return cache.put( request, response ); }) ); return response.clone(); }) .catch(error => { return caches.match( OFFLINE_PAGE ); }) ); }
  • 130. @AaronGustafson Network, then cache if ( request.mode === "navigate" ) { event.respondWith( fetch( request ) .then( response => { event.waitUntil( caches.open( VERSION ).then( cache => { return cache.put( request, response ); }) ); return response.clone(); }) .catch(error => { return caches.match( OFFLINE_PAGE ); }) ); }
  • 131. @AaronGustafson Network, then cache .catch(error => { return caches.match( OFFLINE_PAGE ); })
  • 132. @AaronGustafson Network, then cache .catch(error => { return caches.match( request ).then( cached_result => { if ( cached_result ) { return cached_result; } return caches.match( OFFLINE_PAGE ); }); })
  • 133. @AaronGustafson Network, then cache .catch(error => { return caches.match( request ).then( cached_result => { if ( cached_result ) { return cached_result; } return caches.match( OFFLINE_PAGE ); }); })
  • 134. @AaronGustafson Network, then cache .catch(error => { return caches.match( request ).then( cached_result => { if ( cached_result ) { return cached_result; } return caches.match( OFFLINE_PAGE ); }); })
  • 135. @AaronGustafson Network, then cache if ( request.mode === "navigate" ) { event.respondWith( fetch( request ).then( response => { event.waitUntil( caches.open( VERSION ).then( cache => { return cache.put( request, response ); }) ); return response.clone(); }) .catch(error => { return caches.match( request ).then( cached_result => { if ( cached_result ) { return cached_result; } return caches.match( OFFLINE_PAGE ); }); }) ); }
  • 136. @AaronGustafson Caching strategies ✓ Network → cache → offline ๏ Cache → network → offline ๏ Cache vs network race → cache → offline ๏ etc.
  • 137. @AaronGustafson Cache first, then network if ( /.css$/.test(url) || /.js$/.test(url) ) { event.respondWith( caches.match( request ) .then( cached_result => { if ( cached_result ) { return cached_result; } return fetch( request ).then( response => { event.waitUntil( caches.open( VERSION ).then( cache => { return cache.put( request, reponse ); }) ); return response.clone(); }) .catch( new Response( "", { status: 408, statusText: "The server appears to be offline." }) ); }) ); }
  • 138. @AaronGustafson Cache first, then network if ( /.css$/.test(url) || /.js$/.test(url) ) { event.respondWith( caches.match( request ) .then( cached_result => { if ( cached_result ) { return cached_result; } return fetch( request ).then( response => { event.waitUntil( caches.open( VERSION ).then( cache => { return cache.put( request, reponse ); }) ); return response.clone(); }) .catch( new Response( "", { status: 408, statusText: "The server appears to be offline." }) ); }) ); }
  • 139. @AaronGustafson Cache first, then network if ( /.css$/.test(url) || /.js$/.test(url) ) { event.respondWith( caches.match( request ) .then( cached_result => { if ( cached_result ) { return cached_result; } return fetch( request ).then( response => { event.waitUntil( caches.open( VERSION ).then( cache => { return cache.put( request, reponse ); }) ); return response.clone(); }) .catch( new Response( "", { status: 408, statusText: "The server appears to be offline." }) ); }) ); }
  • 140. @AaronGustafson Cache first, then network if ( /.css$/.test(url) || /.js$/.test(url) ) { event.respondWith( caches.match( request ) .then( cached_result => { if ( cached_result ) { return cached_result; } return fetch( request ).then( response => { event.waitUntil( caches.open( VERSION ).then( cache => { return cache.put( request, reponse ); }) ); return response.clone(); }) .catch( new Response( "", { status: 408, statusText: "The server appears to be offline." }) ); }) ); }
  • 141. @AaronGustafson Cache first, then network if ( /.css$/.test(url) || /.js$/.test(url) ) { event.respondWith( caches.match( request ) .then( cached_result => { if ( cached_result ) { return cached_result; } return fetch( request ).then( response => { event.waitUntil( caches.open( VERSION ).then( cache => { return cache.put( request, response ); }) ); return response.clone(); }) .catch( new Response( "", { status: 408, statusText: "The server appears to be offline." }) ); }) ); }
  • 142. @AaronGustafson Cache first, then network if ( /.css$/.test(url) || /.js$/.test(url) ) { event.respondWith( caches.match( request ) .then( cached_result => { if ( cached_result ) { return cached_result; } return fetch( request ).then( response => { event.waitUntil( caches.open( VERSION ).then( cache => { return cache.put( request, reponse ); }) ); return response.clone(); }) .catch( new Response( "", { status: 408, statusText: "The server appears to be offline." }) ); }) ); }
  • 144. @AaronGustafson We cached a fetch twice return fetch( request ).then( response => { event.waitUntil( caches.open( VERSION ).then( cache => { return cache.put( request, response ); }) ); return response.clone(); })
  • 145. @AaronGustafson Let’s make it a function function cacheResponse( response ) { event.waitUntil( caches.open( VERSION ).then( cache => { return cache.put( request, response ); }) ); return response.clone(); }
  • 146. @AaronGustafson Let’s make it a function function cacheResponse( response, event ) { event.waitUntil( caches.open( VERSION ).then( cache => { return cache.put( event.request, response ); }) ); return response.clone(); }
  • 147. @AaronGustafson And we can use it like this return fetch( request ) .then( response => cacheResponse( response, event ) )
  • 148. @AaronGustafson Network, then cache if ( request.mode === "navigate" ) { event.respondWith( fetch( request ) .then( response => cacheResponse( response, event ) ) .catch(error => { return caches.match( request ).then( cached_result => { if ( cached_result ) { return cached_result; } return caches.match( OFFLINE_PAGE ); }); }) ); }
  • 149. @AaronGustafson Cache first, then network if ( /.css$/.test(url) || /.js$/.test(url) ) { event.respondWith( caches.match( request ) .then( cached_result => { if ( cached_result ) { return cached_result; } return fetch( request ) .then( response => cacheResponse( response, event ) ) .catch( new Response( "", { status: 408, statusText: "The server appears to be offline." }) ); }) ); }
  • 150. @AaronGustafson Caching strategies ✓ Network → cache → offline ✓ Cache → network → offline ๏ Cache vs network race → cache → offline ๏ etc.
  • 151. @AaronGustafson Who will win? document.addEventListener('DOMContentLoaded', function(event) { var networkDone = false; var networkRequest = fetch('weather.json').then(function(response) { return response.json(); }) .then(function(json) { networkDone = true; updatePage(json); }); caches.match('weather.json').then(function(response) { if ( ! response) throw Error('No data'); return response.json(); }) .then(function(json) { if (!networkDone) updatePage(json); }) .catch(function() { return networkRequest; }) .catch(function() { console.log('We have nothing.'); }) .then(hideLoading); }); https://git.io/v56s4
  • 152. @AaronGustafson Who will win? document.addEventListener('DOMContentLoaded', function(event) { var networkDone = false; var networkRequest = fetch('weather.json').then(function(response) { return response.json(); }) .then(function(json) { networkDone = true; updatePage(json); }); caches.match('weather.json').then(function(response) { if ( ! response) throw Error('No data'); return response.json(); }) .then(function(json) { if (!networkDone) updatePage(json); }) .catch(function() { return networkRequest; }) .catch(function() { console.log('We have nothing.'); }) .then(hideLoading); }); https://git.io/v56s4
  • 153. @AaronGustafson Who will win? document.addEventListener('DOMContentLoaded', function(event) { var networkDone = false; var networkRequest = fetch('weather.json').then(function(response) { return response.json(); }) .then(function(json) { networkDone = true; updatePage(json); }); caches.match('weather.json').then(function(response) { if ( ! response) throw Error('No data'); return response.json(); }) .then(function(json) { if (!networkDone) updatePage(json); }) .catch(function() { return networkRequest; }) .catch(function() { console.log('We have nothing.'); }) .then(hideLoading); }); https://git.io/v56s4
  • 154. @AaronGustafson Who will win? document.addEventListener('DOMContentLoaded', function(event) { var networkDone = false; var networkRequest = fetch('weather.json').then(function(response) { return response.json(); }) .then(function(json) { networkDone = true; updatePage(json); }); caches.match('weather.json').then(function(response) { if ( ! response) throw Error('No data'); return response.json(); }) .then(function(json) { if (!networkDone) updatePage(json); }) .catch(function() { return networkRequest; }) .catch(function() { console.log('We have nothing.'); }) .then(hideLoading); }); https://git.io/v56s4
  • 155. @AaronGustafson Let‘s discuss ๏ In what scenarios would these different caching strategies be most appropriate? ๏ Are there other strategies you’d like to discuss? ๏ Do you want to add these caching strategies to your site now?
  • 157. Could we save data?
  • 158. @AaronGustafson Look at the connection var slow_connection = false, save_data = false; function testConnection() { // only test every minute if ( last_tested && Date.now() < last_tested + ( 60 * 1000 ) ) { return; } if ( 'connection' in navigator ) { slow_connection = ( navigator.connection.downlink < 0.5 ); save_data = navigator.connection.saveData; last_tested = Date.now(); } }
  • 159. @AaronGustafson Look at the connection var slow_connection = false, save_data = false; function testConnection() { // only test every minute if ( last_tested && Date.now() < last_tested + ( 60 * 1000 ) ) { return; } if ( 'connection' in navigator ) { slow_connection = ( navigator.connection.downlink < 0.5 ); save_data = navigator.connection.saveData; last_tested = Date.now(); } }
  • 160. @AaronGustafson Look at the connection var slow_connection = false, save_data = false; function testConnection() { // only test every minute if ( last_tested && Date.now() < last_tested + ( 60 * 1000 ) ) { return; } if ( 'connection' in navigator ) { slow_connection = ( navigator.connection.downlink < 0.5 ); save_data = navigator.connection.saveData; last_tested = Date.now(); } }
  • 161. @AaronGustafson Look at the connection var slow_connection = false, save_data = false; function testConnection() { // only test every minute if ( last_tested && Date.now() < last_tested + ( 60 * 1000 ) ) { return; } if ( 'connection' in navigator ) { slow_connection = ( navigator.connection.downlink < 0.5 ); save_data = navigator.connection.saveData; last_tested = Date.now(); } }
  • 162. @AaronGustafson Look at the connection self.addEventListener( "fetch", function( event ){ testConnection(); let request = event.request, url = request.url; … });
  • 163. @AaronGustafson Use that information else if ( request.headers.get("Accept").includes("image") ) { event.respondWith( caches.match( request ).then( cached_result => { // cached first if ( cached_result ) { return cached_result; } // fallback to network if ( ! slow_connection && ! save_data ) { return fetch( request ) .then( response => cacheResponse( response, event ) ) // fail .catch( // Respond with an “offline” image ); } else { // Respond with a “saving data” image } }) ); }
  • 164. @AaronGustafson Use that information else if ( request.headers.get("Accept").includes("image") ) { event.respondWith( caches.match( request ).then( cached_result => { // cached first if ( cached_result ) { return cached_result; } // fallback to network if ( ! slow_connection && ! save_data ) { return fetch( request ) .then( response => cacheResponse( response, event ) ) // fail .catch( // Respond with an “offline” image ); } else { // Respond with a “saving data” image } }) ); }
  • 165. @AaronGustafson Use that information else if ( request.headers.get("Accept").includes("image") ) { event.respondWith( caches.match( request ).then( cached_result => { // cached first if ( cached_result ) { return cached_result; } // fallback to network if ( ! slow_connection && ! save_data ) { return fetch( request ) .then( response => cacheResponse( response, event ) ) // fail .catch( // Respond with an “offline” image ); } else { // Respond with a “saving data” image } }) ); }
  • 166. @AaronGustafson Use that information else if ( request.headers.get("Accept").includes("image") ) { event.respondWith( caches.match( request ).then( cached_result => { // cached first if ( cached_result ) { return cached_result; } // fallback to network if ( ! slow_connection && ! save_data ) { return fetch( request ) .then( response => cacheResponse( response, event ) ) // fail .catch( // Respond with an “offline” image ); } else { // Respond with a “saving data” image } }) ); }
  • 167. @AaronGustafson Use that information else if ( request.headers.get("Accept").includes("image") ) { event.respondWith( caches.match( request ).then( cached_result => { // cached first if ( cached_result ) { return cached_result; } // fallback to network if ( ! slow_connection && ! save_data ) { return fetch( request ) .then( response => cacheResponse( response, event ) ) // fail .catch( // Respond with an “offline” image ); } else { // Respond with a “saving data” image } }) ); }
  • 168. @AaronGustafson Use that information else if ( request.headers.get("Accept").includes("image") ) { event.respondWith( caches.match( request ).then( cached_result => { // cached first if ( cached_result ) { return cached_result; } // fallback to network if ( ! slow_connection && ! save_data ) { return fetch( request ) .then( response => cacheResponse( response, event ) ) // fail .catch( // Respond with an “offline” image ); } else { // Respond with a “saving data” image } }) ); }
  • 169. @AaronGustafson Dynamic images? Yes please! const VERSION = "v2", OFFLINE_PAGE = "offline.html", SVG_OFFLINE = '<svg …></svg>', SVG_SLOW = '<svg …></svg>';
  • 170. @AaronGustafson Dynamic images? Yes please! function newImageResponse( svg ) { return new Response( svg, { headers: { 'Content-Type': 'image/svg+xml' } }); }
  • 171. @AaronGustafson An SVG for your troubles else if ( request.headers.get("Accept").includes("image") ) { event.respondWith( caches.match( request ).then( cached_result => { // cached first if ( cached_result ) { return cached_result; } // fallback to network if ( ! slow_connection && ! save_data ) { return fetch( request ) .then( response => cacheResponse( response, event ) ) // fail .catch( // Respond with an “offline” image ); } else { // Respond with a “saving data” image } }) ); }
  • 172. @AaronGustafson An SVG for your troubles else if ( request.headers.get("Accept").includes("image") ) { event.respondWith( caches.match( request ).then( cached_result => { // cached first if ( cached_result ) { return cached_result; } // fallback to network if ( ! slow_connection && ! save_data ) { return fetch( request ) .then( response => cacheResponse( response, event ) ) // fail .catch( () => newImageResponse( SVG_OFFLINE ) ); } else { return newImageResponse( SVG_SLOW ); } }) ); }
  • 173. @AaronGustafson Let‘s discuss ๏ What other ways we could use Service Workers to improve the user experience?
  • 175. @AaronGustafson Bonus content! ๏ Share Target ๏ Shortcuts (coming soon)
  • 176. PWAs start with a great web experience and then enhance that experience for performance, resilience, installation, and engagement
  • 177. PWAs start with a great web experience and then enhance that experience for performance, resilience, installation, and engagement
  • 178. @AaronGustafson Congrats, you made a PWA! HTTPS Web App Manifest Service Worker

Hinweis der Redaktion

  1. Hi there, my name is Aaron Gustafson Pronouns he/him/his WaSP Microsoft - Web Standards & a11y Thank you to Jeffrey & Eric for having me and to Toby, Marci, Mike and all of the staff that keep this conference running so smoothly.
  2. I’m sure many of you may be wondering: What exactly is a PWA? (apart from yet another acronym)
  3. PWA stands for Progressive Web App. And, believe me, you’re not alone in asking this question.
  4. Lots of very smart people are wrestling with it right now.
  5. When it comes down to it…
  6. I think one of the things about the term progressive web app that trips folks up is the “web app” bit. What is a web app anyway? Jeremy Keith likes to say “Like obscenity and brunch, web apps can be described but not defined.”
  7. What’s important to realize, however is that any web project can (and probably should be) a progressive web app.
  8. In fact, you might as well think of these as progressive…
  9. Web sites.
  10. PWAs have a lot of support from the browser world and at this point, most of the major capabilities of PWAs—with some exceptions like Push on iOS—are available in all modern browsers.
  11. As someone who builds websites, “PWA” as a marketing term isn’t terribly helpful. So let’s look at a what it takes to turn your site into a PWA. It starts with making your site secure. Most of the APIs you will need access to are only available under SSL.
  12. Next, you’ll need to add a Web App Manifest to your site. This contains a lot of the ”meta” information about your site that can be useful should a user choose to install it.
  13. The final requirement for a MVPWA is a Service Worker. I am going to hold off on discussing Service Workers for a moment, but we’ll circle back to them momentarily.
  14. So there’s a lot of hype around PWAs. Should you believe it?
  15. Maybe? Probably. Here are a few tidbits you might find interesting.
  16. There are a ton of success stories out there.
  17. I higly recommend checking out PWA stats.com, from the folks at Cloud Four, for more.
  18. In truth…
  19. The key word here is enhance
  20. Combine enhance with the first word in PWA—progressive—and you arrive at the age old philosophy of
  21. Progressive enhancement, which is really at the core of progressive web apps
  22. So what does that actually look like? Let’s tuck into it…
  23. So we’ve got a normal site that can become a PWA.
  24. So back to our MVPWA. I mentioned that I would come back to Service Worker, so let’s…
  25. So back to our MVPWA. I mentioned that I would come back to Service Worker, so let’s…
  26. So back to our MVPWA. I mentioned that I would come back to Service Worker, so let’s…
  27. So back to our MVPWA. I mentioned that I would come back to Service Worker, so let’s…
  28. Everything is optional, but…
  29. First off, you can define the language. If you have a multi-lingual app, you might consider providing multiple manifests tailored to each as there is—as of yet—no way of managing translations within the manifest.
  30. If you want, you can define a writing direction like “ltr” for left to right or “rtl” for right to left, but you could also…
  31. Set it to optional, which is the default.
  32. In which case, you don’t really need it.
  33. Name is the preferred name for the app. Short name enables you to define a specific short option just in case the host operating system doesn’t support as many characters as you need for your app’s name. Better to not leave it to chance.
  34. Description says what the site or app does.
  35. We’ll come back this this more in a moment.
  36. The next step is to connect this file to your site via HTML, using the link tag
  37. The relationship of the linked document is ”manifest”
  38. And the href should point to your JSON file. It’s worth noting…
  39. 5-7 min
  40. This is the page that should open when your app is launched after install
  41. This is the page that should open when your app is launched after install
  42. There are a few modes available here…
  43. This can be overridden via JS as well using the Screen Orientation API
  44. You can see the title bar is colored
  45. Here the background is applied to the splash screen… But what about this icon?
  46. Three different options
  47. The src is the location of your source file (can be relative to the manifest URL) Any image format is possible, though SVG is not well supported (yet)
  48. MIME declaration for the image to enable browser to select based on support
  49. Space-separated list of dimensions (width x height) supported by the image. ICO files can have multiple, otherwise it should be the natural dimensions.
  50. 5-7 min
  51. So we’ve got a normal site that can become a PWA.
  52. Or to break it down further
  53. 5-7 min
  54. 5-7 min
  55. So back to our MVPWA. I mentioned that I would come back to Service Worker, so let’s…
  56. Focus on that. A special kind of web worker (it runs in it’s own thread) Concerned with reducing network dependence Your own personal “man in the middle”
  57. In order to take advantage of this, you need to register a service worker in the browser.
  58. First test for the feature
  59. Then register the worker by pointing at the Service Worker JavaScript file you’ve created.
  60. The path is very important. If you want to run your service worker for the whole site, it must be in the root directory, NOT your javascript subdirectory. If it is in a subdirectory, it can only run against requests for assets in that subdirectory or deeper.
  61. The registration returns a Promise, so you can act on it.
  62. The great thing about this is that it’s an enhancement. The site will work fine without Service Worker as well, but it works better with it.
  63. A Service Worker has a lifecycle. There is a lot of nuance to it, but here’s the Cliff’s Notes version… The Service Worker is registered with the browser. Then it goes through the install process. You can use this opportunity to pre-cache some assets if you like. Once installed, it’s activated. This is generally a good time to clean up any old caches. Then it’s ready to use, but it will not actually do anything until the next page is loaded. I’m going to quickly go through some examples of how Service Workers can operate, but it’s important to remember you are in total control of their behavior.
  64. 5-7 min
  65. Install is a great time to download key resources & add them to the cache
  66. 5-7 min
  67. Here I’m actually using ES6 syntax with the fat arrow function. ES6 is allowable in Service Worker because all Service Worker-supporting browsers also support the more modern JavaScript syntax
  68. 5-7 min
  69. Normally an installed ServiceWorker will wait until the next page load to activate. This skips the waiting.
  70. And this allows you to claim any open clients (windows, tabs, etc.) and apply the service worker to them.
  71. 5-7 min
  72. 5-7 min
  73. In a traditional networking scenario…
  74. When a Service Worker is in play, it sits between the browser and the network.
  75. It also has programmatic access to a local cache. It can intercept network requests from the browser and respond with items in the cache without ever going out to the network. Say, for instance, requests for CSS and JavaScript files you pre-cached during install
  76. You could also set up rules to tell the SW to go to the cache first, but fall back to the network if a cached copy of the resource is not available. Then you can keep a copy for next time too. Caching is key to improved performance and good offline experiences, which are the hallmark of a PWA.
  77. 5-7 min
  78. But that’s not terribly interesting
  79. But that’s not terribly interesting
  80. Navigation only
  81. This you saw before.
  82. But now we catch errors and respond from the cache
  83. 5-7 min
  84. Different approaches for different needs
  85. We just looked at this… what if we want to try the network and fall back to a cached version of the page?
  86. I’m going to make the code a little easier to read by extracting two useful pieces of data, the request and it’s URL
  87. So this…
  88. Becomes this
  89. Then, before we can actually respond from the cache, we need to add HTML to the cache First, we insert a then() block, which gets the response object
  90. And cache it, using the request as a key
  91. And then we return the response
  92. Now we can focus on the catch() block
  93. Now we can focus on the catch() block
  94. Now we can focus on the catch() block
  95. Now we can focus on the catch() block
  96. Now we can focus on the catch() block
  97. Now we can focus on the catch() block
  98. Different approaches for different needs
  99. Technically, this is an else…if
  100. First we check for a cached result and we send it if we have it
  101. Questions?
  102. Questions?
  103. Questions?
  104. Questions?
  105. Questions?
  106. Now we can focus on the catch() block
  107. Questions?
  108. Different approaches for different needs
  109. Questions?
  110. Questions?
  111. Questions?
  112. Questions?
  113. Cache first with a server refresh in the background (for next time), falling back to the network, then offline
  114. New conditional for images
  115. Respond with cache match first
  116. If all is well
  117. Otherwise, we need a fallback
  118. Incidentally, we can use this same approach as a fallback for a fail state.
  119. Adding two new constants
  120. And a new function to generate an svg response
  121. This
  122. Becomes this, passing in the appropriate SVG
  123. Service workers even have their own levels of enhancement CLICK And more will come with time, like background sync.
  124. As I mentioned before… CLICK
  125. start with a great web experience
  126. Thank you so much for your time and attention!