Service Workers
Maybe you heard about progressive web applications, service workers, offline websites and stuff like that.
Future is coming and the Service Workers will for sure lead a big role on that but, unfortunately, there is still a lot of confusion around.
So, my goal for this article is to provide you with a clear idea about what a service worker is; what is not; why we will inevitably start using them sooner or later and, then, how to start using a Service Worker right now.
What a Service Worker is
A Service Worker AKA sw.js
is just a JavaScript file running in your website
background.
So, what’s the difference between a Service Worker and a normal JavaScript file you import in your HTML document?
Well, in theory there is absolutely no difference at all. A Service Worker is and will alway be just a JavaScript file. However the way Browsers interact with a Service Worker is completely revolutionary.
When you define a Service Worker in your app, your browser loads and keeps that script in the Browser’s background ready to be used by the app at any time and. Once loaded, it will be always available even without the presence of an Internet connection.
Ok, before we continue. Just have a quick look at:
What a Service Worker is not
Service Workers are not used for storing frameworks or libraries like Angular or jQuery, also they are completely DOM blinded which means that they cannot directly interact with your DOM.
Trying to access the document
object inside a Service Worker
will result in an error because that document
will just return undefined
.
Ok, now you’re probably confused because we said we can use Service Workers for
running JavaScript code in the background and they can work without Internet
connection, however they cannot interact with the DOM so we cannot simply
minify all our JavaScript code inside a sw.js
file.
So, what’s the point?
The power behind a Service Worker is another. In fact they can have access to your browser’s cache storage meaning that they are capable of storing all your files inside the browser and serve your application with them in order to increasing the rendering process and also, to give your app an offline support.
HTTPS only
Before we proceed is important to highlight the biggest restriction on Service Workers.
Because this service runs in your browser’s background, using Service Workers from unsafe websites can be extremely dangerous as they can hijack connections, respond differently, & filter responses.
To avoid man-in-the-middle attacks, you can only register for Service Workers on pages served over HTTPS, so we know the Service Worker the browser receives hasn’t been tampered with during its journey through the network.
Github Pages like Heroku are served over HTTPS, so they’re a great place to host demos.
Also, you can still use Service Workers on your development environment
as they accept to be served from localhost
without any extra configuration.
Let’s build our first Service Worker step-by-step
Ok then, we spoke a lot about them but we haven’t seen any single line of code so far. So let’s begin defining the Service Workers lifecycle.
Registration
In order to start using a Service Worker, you need first to register your script as a Service Worker because, as we said before, this is just a normal JavaScript file so your browser is not able to automatically identify a Service Worker by itself.
To tell the browser where your Service Worker JavaScript file lives, you need to
use the navigator service. inside your index.html
body file, create
the following script:
<script>navigator.serviceWorker.register('/sw.js')</script>
However, there is a proper installation method you should follow in order to handle errors and provide a proper cross-browser support.
A bullet-proof register process is just a wrapper around the previous registration one but is alway best practice to follow a clear path in order to avoid errors.
So here is a standard Service Worker registration process.
if ('serviceWorker' in navigator) {
navigator.serviceWorker
.register('/sw.js')
.then(function(registration) {
// Registration was successful
console.log('ServiceWorker registered with scope: ', registration.scope);
})
.catch(function(err) {
// registration failed :(
console.log('ServiceWorker registration failed: ', err);
});
}
Right, but why we need all those lines of code?
First of all, Service Workers are still not supported by all the browsers so we need to make sure that our final user’s browser knows what a Service Worker is.
This is covered in very first line of code:
if ('serviceWorker' in navigator) {
You will then probably want to include a polyfill to make sure all the browser can recognize this service. We are not going to cover that part in here but you can have access to all the resources you need from this link
Let’s move to the next line. Service Workers make a massive use of promises (if you are not confident with this concept, we highly recommend you to have a look at this article before proceeding).
navigator.serviceWorker
.register('/sw.js')
.then(function(registration) {
// Registration was successful
console.log('ServiceWorker registered with scope: ', registration.scope);
})
As soon as the registration process success, a Service Worker will available
a scope referring by default to your website address. So, if you launch
this code from your localhost environment you will see in the console
message that your Service Worker scope will be in fact localhost
.
If you want to specify a different scope for your Sevice Worker, you can manually pass a scope property during the registration method in this way:
navigator.serviceWorker
.register('/worker.js', {
scope: '/my-scope/'
})
.then(function(registration) {
// Registration was successful
console.log('ServiceWorker registered with scope: ', registration.scope);
}, function(err) {
// registration failed :(
console.log('ServiceWorker registration failed: ', err);
});
So, this time, your Service Worker scope will reside inside localhost/my-scope
If we register the service worker file at /my-scope/sw.js, then the service worker would only see fetch events for pages whose URL starts with /my-scope/ (i.e. localhost/my-scope/page1/, mywebsite.com/my-scope/page2/).
Installation
For the installation process you need to create a sw.js
file on the same
folder where your index.html
file resides. Then we are ready to write
our first event listener inside:
self.addEventListener('install', function(event) {
console.log('Service Worker Installed!');
});
This will just being called during the previous registration process. What we need to do now is creating a caching bucket where storing all the files we want to store for an offline access.
We can then define an array of files we want our Service Worker to cache alongside the installation process:
var urlsToCache = [
'/',
'/styles/main.css',
'/scripts/app.js',
'/images/background.png'
];
and pass inside the inside installation function like this:
self.addEventListener('install', function(event) {
event.waitUntil(
caches
.open('myCache')
.then(function(cache) {
return cache.addAll(urlsToCache);
})
);
});
This code will create a new cache object named myCache
and will store all
the files listed in the urlsToCache
array.
Fetch
Now that we’ve installed a Service Worker and instructed to store a bunch of files inside the browser’s cache, it’s time for returning back these files.
After a service worker is installed and the user navigates to a different page or refreshes, the service worker will begin to receive fetch events. Basically, our Service Worker will act like a Proxy to intercept any HTTP request our app will make.
So, we can simply intercept those fetch
calls and immediately respond with
our cached files where we have one already.
Let’s start with defining another eventListener inside our sw.js file then:
self.addEventListener('fetch', function(event) {
});
Now we need to identify the name of the file the application is trying to reach and, in case we already have that in our cache, send that back instead of performing a request outside.
self.addEventListener('fetch', function(event) {
event.respondWith(
caches
.match(event.request)
.then(function(response) {
return response || fetch(e.request);
});
);
});
caches.match()
evaluates the web request that triggered the fetch event,
and checks to see if it’s available in the cache.
It then either responds with the cached version, or uses fetch to get a copy
from the network.
Finally, the response is passed back to the web page with event.respondWith()
.
Demo project
A GitHub repository showing how to create a Service Worker in a real project is available for you at this link.
I really hope you found this article helpful and I’m looking forward to receiving your feedbacks!