Web development has evolved significantly over the years. It allows developers to deploy websites or Web applications and serve millions of people around the world in minutes. With a browser, the user can enter a URL to access the Web application. With the advent of Progressive Web Apps, developers can use modern Web technologies to provide users with well-experienced applications. In this article, you will learn how to build an offline Progressive Web app, called PWA.

First, what is PWA

Although many articles have said this, please skip this step if you already understand. PWA is basically a website built using modern Web technology, but feels like a mobile app. In 2015, Google engineers Alex Russell and Frances Berriman created Progressive Web Apps. Since then, Google has been working to make PWA feel like a native app. A typical PWA would look like this:

1. Access the Web browser in the address bar

2. There is the option to display additions to the device’s home screen

Gradually start showing application properties such as offline usage, push notifications, and background synchronization.

So far, mobile apps can do a lot of things that Web apps can’t really do. PWA, a Web app, has been trying to do mobile apps for a long time. It combines the best of Web technology with the best of app technology to quickly load over slow Internet connections, browse offline, push messages, and load Web application entries on a Web screen.

By now, the best version of Chrome on Android has the ability to quickly open your Web app on your desktop, thanks to PWA, as shown below

The characteristics of the WPA

This new class of Web applications has features that define their existence. Here are some of the features of PWA:

  • Ui can be Responsive to multiple terminals, desktops, mobile phones, tablets and so on

  • App-like: When interacting with a PWA, it should feel like a native application.

  • Connectivity Independent: It can be accessed offline (through Service Workers) or at low speeds

  • Re-engageable: With features like push notifications, users should be able to continuously engage and reuse the application.

  • Installable: Users can add it to the home screen and launch it from there so they can reapply the application.

  • Discoverable: something that should be identified by the user through a search

  • Fresh(Latest data) : Users should be able to provide new content in the application when they connect to the network.

  • Safe: This provides services over HTTPS, preventing content tampering and man-in-the-middle attacks.

  • Progressive: It should work for everyone regardless of browser choice.

  • Linkable: Sharing with others through a URL.

Some production use cases for PWA

Flipkart Lite: Flipkart is one of the largest e-commerce companies in India. The following figure

AliExpress: AliExpress is a very popular global online retail marketplace. After implementing PWA, the number of visits and views doubled. We won’t go into details here. The following figure

Service Workers

Service Workers is a script for a programmable proxy that runs in the background of your browser and has the ability to intercept, process, and respond to HTTP requests in a variety of ways. It has the ability to respond to network requests, push notifications, connection changes, and more. The Service Workers does not have access to the DOM, but it can take advantage of the fetch and caching apis. You can cache all static resources for Service Workers, which automatically reduces network requests and improves performance. The Service worker can display an app shell that notifies the user that they are disconnected from the Internet and provides a page for the user to interact with and browse while offline.

A Service worker file such as sw.js needs to be placed in the root directory like this:

Start service workers in your PWA. If your app’s JS file is app.js, you need to register service workers in your app.js file. The following code is to register your Service workers.

  if ('serviceWorker' in navigator) {
    navigator.serviceWorker
             .register('./sw.js')
             .then(function() { console.log('Service Worker Registered'); });
  }
Copy the code

The code above checks to see if the browser supports Service Workers. If so, start registering service Workers. Once service Workers are registered, we start experiencing the life cycle of Service Workers when the user first visits the page.

Life cycle of service workers

  • Install: Triggers an installation event when a user accesses the page for the first time. In this phase, service Workers is installed in the browser. During installation, you can cache all static assets of the Web App. As shown in the following code:

// Install Service Worker
self.addEventListener('install'.function(event) {

    console.log('Service Worker: Installing.... ');

    event.waitUntil(

        // Open the Cache
        caches.open(cacheName).then(function(cache) {
            console.log('Service Worker: Caching App Shell at the moment...... ');

            // Add Files to the Cache
            returncache.addAll(filesToCache); })); });Copy the code

The filesToCache variable represents an array of all filesToCache

Cachename Specifies the name of the cache store

  • Activate: This event is triggered when the service worker starts.

// Fired when the Service Worker starts up
self.addEventListener('activate'.function(event) {

    console.log('Service Worker: Activating.... ');

    event.waitUntil(
        caches.keys().then(function(cacheNames) {
            return Promise.all(cacheNames.map(function(key) {
                if( key ! == cacheName) { console.log('Service Worker: Removing Old Cache', key);
                    returncaches.delete(key); }})); }));return self.clients.claim();
});
Copy the code

Here, the service worker updates its cache every time the app shell file changes.

  • Fetch: This event is used to cache data from the server to the app shell.caches.match()The event that triggered the Web request is parsed and checked to see if it gets data in the cache. It then either responds to the cached version of the data or usesfetchGet data from the network. withe.respondWith()Method to respond back to the Web page.
self.addEventListener('fetch'.function(event) {

    console.log('Service Worker: Fetch', event.request.url);

    console.log("Url", event.request.url);

    event.respondWith(
        caches.match(event.request).then(function(response) {
            returnresponse || fetch(event.request); })); });Copy the code

When you’re writing code. Note that Chrome, Opera, and Firefox support Service Workers, but Safari and Edge are not yet compatible with Service Workers

Service Worker Specification and Primer are useful learning materials about Service Workers.

Application Shell

Earlier in this article, I mentioned app shells several times. The application shell is the user interface that drives the application with minimal HTML,CSS, and JavaScript. A PWA ensures that the application shell is cached for multiple quick accesses and quick loading of the app.

Next we will write a PWA example step by step

We will build a simple PWA. The app only tracks the latest submissions from specific open source projects. As a PWA, he should have:

  • Offline applications, users should be able to view the latest submissions without an Internet connection.
  • The application should load repeat access immediately
  • When the button notification button is turned on, the user will be notified of the latest submission to an open source project.
  • Installable (add to home screen)
  • There is a list of Web applications

All talk, no bullshit. Let’s do it!

Create index.html and latest. HTML files in your code folder.

index.html

<! DOCTYPE html> <html> <head> <meta charset="utf-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="Width = device - width, initial - scale = 1.0">
  <title>Commits PWA</title>
  <link rel="stylesheet" type="text/css" href="css/style.css">
</head>
<body>
    <div class="app app__layout">
      <header>
        <span class="header__icon">
          <svg class="menu__icon no--select" width="24px" height="24px" viewBox="0 0 48 48" fill="#fff">
            <path d="M6 36h36v-4H6v4zm0-10h36v-4H6v4zm0-14v4h36v-4H6z"></path>
          </svg>
        </span>

        <span class="header__title no--select">PWA - Home</span>
      </header>

      <div class="menu">
        <div class="menu__header"></div>
        <ul class="menu__list">
          <li><a href="index.html">Home</a></li>
          <li><a href="latest.html">Latest</a></li>
      </div>

      <div class="menu__overlay"></div>

      <div class="app__content">

        <section class="section">
          <h3> Stay Up to Date with R-I-L </h3>
          <img class="profile-pic" src="./images/books.png" alt="Hello, World!">

          <p class="home-note">Latest Commits on Resources I like! </a></p> </section> <div class="fab fab__push">
          <div class="fab__ripple"></div>
          <img class="fab__image" src="./images/push-off.png" alt="Push Notification"/> </div> <! -- Toast msg's --> 
      
Copy the code

latest.html

<! DOCTYPE html> <html> <head> <meta charset="utf-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="Width = device - width, initial - scale = 1.0">
  <title>Commits PWA</title>
  <link rel="stylesheet" type="text/css" href="css/style.css">
</head>
<body>
    <div class="app app__layout">
      <header>
        <span class="header__icon">
          <svg class="menu__icon no--select" width="24px" height="24px" viewBox="0 0 48 48" fill="#fff">
            <path d="M6 36h36v-4H6v4zm0-10h36v-4H6v4zm0-14v4h36v-4H6z"></path>
          </svg>
        </span>
        <span class="header__title no--select">PWA - Commits</span>
      </header>

      <div class="menu">
        <div class="menu__header"></div>
        <ul class="menu__list">
          <li><a href="index.html">Home</a></li>
          <li><a href="latest.html">Latest</a></li>
        </ul>
      </div>

      <div class="menu__overlay"></div>

      <section class="card_container">
        <h2 style="margin-top:70px;" align="center">Latest Commits! </h2> <div class="container">
            <section class="card first">

            </section>
            <section class="card second">

            </section>
            <section class="card third">

            </section>
            <section class="card fourth">

            </section>
            <section class="card fifth">

            </section>
        </div>
      </section>

       <div class="loader">
          <svg viewBox="0 0 32 32" width="32" height="32">
            <circle id="spinner" cx="16" cy="16" r="14" fill="none"></circle> </svg> </div> <! -- Toast msg's --> 
      
Copy the code

Create a CSS folder and in this file download create a style. CSS file (can be viewed here), create a JS folder and in this file create app.js, menu.js, offline.js, latest.js, toast.js.

js/offline.js


(function () {
  'use strict';

  var header = document.querySelector('header');
  var menuHeader = document.querySelector('.menu__header');

  //After DOM Loaded
  document.addEventListener('DOMContentLoaded'.function(event) {
    //On initial load to check connectivity
    if(! navigator.onLine) { updateNetworkStatus(); } window.addEventListener('online', updateNetworkStatus, false);
    window.addEventListener('offline', updateNetworkStatus, false);
  });

  //To update network status
  function updateNetworkStatus() {
    if (navigator.onLine) {
      header.classList.remove('app__offline');
      menuHeader.style.background = '#1E88E5'; 
    }
    else {
      toast('You are now offline.. ');
      header.classList.add('app__offline');
      menuHeader.style.background = '#9E9E9E'; }}}) ();Copy the code

The code above helps the user visually distinguish between offline and online states in the UI.

js/menu.js

(function () {
  'use strict';

  var menuIconElement = document.querySelector('.header__icon');
  var menuElement = document.querySelector('.menu');
  var menuOverlayElement = document.querySelector('.menu__overlay');

  //Menu click event
  menuIconElement.addEventListener('click', showMenu, false);
  menuOverlayElement.addEventListener('click', hideMenu, false);
  menuElement.addEventListener('transitionend', onTransitionEnd, false);

   //To show menu
  function showMenu() {
    menuElement.style.transform = "translateX(0)";
    menuElement.classList.add('menu--show');
    menuOverlayElement.classList.add('menu__overlay--show');
  }

  //To hide menu
  function hideMenu() {
    menuElement.style.transform = "translateX(-110%)";
    menuElement.classList.remove('menu--show');
    menuOverlayElement.classList.remove('menu__overlay--show');
    menuElement.addEventListener('transitionend', onTransitionEnd, false);
  }

  var touchStartPoint, touchMovePoint;

  /*Swipe from edge to open menu*/

  //`TouchStart` event to find where user start the touch
  document.body.addEventListener('touchstart'.function(event) {
    touchStartPoint = event.changedTouches[0].pageX;
    touchMovePoint = touchStartPoint;
  }, false);

  //`TouchMove` event to determine user touch movement
  document.body.addEventListener('touchmove'.function(event) {
    touchMovePoint = event.touches[0].pageX;
    if (touchStartPoint < 10 && touchMovePoint > 30) {          
      menuElement.style.transform = "translateX(0)"; }},false);

  function onTransitionEnd() {
    if (touchStartPoint < 10) {
      menuElement.style.transform = "translateX(0)";
      menuOverlayElement.classList.add('menu__overlay--show');
      menuElement.removeEventListener('transitionend', onTransitionEnd, false); }}}) ();Copy the code

The code above animates the menu ellipsis button.

js/toast.js

(function (exports) {
  'use strict';

  var toastContainer = document.querySelector('.toast__container');
 
  //To show notification
  function toast(msg, options) {
    if(! msg)return;

    options = options || 3000;

    var toastMsg = document.createElement('div');
    
    toastMsg.className = 'toast__msg';
    toastMsg.textContent = msg;

    toastContainer.appendChild(toastMsg);

    //Show toast for 3secs and hide it
    setTimeout(function () {
      toastMsg.classList.add('toast__msg--hide');
    }, options);

    //Remove the element after hiding
    toastMsg.addEventListener('transitionend'.function (event) {
      event.target.parentNode.removeChild(event.target);
    });
  }

  exports.toast = toast; //Make this method available in global
})(typeof window === 'undefined' ? module.exports : window);
Copy the code

The above code is a toST prompt message box

Latest.js and app.js are still empty.

Now, use the local server to start your application, such as the HTTP-server module to start the local service. Your Web application should look like this:

Side menu

Index Page

Latest Page

Application Shell

Your application shell is also highlighted above. Loading dynamic content is not yet implemented. Next, we need to get the latest commit from Github’s API.

Get dynamic content

Open js/latest.js to add the following code

(function() {
  'use strict';

  var app = {
    spinner: document.querySelector('.loader')}; var container = document.querySelector('.container');


  // Get Commit Data from Github API
  function fetchCommits() {
    var url = 'https://api.github.com/repos/unicodeveloper/resources-i-like/commits';

    fetch(url)
    .then(function(fetchResponse){ 
      return fetchResponse.json();
    })
    .then(function(response) {

        var commitData = {
            'first': {
              message: response[0].commit.message,
              author: response[0].commit.author.name,
              time: response[0].commit.author.date,
              link: response[0].html_url
            },
            'second': {
              message: response[1].commit.message,
              author: response[1].commit.author.name,
              time: response[1].commit.author.date,
              link: response[1].html_url
            },
            'third': {
              message: response[2].commit.message,
              author: response[2].commit.author.name,
              time: response[2].commit.author.date,
              link: response[2].html_url
            },
            'fourth': {
              message: response[3].commit.message,
              author: response[3].commit.author.name,
              time: response[3].commit.author.date,
              link: response[3].html_url
            },
            'fifth': {
              message: response[4].commit.message,
              author: response[4].commit.author.name,
              time: response[4].commit.author.date,
              link: response[4].html_url
            }
        };

        container.querySelector('.first').innerHTML = 
        "<h4> Message: " + response[0].commit.message + "</h4>" +
        "<h4> Author: " + response[0].commit.author.name + "</h4>" +
        "<h4> Time committed: " + (new Date(response[0].commit.author.date)).toUTCString() +  "</h4>" +
        "<h4>" + "<a href='" + response[0].html_url + "'>Click me to see more! "  + "</h4>";

        container.querySelector('.second').innerHTML = 
        "<h4> Message: " + response[1].commit.message + "</h4>" +
        "<h4> Author: " + response[1].commit.author.name + "</h4>" +
        "<h4> Time committed: " + (new Date(response[1].commit.author.date)).toUTCString()  +  "</h4>" +
        "<h4>" + "<a href='" + response[1].html_url + "'>Click me to see more! "  + "</h4>";

        container.querySelector('.third').innerHTML = 
        "<h4> Message: " + response[2].commit.message + "</h4>" +
        "<h4> Author: " + response[2].commit.author.name + "</h4>" +
        "<h4> Time committed: " + (new Date(response[2].commit.author.date)).toUTCString()  +  "</h4>" +
        "<h4>" + "<a href='" + response[2].html_url + "'>Click me to see more! "  + "</h4>";

        container.querySelector('.fourth').innerHTML = 
        "<h4> Message: " + response[3].commit.message + "</h4>" +
        "<h4> Author: " + response[3].commit.author.name + "</h4>" +
        "<h4> Time committed: " + (new Date(response[3].commit.author.date)).toUTCString()  +  "</h4>" +
        "<h4>" + "<a href='" + response[3].html_url + "'>Click me to see more! "  + "</h4>";

        container.querySelector('.fifth').innerHTML = 
        "<h4> Message: " + response[4].commit.message + "</h4>" +
        "<h4> Author: " + response[4].commit.author.name + "</h4>" +
        "<h4> Time committed: " + (new Date(response[4].commit.author.date)).toUTCString() +  "</h4>" +
        "<h4>" + "<a href='" + response[4].html_url + "'>Click me to see more! "  + "</h4>";

        app.spinner.setAttribute('hidden'.true); //hide spinner
      })
      .catch(function(error) { console.error(error); }); }; fetchCommits(); }) ();Copy the code

Also introduce latest.js in your latest.html

<script src="./js/latest.js"></script>
Copy the code

Add loading to your latest.html

. <div class="loader">
      <svg viewBox="0 0 32 32" width="32" height="32">
        <circle id="spinner" cx="16" cy="16" r="14" fill="none"></circle>
      </svg>
</div>

<div class="toast__container"></div>
Copy the code

In latest.js you can see that we took the data from GitHub’s API and attached it to the DOM, and now the page looks like this.

Latest. The HTML page

Preloading the APP shell through Service Workers

To ensure that our app loads quickly and works offline, we need to cache the app shell through the service worker

  • First, create a service worker file in the root directory. Its name is sw.js
  • Second, open the app.js file and add this code to register the service worker

app.js

  if ('serviceWorker' in navigator) {
     navigator.serviceWorker
             .register('./sw.js')
             .then(function() { console.log('Service Worker Registered'); });
  }
Copy the code
  • Open thesw.jsFile and add this code

sw.js

var cacheName = 'pwa-commits-v3';

var filesToCache = [
    '/'.'./css/style.css'.'./images/books.png'.'./images/Home.svg'.'./images/ic_refresh_white_24px.svg'.'./images/profile.png'.'./images/push-off.png'.'./images/push-on.png'.'./js/app.js'.'./js/menu.js'.'./js/offline.js'.'./js/toast.js'
];

// Install Service Worker
self.addEventListener('install'.function(event) {

    console.log('Service Worker: Installing.... ');

    event.waitUntil(

        // Open the Cache
        caches.open(cacheName).then(function(cache) {
            console.log('Service Worker: Caching App Shell at the moment...... ');

            // Add Files to the Cache
            returncache.addAll(filesToCache); })); }); // Fired when the Service Worker starts up self.addEventListener('activate'.function(event) {

    console.log('Service Worker: Activating.... ');

    event.waitUntil(
        caches.keys().then(function(cacheNames) {
            return Promise.all(cacheNames.map(function(key) {
                if( key ! == cacheName) { console.log('Service Worker: Removing Old Cache', key);
                    returncaches.delete(key); }})); }));return self.clients.claim();
});


self.addEventListener('fetch'.function(event) {

    console.log('Service Worker: Fetch', event.request.url);

    console.log("Url", event.request.url);

    event.respondWith(
        caches.match(event.request).then(function(response) {
            returnresponse || fetch(event.request); })); });Copy the code

As I explained earlier in this article, all of our static resources are in the filesToCache array, and when the service worker is installed, it opens the cache in the browser and all files in the array are cached in the PWA-commits -v3 cache. Once the service worker has been installed, the Install event is triggered. This phase ensures that your service worker updates its cache whenever any shell files change. The FETCH event phase applies the shell to fetch data from the cache. Note: Pre-cache your resources for an easier and better way. Check out Sw-Toolbox and SW-Precachelibraries for Google Browser

Now reload your Web app and open DevTools, go to the Application option to view the Service Worker panel and make sure Update on Reload is checked. The following figure

Now reload the Web page and examine it. Is there offline browsing?

Index Page Offline

Yaaay!!! The “latest” page shows the latest submission.

Latest Page Offline

Yaaay!!! Latest is an offline service. But wait! Where is the data? Where is the submission? Oh dear! Our app tried to request the Github API when the user was disconnected from the Internet, and it failed.

Data Fetch Failure, Chrome DevTools

What should we do? There are different ways to approach this scenario. One option is to tell the service worker to provide an offline page. Another option is to cache the commit data on the first load, load locally saved data on subsequent requests, and then fetch the latest data when the user connects. Submitted data can be stored in IndexedDB or local Storage.

Okay, we’re done here now!

Attach:

The original address: https://auth0.com/blog/introduction-to-progressive-apps-part-one/

Project code address: https://github.com/unicodeveloper/pwa-commits

Blog: https://blog.naice.me/articles/5a31d20a78c3ad318b837f59

If there is any mistake or mistake in translation, please kindly give me your advice. I will be very grateful

Looking forward to the next article: An introduction to progressive Web Apps (Instant Loading) – Part 2