Add Service Worker to Django

Table of Contents

Django PWA Tutorial Series:

  1. Introduction
  2. Add Web app manifest to Django
  3. Add Service Worker to Django
  4. Fallback Offline Page in Django
  5. Caching and Routing (coming soon)
  6. How to add install button to PWA (coming soon)
  7. Send Web Push Notification from Django (part 1) (coming soon)
  8. Send Web Push Notification from Django (part 2) (coming soon)

The source code is available on https://github.com/AccordBox/django-pwa-demo

Objective

By the end of this chapter, you should be able to:

  1. Understand what is Service Worker, and the life cycle.
  2. Learn to develop Service Worker with Workbox packages.
  3. Serve Service Worker in Django.

Basic concepts

A service worker is a type of web worker. It's essentially a JavaScript file that runs separately from the main browser thread, intercepting network requests, caching or retrieving resources from the cache, and delivering push messages.

Because workers run separately from the main thread, service workers are independent of the application they are associated with.

Service worker lifecycle:

A service worker goes through three steps in its lifecycle:

  • Registration
  • Installation
  • Activation

Scope

The scope of the service worker determines which files the service worker controls, in other words, from which path the service worker will intercept requests. The default scope is the location of the service worker file, and extends to all directories below. So if service-worker.js is located in the root directory, the service worker will control requests from all files at this domain.

As you know, Django developers usually put JS files under /static/js path. To resolve this issue, we should create a Django view for /sw.js and return content of /static/js/sw.js back.

Vanilla js

I have seen some people and online resource teaching people to write Service worker in vanilla js.

However, this is not the best way.

As I said, frontend ecosystem is very great now. We can use other package to help us get things done in easier and better way.

Workbox

Workbox is a set of libraries that can power a production-ready service worker for your Progressive Web App.

Let's first install workbox-webpack-plugin to our frontend project.

$ cd frontend
$ npm install workbox-webpack-plugin

Update frontend/webpack/webpack.common.js

const {InjectManifest} = require('workbox-webpack-plugin');

plugins: [
  ...
  new InjectManifest({
    swSrc: './src/sw.js',
  }),
],

Notes:

  1. We add const {InjectManifest} = require('workbox-webpack-plugin'); at the top.
  2. We add InjectManifest to the plugins, and set the service worker source file path.
  3. With injectManifest, we have more control of the Service Worker and we will do more things in the next chapters.

Let's create frontend/src/sw.js

const manifest = self.__WB_MANIFEST;
if (manifest) {
  // do nothing
}

self.addEventListener('install', function(event) {
  console.log('service worker install');
});

self.addEventListener('activate', function(event) {
  console.log('service worker activate');
});

self.addEventListener('fetch', event => {
  console.log(`fetch ${event.request.url}`);
});

Notes:

  1. I will talk about const manifest = self.__WB_MANIFEST; later, now we can ignore it.
  2. We added event handler to install, activate and fetch.
$ cd frontend
$ npm run watch

We can ignore the warning below:

WARNING in InjectManifest has been called multiple times, perhaps due to running webpack in --watch mode. The precache manifest generated after the first call may be inaccurate! Please see https://github.com/GoogleChrome/workbox/issues/1790 for more information.

This is the directory structure

build
├── assets
│       # files omitted for brevity
├── css
│       # files omitted for brevity
├── index.html
├── js
│       # files omitted for brevity
├── manifest.json
├── sw.js                    # new

Serve Service Worker

Next, we will use Django view to serve the frontend/build/sw.js.

Create django_pwa_app/views.py

from django.conf import settings
from django.http import HttpResponse


def service_worker(request):
    sw_path = settings.BASE_DIR / "frontend/build" / "sw.js"
    response = HttpResponse(open(sw_path).read(), content_type='application/javascript')
    return response
  1. The logic is very simple, we read the content of the frontend/build/sw.js and return it back.

Update django_pwa_app/urls.py

from django.contrib import admin
from django.urls import path
from django.views.generic import TemplateView
import django_pwa_app.views

urlpatterns = [
    path('', TemplateView.as_view(template_name="index.html")),
    path('sw.js', django_pwa_app.views.service_worker),                 # new
    path('admin/', admin.site.urls),
]

Let's run django server and do a test

(env)$ python manage.py runserver
# check on http://127.0.0.1:8000/sw.js

Register Service Worker

Update frontend/src/application/app.js

import "../styles/index.scss";

import "bootstrap/dist/js/bootstrap.bundle";

if ('serviceWorker' in navigator) {
  window.addEventListener('load', () => {
    navigator.serviceWorker.register('/sw.js').then(registration => {
      console.log('SW registration succeeded:', registration);
      navigator.serviceWorker.ready
        .then(function (registration) {
          console.log('SW is active:', registration.active);


        });
    }).catch(registrationError => {
      console.log('SW registration failed: ', registrationError);
    });
  });
}
  1. 'serviceWorker' in navigator is to check if the browser supports service worker
  2. navigator.serviceWorker.register is to register the service worker
  3. navigator.serviceWorker.ready is to make sure the service worker is active now.

First Load

Now, let's visit http://127.0.0.1:8000/ for the first time.

If we open the devtools, we will see some messages in the console.

sw.js:7 service worker install
app.js:7 SW registration succeeded: ServiceWorkerRegistration {installing: ServiceWorker, waiting: null, active: null, navigationPreload: NavigationPreloadManager, scope: "http://127.0.0.1:8000/", …}
sw.js:10 service worker activate
app.js:9 SW is active: ServiceWorker {scriptURL: "http://127.0.0.1:8000/sw.js", state: "activating", onstatechange: null, onerror: null}
  1. activate event is fired after the install event.

Second Load

Now let's refresh the page and check the console again.

sw.js:13 fetch http://127.0.0.1:8000/static/css/app.css
sw.js:13 fetch http://127.0.0.1:8000/static/js/runtime.js
sw.js:13 fetch http://127.0.0.1:8000/static/js/vendors-node_modules_bootstrap_dist_js_bootstrap_bundle_js.js
sw.js:13 fetch http://127.0.0.1:8000/static/js/app.js
app.js:7 SW registration succeeded: ServiceWorkerRegistration {installing: null, waiting: null, active: ServiceWorker, navigationPreload: NavigationPreloadManager, scope: "http://127.0.0.1:8000/", …}
sw.js:13 fetch http://127.0.0.1:8000/static/assets/manifest.json
app.js:9 SW is active: ServiceWorker {scriptURL: "http://127.0.0.1:8000/sw.js", state: "activated", onstatechange: null, onerror: null}
sw.js:13 fetch http://127.0.0.1:8000/static/assets/android-chrome-144x144.png

Notes:

  1. A fetch event fires every time any resource controlled by a service worker is fetched, which includes the documents inside the scope
  2. The install and activate event are not fired this time because the service worker is already running

Update Service Worker

Please open chrome devtools, go to Application tab and click the Service Worker on the left sidebar.

  1. We can see the service worker is active and running.
  2. And we can click the top right link to Update or Unregister the service worker.

There will be a point in time where your service worker will need updating. When that time comes, you'll need to follow these steps:

  1. Update your service worker JavaScript file. When the user navigates to your site, the browser tries to redownload the script file that defined the service worker in the background. If there is even a byte's difference in the service worker file compared to what it currently has, it considers it new.
  2. Your new service worker will be started and the install event will be fired.
  3. At this point the old service worker is still controlling the current pages so the new service worker will enter a waiting state.
  4. When the currently open pages of your site are closed, the old service worker will be killed and the new service worker will take control.
  5. Once your new service worker takes control, its activate event will be fired.

You can check Update a service worker to learn more.

During the development, we can click the Update on realod checkbox, this can force the browser always install the latest service worker

Notes:

  1. Service worker can only work on localhost or HTTPS.
  2. If you want to test HTTPS on local env, please check ngrok
  3. It is not recommended test SW in the incognitoe window.

Please check Service worker lifecycle if you want to know more.

Django PWA Tutorial Series:

  1. Introduction
  2. Add Web app manifest to Django
  3. Add Service Worker to Django
  4. Fallback Offline Page in Django
  5. Caching and Routing (coming soon)
  6. How to add install button to PWA (coming soon)
  7. Send Web Push Notification from Django (part 1) (coming soon)
  8. Send Web Push Notification from Django (part 2) (coming soon)

The source code is available on https://github.com/AccordBox/django-pwa-demo

Launch Products Faster with Django

Unlock the power of Django combined with Hotwire through SaaS Hammer. Supercharge productivity, tap into Python's rich ecosystem, and focus on perfecting your product!

Michael Yin

Michael Yin

Michael is a Full Stack Developer who loves writing code, tutorials about Django, and modern frontend techs.

He has published some tech course on testdriven.io and ebooks on leanpub.

© 2024 SaaS Hammer