In this article by Sean Amarasinghe, the author of the book, Service Worker Development Cookbook, we are going to look at the methods that enable us to control cached content by creating a performance art event viewer web app. If you are a regular visitor to a certain website, chances are that you may be loading most of the resources, like CSS and JavaScript files, from your cache, rather than from the server itself. This saves us necessary bandwidth for the server, as well as requests over the network. Having the control over which content we deliver from the cache and server is a great advantage. Server workers provide us with this powerful feature by having programmatic control over the content.
(For more resources related to this topic, see here.)
To get started with service workers, you will need to have the service worker experiment feature turned on in your browser settings. Service workers only run across HTTPS.
Follow these instructions to set up your file structure. Alternatively, you can download the files from the following location:
https://github.com/szaranger/szaranger.github.io/tree/master/service-workers/03/02/
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Cache First, then Network</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<section id="events">
<h1><span class="nyc">NYC</span> Events TONIGHT</h1>
<aside>
<img src="hypecal.png" />
<h2>Source</h2>
<section>
<h3>Network</h3>
<input type="checkbox" name="network" id="network-
disabled-checkbox">
<label for="network">Disabled</label><br />
<h3>Cache</h3>
<input type="checkbox" name="cache" id="cache-
disabled-checkbox">
<label for="cache">Disabled</label><br />
</section>
<h2>Delay</h2>
<section>
<h3>Network</h3>
<input type="text" name="network-delay"
id="network-delay" value="400" /> ms
<h3>Cache</h3>
<input type="text" name="cache-delay" id="cache-
delay" value="1000" /> ms
</section>
<input type="button" id="fetch-btn" value="FETCH" />
</aside>
<section class="data connection">
<table>
<tr>
<td><strong>Network</strong></td>
<td><output id='network-status'></output></td>
</tr>
<tr>
<td><strong>Cache</strong></td>
<td><output id='cache-status'></output><td>
</tr>
</table>
</section>
<section class="data detail">
<output id="data"></output>
</section>
<script src="index.js"></script>
</body>
</html>
https://github.com/szaranger/szaranger.github.io/blob/master/service-workers/03/02/style.css
https://github.com/szaranger/szaranger.github.io/blob/master/service-workers/03/02/index.js
In the index.js file, we are setting a page specific name for the cache, as the caches are per origin based, and no other page should use the same cache name:
var CACHE_NAME = 'cache-and-then-network';
If you inspect the Resources tab of the development tools, you will find the cache inside the Cache Storage tab.
If we have already fetched network data, we don't want the cache fetch to complete and overwrite the data that we just got from the network. We use the networkDataReceived flag to let the cache fetch callbacks to know if a network fetch has already completed:
var networkDataReceived = false;
We are storing elapsed time for both network and cache in two variables:
var networkFetchStartTime;
var cacheFetchStartTime;
The source URL for example is pointing to a file location in GitHub via RawGit:
var SOURCE_URL = 'https://cdn.rawgit.com/szaranger/
szaranger.github.io/master/service-workers/03/02/events';
If you want to set up your own source URL, you can easily do so by creating a gist, or a repository, in GitHub, and creating a file with your data in JSON format (you don't need the .json extension). Once you've done that, copy the URL of the file, head over to https://rawgit.com, and paste the link there to obtain another link with content type header as shown in the following screenshot:
Between the time we press the Fetch button, and the completion of receiving data, we have to make sure the user doesn't change the criteria for search, or press the Fetch button again. To handle this situation, we disable the controls:
function clear() {
outlet.textContent = '';
cacheStatus.textContent = '';
networkStatus.textContent = '';
networkDataReceived = false;
}
function disableEdit(enable) {
fetchButton.disabled = enable;
cacheDelayText.disabled = enable;
cacheDisabledCheckbox.disabled = enable;
networkDelayText.disabled = enable;
networkDisabledCheckbox.disabled = enable;
if(!enable) {
clear();
}
}
The returned data will be rendered to the screen in rows:
function displayEvents(events) {
events.forEach(function(event) {
var tickets = event.ticket ?
'<a href="' + event.ticket + '" class="tickets">Tickets</a>'
: '';
outlet.innerHTML = outlet.innerHTML +
'<article>' +
'<span class="date">' + formatDate(event.date) + '</span>'
+
' <span class="title">' + event.title + '</span>' +
' <span class="venue"> - ' + event.venue + '</span> ' +
tickets +
'</article>';
});
}
Each item of the events array will be printed to the screen as rows.
The function handleFetchComplete is the callback for both the cache and the network.
If the disabled checkbox is checked, we are simulating a network error by throwing an error:
var shouldNetworkError = networkDisabledCheckbox.checked,
cloned;
if (shouldNetworkError) {
throw new Error('Network error');
}
Because of the reason that request bodies can only be read once, we have to clone the response:
cloned = response.clone();
We place the cloned response in the cache using cache.put as a key value pair. This helps subsequent cache fetches to find this update data:
caches.open(CACHE_NAME).then(function(cache) {
cache.put(SOURCE_URL, cloned); // cache.put(URL, response)
});
Now we read the response in JSON format. Also, we make sure that any in-flight cache requests will not be overwritten by the data we have just received, using the networkDataReceived flag:
response.json().then(function(data) {
displayEvents(data);
networkDataReceived = true;
});
To prevent overwriting the data we received from the network, we make sure only to update the page in case the network request has not yet returned:
result.json().then(function(data) {
if (!networkDataReceived) {
displayEvents(data);
}
});
When the user presses the fetch button, they make nearly simultaneous requests of the network and the cache for data. This happens on a page load in a real world application, instead of being the result of a user action:
fetchButton.addEventListener('click', function handleClick() {
...
}
We start by disabling any user input while the network fetch requests are initiated:
disableEdit(true);
networkStatus.textContent = 'Fetching events...';
networkFetchStartTime = Date.now();
We request data with the fetch API, with a cache busting URL, as well as a no-cache option in order to support Firefox, which hasn't implemented the caching options yet:
networkFetch = fetch(SOURCE_URL + '?cacheBuster=' + now, {
mode: 'cors',
cache: 'no-cache',
headers: headers
})
In order to simulate network delays, we wait before calling the network fetch callback. In situations where the callback errors out, we have to make sure that we reject the promise we received from the original fetch:
return new Promise(function(resolve, reject) {
setTimeout(function() {
try {
handleFetchComplete(response);
resolve();
} catch (err) {
reject(err);
}
}, networkDelay);
});
To simulate cache delays, we wait before calling the cache fetch callback. If the callback errors out, we make sure that we reject the promise we got from the original call to match:
return new Promise(function(resolve, reject) {
setTimeout(function() {
try {
handleCacheFetchComplete(response);
resolve();
} catch (err) {
reject(err);
}
}, cacheDelay);
});
The formatDate function is a helper function for us to convert the date format we receive in the response into a much more readable format on the screen:
function formatDate(date) {
var d = new Date(date),
month = (d.getMonth() + 1).toString(),
day = d.getDate().toString(),
year = d.getFullYear();
if (month.length < 2) month = '0' + month;
if (day.length < 2) day = '0' + day;
return [month, day, year].join('-');
}
If you consider a different date format, you can shuffle the position of the array in the return statement to your preferred format.
In this article, we have learned how to control cached content by creating a performance art event viewer web app.
Further resources on this subject: