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
30. @AaronGustafson
HTTPS is simple (& free) now
๏ Many hosts include it
๏ GitHub
๏ Netlify
๏ AWS
๏ etc.
๏ LetsEnrcypt & Certbot for everything else
https://letsencrypt.org/
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."
}
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
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?
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 )
);
});
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
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 ) )
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?
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.
I’m sure many of you may be wondering: What exactly is a PWA? (apart from yet another acronym)
PWA stands for Progressive Web App. And, believe me, you’re not alone in asking this question.
Lots of very smart people are wrestling with it right now.
When it comes down to it…
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.”
What’s important to realize, however is that any web project can (and probably should be) a progressive web app.
In fact, you might as well think of these as progressive…
Web sites.
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.
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.
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.
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.
So there’s a lot of hype around PWAs. Should you believe it?
Maybe? Probably. Here are a few tidbits you might find interesting.
There are a ton of success stories out there.
I higly recommend checking out PWA stats.com, from the folks at Cloud Four, for more.
In truth…
The key word here is enhance
Combine enhance with the first word in PWA—progressive—and you arrive at the age old philosophy of
Progressive enhancement, which is really at the core of progressive web apps
So what does that actually look like? Let’s tuck into it…
So we’ve got a normal site that can become a PWA.
So back to our MVPWA. I mentioned that I would come back to Service Worker, so let’s…
So back to our MVPWA. I mentioned that I would come back to Service Worker, so let’s…
So back to our MVPWA. I mentioned that I would come back to Service Worker, so let’s…
So back to our MVPWA. I mentioned that I would come back to Service Worker, so let’s…
Everything is optional, but…
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.
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…
Set it to optional, which is the default.
In which case, you don’t really need it.
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.
Description says what the site or app does.
We’ll come back this this more in a moment.
The next step is to connect this file to your site via HTML, using the link tag
The relationship of the linked document is ”manifest”
And the href should point to your JSON file.
It’s worth noting…
5-7 min
This is the page that should open when your app is launched after install
This is the page that should open when your app is launched after install
There are a few modes available here…
This can be overridden via JS as well using the Screen Orientation API
You can see the title bar is colored
Here the background is applied to the splash screen…
But what about this icon?
Three different options
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)
MIME declaration for the image to enable browser to select based on support
Space-separated list of dimensions (width x height) supported by the image. ICO files can have multiple, otherwise it should be the natural dimensions.
5-7 min
So we’ve got a normal site that can become a PWA.
Or to break it down further
5-7 min
5-7 min
So back to our MVPWA. I mentioned that I would come back to Service Worker, so let’s…
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”
In order to take advantage of this, you need to register a service worker in the browser.
First test for the feature
Then register the worker by pointing at the Service Worker JavaScript file you’ve created.
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.
The registration returns a Promise, so you can act on it.
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.
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.
5-7 min
Install is a great time to download key resources & add them to the cache
5-7 min
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
5-7 min
Normally an installed ServiceWorker will wait until the next page load to activate. This skips the waiting.
And this allows you to claim any open clients (windows, tabs, etc.) and apply the service worker to them.
5-7 min
5-7 min
In a traditional networking scenario…
When a Service Worker is in play, it sits between the browser and the network.
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
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.
5-7 min
But that’s not terribly interesting
But that’s not terribly interesting
Navigation only
This you saw before.
But now we catch errors and respond from the cache
5-7 min
Different approaches for different needs
We just looked at this… what if we want to try the network and fall back to a cached version of the page?
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
So this…
Becomes this
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
And cache it, using the request as a key
And then we return the response
Now we can focus on the catch() block
Now we can focus on the catch() block
Now we can focus on the catch() block
Now we can focus on the catch() block
Now we can focus on the catch() block
Now we can focus on the catch() block
Different approaches for different needs
Technically, this is an else…if
First we check for a cached result and we send it if we have it
Questions?
Questions?
Questions?
Questions?
Questions?
Now we can focus on the catch() block
Questions?
Different approaches for different needs
Questions?
Questions?
Questions?
Questions?
Cache first with a server refresh in the background (for next time), falling back to the network, then offline
New conditional for images
Respond with cache match first
If all is well
Otherwise, we need a fallback
Incidentally, we can use this same approach as a fallback for a fail state.
Adding two new constants
And a new function to generate an svg response
This
Becomes this, passing in the appropriate SVG
Service workers even have their own levels of enhancement
CLICK
And more will come with time, like background sync.