Saving recently viewed pages offline with service workers and vanilla JS
Yesterday, we learned how to write your first service worker. We created a simple offline page to show users when they’re not online.
Today, we’re going to learn how to save pages as the user visits them and make them accessible to users when they go offline.
(This article is going to build on yesterday’s, so if you haven’t read it yet, start there.)
Updating our service worker
In the request for HTML files, if the file is found, we’re going to create a copy of it and save it to a cache before returning it.
First, we’ll check to see if the response.type
is opaque
. If so, that means we’re not allowed to store it because of CORS settings and we’ll ignore it. Otherwise, we can save it.
// HTML files
// Network-first
if (request.headers.get('Accept').includes('text/html')) {
event.respondWith(
fetch(request).then(function (response) {
// Save the response to cache
if (response.type !== 'opaque') {
// Good to save!
}
// Then return it
return response;
}).catch(function (error) {
return caches.match('offline.html');
})
);
}
To save it, we’ll create a copy of the response
using the clone()
method. If we don’t copy it, our service worker will throw an error when trying to return it since, as far as the worker is concerned, it’s already “in use.”
Then, we’ll use the caches.open()
method to open up a new cache, pages
(as you’ll see later in the article, it’s a good idea to store different types of assets in different caches).
Once the cache opens up, we’ll use the cache.put()
method to put the copy
into the cache, associating it with the request
.
// HTML files
// Network-first
if (request.headers.get('Accept').includes('text/html')) {
event.respondWith(
fetch(request).then(function (response) {
// Save the response to cache
if (response.type !== 'opaque') {
var copy = response.clone();
event.waitUntil(caches.open('pages').then(function (cache) {
return cache.put(request, copy);
}));
}
// Then return it
return response;
}).catch(function (error) {
return caches.match('offline.html');
})
);
}
Serving the cached file when the user is offline
Now that we’ve got our file saved in a cache, we’ll want to fallback to it when the user is offline.
If the page isn’t found (such as when the user is offline), the catch()
method will fire. In our callback, we can use the caches.match()
method to check if the request
is saved in one of the caches.
If it is, we’ll return the cached response
. If not, we’ll find and return our offline page instead.
// HTML files
// Network-first
if (request.headers.get('Accept').includes('text/html')) {
event.respondWith(
fetch(request).then(function (response) {
// Save the response to cache
if (response.type !== 'opaque') {
var copy = response.clone();
event.waitUntil(caches.open('pages').then(function (cache) {
return cache.put(request, copy);
}));
}
// Then return it
return response;
}).catch(function (error) {
return caches.match(request).then(function (response) {
return response || caches.match('offline.html');
});
})
);
}
Showing the user a list of available offline pages
If you want, you can display a list of the pages that are available for offline browsing.
In our offline.html
page, let’s add an empty element to add a list of articles to.
<div data-offline></div>
On the page, we’re going to add some JavaScript to check the service worker cache and get back a list of cached files.
First, let’s make sure that the navigator
object exists, and that serviceWorker
is a property of it.
if (navigator && navigator.serviceWorker) {
// We can do service worker stuff...
}
If so, we can use the caches.open()
method to open our pages
method. Once it’s opened, the cache.keys()
method will return an array of keys in the cache.
This is why it’s helpful to save different types of files into different caches. It makes it really easy to quickly get a list of all cached HTML files (or images, or whatever else you’ve cached).
if (navigator && navigator.serviceWorker) {
caches.open('pages').then(function (cache) {
cache.keys().then(function (keys) {
// Do something with the files...
});
});
}
Next, we’ll use the querySelector()
to get the [data-offline]
page. Then, we’ll use the innerHTML
property to add a list of pages.
I like to use the Array.map()
method to convert arrays into markup. For each item, I’m first going to create a list item with the key.url
.
if (navigator && navigator.serviceWorker) {
caches.open('pages').then(function (cache) {
cache.keys().then(function (keys) {
var offline = document.querySelector('[data-offline]');
offline.innerHTML =
'<ul>' +
keys.map(function(key) {
return '<li><a href="' + key.url + '">' + key.url + '</a></li>';
}).join('') +
'</ul>';
});
});
}
A demo
To see this in action, visit this demo page.
Once you’ve given the service worker a chance to install (visit, quit, come back), turn off your wifi and reload the page. The page should still show up, but without the picture.
With your wife still off, try to visit this other demo page. The page should show the offline view instead, with a list of available pages.