Release 0.2a: #good-first-issue - Make web app work offline!

Hello, folks!

October has arrived (or at least arriving when I am writing this) which means HacktoberFEST 2019 has commenced. As many participants around the globe, I am starting my contribution to the event.

My plan for the event is simple! I will make 1 pull request for each of the 4 weeks of October. Stalling no more! I have already worked on my first contribution and I want to share with you folks about it.

Issue #1: Creating a simple Progressive Web App!

Links: Repository | Issue | Pull Request

To recap...
The web app is a simply tool for developers to know what is the size of the current viewport by opening the app on the device that they want to measure. The app is written in JavaScript and requires an improvement - offline working capability.
The app is interactive when we resize the viewport and the code for the app is very simple with only static resources.

The app's simple UIAdd caption

As I have planned in my previous post about HacktoberFEST, this is an issue I want to look into for 2 reasons:
  • I was very fortunate to learn about Progressive Web App (PWA) in a unique professional option course in college and I want to apply this useful piece of knowledge into the real world.
  • It is simply a #good-first-issue 😅. I think the solution for this issue would be simple and its implication would be significant enough to be added into the app.
Let's go and dive into the process!

Offline capability

Firstly, I must introduce the technology we use to achieve offline working capability which is the Service Worker. It is simply a JavaScript source file containing the code that intercepts and handles network requests from our app. By this interception, we can achieve endless possibility, one of which we are interested here is that we can cache those requested resources and use them later (when a network connection is not available).

Most (modern) browsers today support this technology. However, they are not enabled by default. We, as developers, have to do it ourselves. I did it by adding the highlighted snippet into the main page of the app (index.html).
<html lang="en" class="no-js">
 <head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <meta http-equiv="X-UA-Compatible" content="ie=edge" />
  <title>What is my viewport</title>
  <meta name="description" content="What Is My Viewport &mdash;; A simple online tool for quickly finding the dimensions of your current device's viewport!" />
  <link rel="stylesheet" href="css/normalize.min.css" />
  <link rel="stylesheet" href="css/main.css" />
 </head>
 <body>
                
  <script async src="js/main.js"></script>
                
  <script>
                window.addEventListener("load", async _ => {
                    if('serviceWorker' in navigator) {
                        await navigator.serviceWorker.register('sw.js');
                        console.log('[Service Worker] Registered');
                    };
                });
  </script>
 </body>
</html>
What the added script element does is registering a file called sw.js as the Sevice Worker. The registration process is only fired when the whole page has been loaded (onload event of the window object).

You will also see the condition 'serviceWorker' in navigator, the condition is to determined if the user's browser supports Service Worker technology is not (for now, literally every common browser does except Internet Explorer).

The above code is the only code that we will have outside of the sw.js file. Let's create the sw.js at the root folder of our app (Don't mind the missing files, we will get there):
Root folder structure

Inside sw.js, let's have 2 variables: cacheName which is a string and staticAssets (an array):
const cacheName = 'whatismyviewport-app';
const staticAssets = [
    './',
    './index.html',
    './favicon.ico',
    './css/main.css',
    './css/normalize.min.css',
    './js/main.js',
    './js/vendor/html5shiv.min.js',
];
While cacheName is just a name for the cache memory where we are going to store our app's files locally, staticAssets is an array of strings representing all the routes our app might have and all the name of all static files we want to cache to the local memory when the app is loaded.

Since this app is going to be a single page app so there's only the home route ("./") needs caching. Below that are all static files including JS, CSS and other assets such as images and icons.

Next, in the sw.js file, we need to instruct the Service Worker on how to cache our web app. There are a few events that are going to happy during the life cycle of the Service Worker. Since this is a simple app, we are only interested in 2 events: Install and Fetch.

Install

This event is only fired once when the Service Worker initiates. During this event, we would want to cache all the static assets into local memory first. Therefore, we add a listener for the "install" event of the Service Worker with a handler which does exactly that:
self.addEventListener('install', async _ => {
    console.log('[Service Worker] Installed');
    let cache = await caches.open(cacheName);
    console.log('[Service Worker] Caching Static Assets');
    cache.addAll(staticAssets);
});
Note that we are adding the listener to the "self" object which is the Service Worker. This is because we are in the sw.js file which has been registered as the Service Worker earlier in index.html.

At this point, if you host the app with Node.js/Express.js on localhost, turn on your browser (I am using Chrome), go to the localhost:8080 (or any other port that you used to host the app) and go to DevTools panel, Application tab, you will see that all the static files are in the browser's Cache Storage:

Chrome's DevTools caching contents

However, if you go to the Service Worker menu also in the Application tab, check on the box "Offline" to tell the browser to run the app in offline mode...

Check "Offline" checkbox to force the app to work without a network connection

...and then refresh the page, the app fails!

This is actually expected because although all files needed for the app have been cached when the app requests for those files, they still request from the network which is not available in offline mode. Therefore, we need to write extra instruction for the app to retrieve the files from Cache Storage. This brings us to the next event of the Service Worker.

Fetch

This event is fired and intercepts a request whenever it is made from the client app. In the handler of this event is where we can define our strategy for our so-called Progressive Web App.

There are a few common strategies for caching our app, however, for simplicity and because of the simplicity of our app, we can go with "cache only" approach.

In this strategy, we only cache the app the first (few) time the client loads it. After the first time, we will check if the app has been cached. If so, we will load the app from the Cache Storage. To achieve that, we need to add a listener for the "fetch" event as shown below:
self.addEventListener('fetch', event => {
    event.respondWith(
        async function() {
            let cachedResponse = await caches.match(event.request);
            if (cachedResponse) {
                console.log('[Service Worker] Fetching cached assets');
                return cachedResponse;
            }
            return await fetch(event.request);
        }()
    );
});
The code snippet relatively means that whenever there is a request to the network, if the request has been cached before, instead of getting a response from the network, get the response from Cache Storage. If it has not been cached before, make a request as normal and return the response from the network.

With the 2 listeners for the Install and Fetch events, the app should be capable of working offline by now. You can test it by disabling the network on your device or checking the "Offline" checkbox I mentioned above and refreshing the page.

Installability

As our app now is working-offline capable but it still relies on the browser to run, why don't we leverage this a bit more so the app can be installed as a standalone app? Yes! This is 100% achievable with a little bit more work. By doing this, we will make our app become a true Progressive Web App.

Most modern browsers have done a lot of heavy lifting work for us when it comes to building a PWA. To enable installability for the app, we essentially just need 1 extra file: manifest.json. Let's create one with the following content and place it in the root folder, alongside side sw.js.
{
    "name": "What Is My Viewport",
    "short_name": "whatismyviewport",
    "icons": [{
      "src": "icons/icon192.png",
        "sizes": "192x192",
        "type": "image/png"
      },
      {
        "src": "icons/icon512.png",
        "sizes": "512x512",
        "type": "image/png"
      },
      {
        "src": "icons/icon144.png",
        "sizes": "144x144",
        "type": "image/png"
      }
    ],
    "start_url": "./index.html",
    "display": "standalone",
    "background_color": "#2a2a2a",
    "theme_color": "hsla(27, 93%, 58%, 1)"
  }
The manifest.json is a configuration file that helps the browser gather extra information needed to compile a standalone app. Some important field in the file:
  • name or short_name: At least one must be presented. They are the name of the app, short_name is used when there is a space restriction.
  • icons: An array of objects identifying the icon for the app in different sizes. On Chrome, at least 2 icons of size 192x192 and 512x512 are needed. The browser will scale the icon for other sizes.
  • start_url: This field is mandatory and tells the browser where to start the app.
  • display: Mode of display, must be Standalone to be installable to the home screen.
  • theme_color: Browser's theme color when the app is opened. Mobile devices only.
  • background_color: Color of the loading screen of the app. Mobile devices only.
Finally, add a link to the manifest.json in the index.html for the browser to find it:
<html lang="en" class="no-js">
 <head>
            <meta charset="UTF-8" />
            <meta name="viewport" content="width=device-width, initial-scale=1.0" />
            <meta http-equiv="X-UA-Compatible" content="ie=edge" />
            <title>What is my viewport</title>
            <meta name="description" content="What Is My Viewport &mdash;; A simple online tool for quickly finding the dimensions of your current device's viewport!" />
            <link rel="stylesheet" href="css/normalize.min.css" />
            <link rel="stylesheet" href="css/main.css" />
            <link rel="manifest" href="manifest.json">
 </head>
 <body>
            
 </body>
</html>

Result

I have hosted the result of this simple PWA on Github Page here:
https://vitokhangnguyen.github.io/whatismyviewport/
 The site is capable of working offline in the browser. It is also installable as a standalone app.

On the Desktop browser, if you look at the end of the address bar, there should be a button on which if you click, it will prompt you to "Install the app".

Chrome prompts the user to Install the PWA

If you are on the phone, the browser should also prompt you to "Add to home screen":

Chrome on mobile prompts the user to install the PWA

After Installation or "Add to home screen", the app should present itself as a shortcut on the Desktop or application on the mobile device's Home Screen. The view on the app installed on the devices should be the same as its view on the browser.

And there we go! We made a web application not only works offline but also works as a standalone Desktop or mobile application!

Comments

Popular posts from this blog

discord.js - A powerful library for interacting with Discord API

My Lightweight Noteboard 1.0