How to load a Service Worker in Django

Loading a Service Worker in Django might be tricky. Let's see how to make it work in this brief post!

How to load a service worker in Django

A service worker, part of the family of web workers, is, to put it shortly, a particular type of JavaScript code which can run off the main thread of execution.

This has a number of benefits. In particular, service workers make possible to augment web applications with offline capabilities, and fine-grained cache management.

In this brief post we see how to load a service worker in Django.

The shortest introduction ever to service workers

To put it simply, a service worker is like a proxy sitting in between the network and the web application.

How to use a service worker? In its simplest form, we can load a service worker from any web page of the application, as in the following example:

// Place this preferably in your app entry point:
if ("serviceWorker" in navigator) {
    window.addEventListener("load", () => {
        navigator.serviceWorker
            .register("/service-worker.js")
            .then(registration =>
                console.log("Service worker registered", registration)
            )
            .catch(err => console.log(err));
    });
}

Here we check that the browser supports service workers, and we wait for the load DOM event before triggering the service worker registration:

//
navigator.serviceWorker
    .register("/service-worker.js") //

This snippet is really important, and we can notice that we load our service worker from the root of the application:

"/service-worker.js"

To understand why this is important, we need to talk a bit about the service worker scope.

Understanding the service worker scope

Service workers are incredibly powerful. They can intercept Fetch requests, and respond back to the page with anything they like.

Consider the following example:

// service-worker.js
self.addEventListener("fetch", event => {
    if (event.request.url.includes("somewhere")) {
        event.respondWith(new Response("<h1>Some response</h1>"));
    }
});

Here, in the service worker file, service-worker.js, we listen for the fetch event, to which the service worker has access, and if the request includes the string somewhere we respond with an arbitrary piece of HTML.

With a registered service worker we can return virtually anything to the web page.

For this reason, the browser enforces a strict policy when it comes to registering a service worker:

  • a service worker follows the same-origin policy
  • a service worker can operate only in a limited scope, and the scope cannot be widened at will

What does it mean? Let's take a look at this example:

//
navigator.serviceWorker
    .register("/a-folder/service-worker.js") //

A service worker loaded from /a-folder/service-worker.js will have a scope of origin:/a-folder. That is, it will be able to intercept only those requests originating from this origin/folder pair.

For example, a Fetch request originating from https://my-domain.com/a-folder/a-page.html will be intercepted by the service worker.

Instead, a Fetch request originating from https://my-domain.com/another-folder/another-page.html will not be intercepted by the service worker loaded from /a-folder/service-worker.js.

There is no way to widen the scope of a service worker. The following example won't work:

navigator.serviceWorker
    .register("/a-folder/service-worker.js", {
        scope: "/"
    })

A service worker loaded from /a-folder/ can't elevate its scope. On the other hand, we can restrict the scope of a service worker. For example:

navigator.serviceWorker
    .register("/a-folder/service-worker.js", {
        scope: "/a-folder/sub-folder"
    })

Since in most cases we want to intercept everything with our service worker to provide offline capabilities to our app, it makes sense to load the service worker with the widest scope possible, as in our original example:

//
navigator.serviceWorker
    .register("/service-worker.js") //

Given this requirement, how can we load such file in Django?

In Django, loading a static file from the root of our project is not that simple, but we can use two tools, depending on the situation, to make this work.

Let's see.

How to load a Service Worker in Django with the widest scope possible

Let's imagine we have a Django project running at https://my-project.com, and we want to load a service worker from the root of this website.

As we said at the beginning of this post, the registration process can happen in any page of the website. For example, we might have a <script> block in a Django template, in any sub-app of the project:

{# This can be any Django template block loaded from an app #}
<script>
    if ('serviceWorker' in navigator) {
        window.addEventListener('load', () => {
            navigator.serviceWorker
                    .register('/service-worker.js')
                    .then(registration =>
                            console.log('Service worker registered', registration)
                    )
                    .catch(err => console.log(err))
        })
    }

    const button = document.getElementById('fetch')
    button.addEventListener('click',()=> {
        fetch('api/example').then(res=>res.json()).then(json=>console.log(json))
    })
</script>

Let's also imagine that the project has a root_files folder from which we want to load the service worker.

We have two options.

When Django is behind Nginx, we can easily solve the issue by pointing a location block to an arbitrary path on the filesystem, as in the following example:

...
location /service-worker.js {
    alias /home/user/django_project/root_files/service-worker.js;
}
...

When the user loads the page where the service worker registration is declared, the process will kick in, and the service worker will be loaded correctly from https://my-project.com/service-worker.js.

Instead, in all those situations where Nginx is not available, we can use Whitenoise.

After installing and enabling Whitenoise, we can declare a configuration named WHITENOISE_ROOT:

WHITENOISE_ROOT = 'root_files'

This will make accessible any file present in root_files at the root of our domain.

This is ideal when we need to load a Service Worker in Django with the widest scope possible.

By doing so, the service worker file will respond correctly at https://my-project.com/service-worker.js, and the registration process will kick in.

Thanks for reading!

Further resources

Valentino Gagliardi

Hi! I'm Valentino! I'm a freelance consultant with a wealth of experience in the IT industry. I spent the last years as a frontend consultant, providing advice and help, coaching and training on JavaScript, testing, and software development. Let's get in touch!

More from the blog: