Building Progressive Web Applications

technology - 05 Jun 2017
Aman Raj

What are progressive web applications?

Progressive web applications are web applications with rich user experience. They make smooth user interaction with web and have following features :

  • Progressive - Works for any user independent of browser choice.
  • Instant Loading - Loads instantly without displaying those annoying loading icons.
  • Fast - Responds quickly to the user request without making user to wait forever.
  • Connectivity Independent - Work offline or on low quality networks. Either displays a proper offline message or cached data.

Force that drives progressive web applications

The core concept behind progressive web app is service worker. Service wroker is a script which browser runs in background without interfering the user’s interaction with the application. Some of the work done by this script include push notification, content caching,data synch when internet connection is available.

There are 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 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 it’s scope. We have defined charcha-serviceworker.js at / i.e at root domain. That means our service worker will listen for all request which are made at this domain and it’s sub domains. It’s all upto 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 efined 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.

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 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 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 root domain of your application. In our case we created this file at 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 adidng 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 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 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 install event.
  2. Opens a cache with the name charcha.
  3. Adds 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, 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 later phase which is update phase of service worker.

Rerun your application and open chrome dev tools. Open Service Workers pane in Application panel. You will find your service worker with running status. You should see something like below to make sure that service worker is successfully installed.

service worker installed

You can also find 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 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 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 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 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 above code snippet, we are trying to fetch the requested resource from list of cached reources. If it finds, resource is returned from the cache, otherwise we make a network request to fetch the resource and return it.

In previous section we saw an issue of caching dynamic urls. Let’s see how we can resolve that inside fetch event 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.

  1. If requested resource is available in cache, a cache hit occurs and resource is returned from cache.
  2. If cache hit does not occur with requested resource (which will be case with dynamic urls), a fresh fetch request is made to fetch the resource from network.
  3. If response it not valid, status is not 200 or response type is not basic, response is returned because error responses need not to be cached.
  4. If response is valid, status is 200 and response type is basic, this response is first saved into 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 and can be consumed only one. In our code we are consuming request/response for both caching and calling a fetch event. Once read, stream is gone and will not be available for next read. In order to get fresh request/response stream again we clone the request/response.

Now with above fetch event listener, we are sure that each and every new requested resource is cached and returned when request is made.

At this stage we are good to go and test offline running capability of our application. To test it follow these steps:

  1. Rerun the application and browse through 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 waiting state.
service worker waiting
  • 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, 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 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

Your django web app can be made work faster, more engaging with users and work offline. You just need to have support for sevice worker’s in your target browser.

There is more of 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.

Contact Us

Learn more about our Services and Solutions