05 Jun 2017
Building Progressive Web Applications
   
Aman Raj
#Django | 11 Min Read
With, Progressive Web Applications (PWAs), developers can deliver amazing app-like experiences to users using modern web technologies. It is an emerging technology designed to deliver functionalities like push notifications, working offline etc to deliver a rich mobile experience.In this post, we will learn how to make your Django application progressive that works offline.
What are progressive web applications?
Progressive web applications are web applications with the rich user experience. They make smooth user interaction with web and have the following features :
  1. Progressive – Works for any user independent of browser choice.
  2. Instant Loading – Loads instantly without displaying those annoying loading icons.
  3. Fast – Responds quickly to the user request without making the user wait forever.
  4. Connectivity Independent – Work offline or on low-quality networks. Either displays a proper offline message or cached data.
A force that drives progressive web applications
The core concept behind progressive web app is service worker. Service worker is a script which browser runs in the background without interfering with the user’s interaction with the application. Some of the work done by this script include push notification, content caching, data synch when the internet connection is available.
There are a lot more about progressive web applications. But in this post, we are more concerned with connectivity independent feature. We will learn how service workers help us to get the things done. This post is going to be about making a Django application work offline. We will use charcha as our reference application.
Let’s get started without any further delay.
Register a service worker
To bring service worker in the loop, we need to register it in our application. Basically, it tells the browser where it needs to look for the service worker script. Put the following code snippet in your javascript file, it gets loaded as soon as your application runs.
var serviceWorkerPath = "/charcha-serviceworker.js";
 var charchaServiceWorker = registerServiceWorker(serviceWorkerPath);
 function registerServiceWorker(serviceWorkerPath){
    if('serviceWorker' in navigator){
      navigator.serviceWorker
        .register(serviceWorkerPath)
          .then(
            function(reg){
              console.log('charcha service worker registered');
            }
          ).catch(function(error){
            console.log(error)
          });
    }
 }
How does the above code work?
  • If there is browser support for service worker service worker at location /charcha-serviceworker.js is registered. If your browser doesn’t support service worker, then your web application runs normally as if there was no service worker at all.
  • Path of service worker defines its scope. We have defined charcha-serviceworker.js at / i.e at root domain. That means our service worker will listen for all request which is made at this domain and it’s subdomains.
  • It’s all up to you where you want your service worker’s scope to be. In our case we want each and every request to our application to be intercepted, that’s why we have defined root domain scope for our service worker.
If service worker is not successfully registered, you see something like following error in your console. TypeError: Failed to register a ServiceWorker: A bad HTTP response code (404) was received when fetching the script.
Launching your Django application
Now go ahead and launch your Django application. Open the browser console and if you are able to see the above-mentioned error, that means you should be grateful to your browser for having service worker support. Okay! that being said, we encountered this error, which means something is wrong. Reload your application and you will see a detailed error message as mentioned below service worker registration failure
This is because Django could not find anything to render for request /charcha-serviceworker.js.
Our next step is to define a URL path and call a view for this path.
Open url.py file and add this url in url pattern list.
url(r'^charcha-serviceworker(.*.js)$', views.charcha_serviceworker, name='charcha_serviceworker'),
Open views.py and write a view function which responds to a browser’s request at /charcha-serviceworker.js
 def charcha_serviceworker(request, js):
      template = get_template('charcha-serviceworker.js')
      html = template.render()
      return HttpResponse(html, content_type="application/x-javascript")
what we are doing above is, just returning our Charcha-serviceworker.js whenever a request is made to download this file.
  • Create an empty service worker script in the root domain of your application. In our case, we created this file at the project root directory as [PROJECT_ROOT]/charcha-serviceworker.js.
  • Open application settings file and inside TEMPLATES add application root path as DIRS value along with existing templates path. After adding, it should look like
TEMPLATES = [
    {
        ...
        'DIRS': [os.path.join(PROJECT_ROOT, 'templates'), os.path.join(PROJECT_ROOT, ' '),],
        ...
        ...
    },
]
Notice os.path.join(PROJECT_ROOT, ‘templates’) was existing template path and os.path.join(PROJECT_ROOT, ‘ ‘) was added and hence enabling get_template function to search for script file in the project root directory.
Now re run your application and if you do not see the previous error, that means you are good to go with writing service worker script.
Install the service worker
When a service worker is registered, an install event is triggered the very first time an user visits the application page. This is the place where we cache all the resources needed for our application to work offline.
Open the previously created charcha-serviceworker.js and define a callback function which listens for install event.
var cacheName = 'charcha';
var filesToCache = [
'/',
];
self.addEventListener('install', function(event) {
  event.waitUntil(
    caches.open(cacheName).then(function(cache) {
      return cache.addAll(filesToCache);
    })
  );
});
Let us see what’s going on here
  1. It adds an eventListener which listens for the install event.
  2. Opens a cache with the name charcha.
  3. Adds an array of files which need to be cached and pass it to cache.addAll() function.
  4. event.waitUntil waits untill the caching promise is resolved or rejected.
  5. Service worker is considered successfully installed only when it caches all the files successfully. If it fails to download and cache even a single file, the installation fails and browser throws away this service worker.
Install occurs only once per service worker unless you update and modify the script. In that case, a fresh new service worker will be created without altering the existing running one. We will discuss it more in a later phase which is update phase of a service worker.
Rerun your application and open chrome dev tools. Open Service Workers pane in the Application panel. You will find your service worker with running status. It will show something like below to make sure that the service worker is successfully installed.
service worker installed
You will see a list of cached file in Cache Storage pane under Cache in same Application panel.
NOTE : We can add all the static resources like css files, image files in the list of files to be cached. But dynamic url like /discuss/{post_id} cannot be defined in this list. So how to cache wildcard urls. We will see it in the next section.
Intercept the request and return cached resources
Since we have installed our service worker , we are ready to roll and return responses from our cached resources. Once the service worker is installed, every new request triggers a ‘fetch’ event. We can write a function which listen for this event and responds with the cached version of the requested resource. Our fetch event listener is going to look like
self.addEventListener('fetch', function(event) {
  event.respondWith(
    caches.match(event.request).then(function(response) {
      return response || fetch(event.request);
    })
  );
});
Through the above code snippet, we are trying to fetch the requested resource from the list of cached reources. If it finds, the resource is returned from the cache, otherwise, we make a network request to fetch the resource and return it.
In the previous section, we saw an issue of caching dynamic urls. Let’s see how we can resolve that inside fetchevent listener. Modify the previous code for fetch event to look like
self.addEventListener('fetch', function(event) {
  event.respondWith(
    caches.match(event.request)
      .then(function(response) {
        if (response) {
          return response;
        }
        var fetchRequest = event.request.clone();
        return fetch(fetchRequest).then(
          function(response) {
            if(!response || response.status !== 200 || response.type !== 'basic') {
              return response;
            }
            var responseToCache = response.clone();
            caches.open(cacheName)
              .then(function(cache) {
                cache.put(event.request, responseToCache);
              });
            return response;
          }
        );
      })
    );
});
Let’s walk through the code to see how it solves the aforementioned issue.
How the code solves the aforementioned issue
  1. Only if the requested resource is available in the cache, a cache hit occurs and resource is returned from cache.
  2. In case the cache hit does not occur with the requested resource (which will be the case with dynamic urls), a fresh fetch request is made to fetch the resource from the network.
  3. If the response is not valid, status is not 200 or response type is not basic, the response is returned because error responses need not to be cached.
  4. In case the response is valid, status is 200 and response type is basic, this response is first saved into a cached list and then returned to the client.
You might wonder why are we clonning request and response. Basically, a web request/response is a stream. It can be consumed by only one. In our code, we are consuming request/response for both caching and calling a fetch event. Once read, the stream is gone and will not be available for next read. In order to get a fresh request/response stream again, we clone the request/response.
Now with the above fetch event listener, we are sure that each and every new requested resource is cached and returned when a request is made. At this stage, we are good to go and test the offline running capability of our application. To test it follow these steps:
  1. Rerun the application and browse through a few pages.
  2. Open chrome dev tools and open Service Workers pane in Application panel.
  3. Enable offline check box. After enabling it, you will see a yellow warning icon. This indicates that your application has gone offline.
  4. Now go and browse through all those pages which you previously visited and were cached. You will be able to access those pages without any network error.
Activate the service worker
When you make even a small change in your service worker, it is detected by your browser and your browser downloads and install it.
The updated service worker is launched along side the existing one. Even if it is sucessfully installed, it waits untill the existing service worker has relinquished it’s control from all the pages. If you open service workers pane in Application panel of chrome dev tools, you see new service worker in the waiting state.
Most common use case of activate callback is cache management. When new service worker takes control over your application, you would want to clear the old cache created by old service worker. To do this take a look at following code
self.addEventListener('activate', function(e) {
  e.waitUntil(
    caches.keys().then(function(keyList) {
      return Promise.all(keyList.map(function(key) {
        if (key !== cacheName) {
          return caches.delete(key);
        }
      }));
    })
  );
});
As mentioned previously, the new service worker doesn’t take control over your application immediately. To force new service worker to take over your application you could do any one of the following.
  1. Click on the skipWaiting on the left side of your waiting service worker. (view it in the above image)
  2. You can add self.skipWaiting() in install event listener. When skipWaiting() is called from a installing service worker, it will skip the waiting state and immediately activate.
Now you can Go Gaga over it because you have managed to make your django application work offline.
Summary
You can make Django web apps work faster, more engaging and also work offline. You just need to have support for sevice worker’s in your target browser.
There is more of a progressive web app and service worker. Hang around because in further posts we will discuss
  • How to cache all the pages of your application without browsing each and every page of your application.
  • How to handle post request in offline django application with push and sync event listeners.