There’s been a lot of news lately about Progressive Web Apps (PWAs), and a lot of people are asking if this is the future of the (mobile) Web. I don’t want to get into a fight between Native apps and PWA, but one thing is for sure — PWA dramatically improves mobile performance and user experience.

The good news is that developing a PWA is not hard. In fact, we can take an existing web site and make it a PWA. That’s what THIS article is all about — when you’re done with this article, you can improve your site to look like a native Web app. It can work offline and has its own home screen icon.

What is Progressive Web Apps?

Progressive Web Apps (hereinafter referred to as “PWAs”) is an exciting innovation of front-end technology. PWAs combines a number of technologies to make your Web app behave like a native mobile app. Compared to pure Web and native solutions, PWAs has the following advantages for developers and users:

  1. You only need web development techniques based on open W3C standards to develop an app. No need for multi-client development.

  2. Users can experience your app before it is installed.

  3. You don’t need to download an app through the AppStore. The app will automatically upgrade without requiring the user to upgrade.

  4. Users will be prompted to ‘install’, and clicking install will add an icon to the user’s home screen.

  5. When turned on, the PWA displays an attractive flash screen.

  6. Chrome provides optional options to give the PWA a full screen experience.

  7. The necessary files are cached locally and therefore more responsive than a standard Web app (and perhaps more responsive than a Native app)

  8. The installation is extremely light — maybe a few hundred kilobytes of cached data.

  9. Data transfer for the site must be an HTTPS connection.

  10. PWAs can work offline and can synchronize the latest data when the network is restored.

It’s still early days for PWA, but there are plenty of success stories.

PWA technology is currently supported by Firefox, Chrome, and other browsers based on the Blink kernel. Microsoft is working on that with its Edge browser. Apple has no action although there are promising comments in the WebKit Five-Year plan. Fortunately, browser support doesn’t seem to be very important for PWA…

PWAs is incrementally enhanced

Your app can still run in browsers that don’t support PWA. Users won’t be able to access it offline, but everything else will remain intact. Given the pros and cons, there’s no reason not to make your app PWA.

Not only the Apps

Google is leading the PWA movement, so most tutorials talk about how to build a Chrome, native looking mobile app from scratch. However, not only special single-page applications can be PWA, nor do they need to follow material Interface Design guidelines. Most websites can be pWAized in a matter of hours. This includes your WordPress site or static site.

The sample code

Sample code can be found at github.com/sitepoint-e… To find it.

The code provides a simple four-page website. It contains some images, a style sheet, and a main javascript file. The site runs on all modern browsers (IE10+). If the browser supports PWA, users can view pages they have previously viewed while offline.

Make sure node.js is installed before running the code, and then start the service from the command line:

node ./server.js [port]Copy the code

[port] is configurable and the default value is 8888. Open Chrome or another browser based on the Blink kernel, such as Opera or Vivaldi, and type in the link http://localhost:8888/ (or a port of your choice). You can also open the developer tools to see the console information.

Browse the home page, or any other page, and take the page offline using either of the following methods:

  1. Press Cmd/Ctrl + C to stop the Node server, or

  2. In the Developer tool’s Network or Application-Service Workers column, click the Offline option.

Revisit any of the previously visited pages and they will still be accessible. Browse a page you haven’t seen before and you’ll see a dedicated offline page marked “You’re Offline” and a list of pages you can see:

Connect the phone

You can also preview the sample web page via a USB connection to your Android phone. Open the Remote Devices menu in developer Tools.

Select Settings on the left, click Add Rule and enter port 8888. You can open Chrome on your phone and go to http://localhost:8888/.

You can click “Add to Home Screen” in the browser menu. Browse a few pages and your browser will remind you to install it. Either way you can create a new icon on your home screen. After browsing a few pages, close Chrome and disconnect the device. You can still open the PWA Website app — you’ll see a launch page and have offline access to pages you’ve previously visited.

There are three necessary steps to turning your website into a Progressive Web App:

Step 1: Enable HTTPS

PWAs requires AN HTTPS connection for some obvious reasons.

HTTPS is not required in the sample code because Chrome allows testing using localhost or any 127.x.x.x address. You can also test your PWA over an HTTP connection by using Chrome and entering the following command line parameters:

  • --user-data-dir
  • --unsafety-treat-insecure-origin-as-secure

Step 2: Create a Web App Manifest

The manifest file provides some information about our site, such as name, description and images of ICONS that need to be used on the home screen, launch screen, etc.

The manifest file is a JSON-formatted file located at the root of your project. It must be requested using HTTP headers such as Content-Type: application/manifest+json or Content-Type: application/json. This file can be named by any name, in the sample code it is named /manifest.json:

{
  "name"              : "PWA Website"."short_name"        : "PWA"."description"       : "An example PWA website"."start_url"         : "/"."display"           : "standalone"."orientation"       : "any"."background_color"  : "#ACE"."theme_color"       : "#ACE"."icons": [{"src"           : "/images/logo/logo072.png"."sizes"         : "72x72"."type"          : "image/png"
    },
    {
      "src"           : "/images/logo/logo152.png"."sizes"         : "152x152"."type"          : "image/png"
    },
    {
      "src"           : "/images/logo/logo192.png"."sizes"         : "192x192"."type"          : "image/png"
    },
    {
      "src"           : "/images/logo/logo256.png"."sizes"         : "256x256"."type"          : "image/png"
    },
    {
      "src"           : "/images/logo/logo512.png"."sizes"         : "512x512"."type"          : "image/png"}}]Copy the code

In the page introduce:

<link rel="manifest" href="/manifest.json">Copy the code

The main attributes in the manifest are:

  • name— The full name of the page displayed to the user
  • short_name— Abbreviated name of the site when there is not enough space to display the full name
  • description— A detailed description of the site
  • start_url— The initial relative URL of the page (e.g/)
  • scope— Navigation range. For instance,/app/Scope limits the app to this folder.
  • background-color— The background color of the startup screen and browser
  • theme_colorThe theme color of the site, which is usually the same as the background color, can affect the display of the site
  • orientation— Preferred display direction:any.natural.landscape.landscape-primary.landscape-secondary.portrait.portrait-primary, andportrait-secondary.
  • display— Preferred display mode:fullscreen.standalone(Looks like a native app),minimal-ui(with simplified browser control options) andbrowser(Regular browser TAB)
  • icons– defines thesrc URL, sizesandtypeAn array of picture objects.

MDN provides a complete list of MANIFEST properties :Web App Manifest Properties

The Manifest option on the left side of the Application TAB in the developer tools allows you to validate your Manifest JSON file and provides “Add to Homescreen”.

Step 3: Create a Service Worker

The Service Worker is the programming interface that intercepts and responds to your network requests. This is a separate javascript file in your root directory.

Your js file (/js/main.js in the sample code) can check to see if Service workers are supported and register:

if ('serviceWorker' in navigator) {

  // register service worker
  navigator.serviceWorker.register('/service-worker.js');

}Copy the code

If you don’t need offline functionality, simply create an empty /service-worker.js file — the user will be prompted to install your app.

Service workers are complex, and you can modify the sample code to suit your purposes. This is a standard Web worker and the browser uses a separate thread to download and execute it. It doesn’t have the ability to call DOM and other page apis, but it can block web requests, including page switches, static resource downloads, and Ajax requests.

This is the main reason HTTPS is needed. It would be a disaster to imagine third-party code blocking service workers from other sites.

A service worker has three main events: install, Activate, and fetch.

Install the event

This event is triggered when the app is installed. It is often used to cache necessary files. Caching is implemented through the Cache API.

First, let’s construct a few variables:

  1. CACHE name and version number. Your application can have multiple caches but only reference one. We set the version number so that when we have a major update, we can update the cache and ignore the old cache.

  2. The URL of an offline page. When the user attempts to access a previously uncached page while offline, the page is rendered to the user.

  3. An array of files necessary for a page with offline functionality (installFilesEssential). This array should contain static resources such as CSS and JavaScript files, but I’ve also included the home page (/) and icon files. If the main page can be accessed from multiple urls, you should include them all, such as/and /index.html. Notice that offlineURL is also written to this array.

  4. Optional, describes the file array (installFilesDesirable). These files are likely to be downloaded, but the installation will not be aborted if the download fails.

// configuration
const
  version = '1.0.0',
  CACHE = version + '::PWAsite',
  offlineURL = '/offline/',
  installFilesEssential = [
    '/'.'/manifest.json'.'/css/styles.css'.'/js/main.js'.'/js/offlinepage.js'.'/images/logo/logo152.png'
  ].concat(offlineURL),
  installFilesDesirable = [
    '/favicon.ico'.'/images/logo/logo016.png'.'/images/hero/power-pv.jpg'.'/images/hero/power-lo.jpg'.'/images/hero/power-hi.jpg'
  ];Copy the code

The installStaticFiles() method adds files to the Cache using the Promise-based Cache API. The return value is generated when all the necessary files have been cached.

// install static assets
function installStaticFiles() {

  return caches.open(CACHE)
    .then(cache= > {

      // cache desirable files
      cache.addAll(installFilesDesirable);

      // cache essential files
      return cache.addAll(installFilesEssential);

    });

}Copy the code

Finally, we add Install’s event listener. The waitUntil method ensures that all code has been executed before the service worker executes install. Execute the installStaticFiles() method and then execute the self.skipWaiting() method to put the service worker into the active state.

// application installation
self.addEventListener('install', event => {

  console.log('service worker: install');

  // cache core files
  event.waitUntil(
    installStaticFiles()
    .then((a)= > self.skipWaiting())
  );

});Copy the code

Activate the event

When install is complete and the service worker enters the active state, this event is executed immediately. You probably don’t need to implement this event listener, but the sample code removes old, useless cache files here:

// clear old caches
function clearOldCaches() {

  return caches.keys()
    .then(keylist= > {

      return Promise.all(
        keylist
          .filter(key= >key ! == CACHE) .map(key= > caches.delete(key))
      );

    });

}

// application activated
self.addEventListener('activate', event => {

  console.log('service worker: activate');

    // delete old caches
  event.waitUntil(
    clearOldCaches()
    .then((a)= > self.clients.claim())
    );

});Copy the code

Note that the last self.clients.claim() method sets up the service worker itself as active.

The Fetch event

This event is triggered when there is a network request. It calls the respondWith() method to hijack the GET request and return:

  1. A static resource in the cache.

  2. If #1 fails, the Fetch API (which has nothing to do with the Service worker’s Fetch event) is used to request the resource from the network. The resource is then added to the cache.

  3. If #1 and #2 both fail, return an appropriate value.
// application fetch network data
self.addEventListener('fetch', event => {

  // abandon non-GET requests
  if(event.request.method ! = ='GET') return;

  let url = event.request.url;

  event.respondWith(

    caches.open(CACHE)
      .then(cache= > {

        return cache.match(event.request)
          .then(response= > {

            if (response) {
              // return cached file
              console.log('cache fetch: ' + url);
              return response;
            }

            // make network request
            return fetch(event.request)
              .then(newreq= > {

                console.log('network fetch: ' + url);
                if (newreq.ok) cache.put(event.request, newreq.clone());
                return newreq;

              })
              // app is offline
              .catch((a)= >offlineAsset(url)); }); })); });Copy the code

Finally, the offlineAsset(URL) method returns an appropriate value with several helper functions:

// is image URL?
let iExt = ['png'.'jpg'.'jpeg'.'gif'.'webp'.'bmp'].map(f= > '. ' + f);
function isImage(url) {

  return iExt.reduce((ret, ext) = > ret || url.endsWith(ext), false);

}


// return offline asset
function offlineAsset(url) {

  if (isImage(url)) {

    // return image
    return new Response(
      '<svg role="img" viewBox="0 0 400 300" xmlns="http://www.w3.org/2000/svg"><title>offline</title><path d="M0 0h400v300H0z" fill="#eee" /><text x="200" y="150" text-anchor="middle" dominant-baseline="middle" font-family="sans-serif" font-size="50" fill="#ccc">offline</text></svg>',
      { headers: {
        'Content-Type': 'image/svg+xml'.'Cache-Control': 'no-store'}}); }else {

    // return page
    returncaches.match(offlineURL); }}Copy the code

The offlineAsset() method checks if it is an image request, and if so, returns an SVG with the word “Offline”. If not, return to the offlineURL page.

The developer tool provides options to view information about Service workers:

The Cache Storage option in the developer tools lists all caches in the current domain and the static files they contain. When the cache is updated, you can click the refresh button in the lower left corner to update the cache:

Not surprisingly, the Clear Storage option removes your service worker and cache:

One more step – Step 4: Create a usable offline page

An offline page can be a static page to indicate that the current user request is not available. However, we can also list links to accessible pages on this page.

We can use the Cache API in main.js. However, the API uses Promises, causing all javascript to block in unsupported browsers. To avoid this, we must check the offline file list and Cache API support before loading another /js/offlinepage.js file.

// load script to populate offline page list
if (document.getElementById('cachedpagelist') && 'caches' in window) {
  var scr = document.createElement('script');
  scr.src = '/js/offlinepage.js';
  scr.async = 1;
  document.head.appendChild(scr);
}Copy the code

/js/offlinepage.js locates The most recent cache by version name Sort all lists and add them to a DOM node with ID cachedPagelist:

// cache name
const
  CACHE = '::PWAsite',
  offlineURL = '/offline/',
  list = document.getElementById('cachedpagelist');

// fetch all caches
window.caches.keys()
  .then(cacheList= > {

    // find caches by and order by most recent
    cacheList = cacheList
      .filter(cName= > cName.includes(CACHE))
      .sort((a, b) = > a - b);

    // open first cache
    caches.open(cacheList[0])
      .then(cache= > {

        // fetch cached pages
        cache.keys()
          .then(reqList= > {

            let frag = document.createDocumentFragment();

            reqList
              .map(req= > req.url)
              .filter(req= > (req.endsWith('/') || req.endsWith('.html')) && !req.endsWith(offlineURL))
              .sort()
              .forEach(req= > {
                let
                  li = document.createElement('li'),
                  a = li.appendChild(document.createElement('a'));
                  a.setAttribute('href', req);
                  a.textContent = a.pathname;
                  frag.appendChild(li);
              });

            if(list) list.appendChild(frag); }); })});Copy the code

The development tools

If you find javascript debugging difficult, the service worker won’t be great either. Chrome’s Developer Tools Application provides a range of debugging tools.

You should test your app by opening the invisibleness window so that the cache file does not survive after you close the window.

Finally, Lighthouse Extension for Chrome provides a lot of useful information on how to improve your PWA.

PWA trap

A few things to note:

URL hidden

Our sample code hides the URL bar, which I don’t recommend unless you have a single URL application, such as a game. For most sites, the manifest option display: minimal- UI or display: browser is the best choice.

The cache too much

You can cache all pages of your site and all static files. This is fine for a small site, but is it practical for a large site with thousands of pages? No one will be interested in all the content on your site, and the amount of memory on your device will be a limitation. Even if you cache only visited pages and files, as in the example code, the cache size grows rapidly.

Maybe you should note:

  • Cache only important pages, such as home pages, and recent articles.
  • Don’t cache images, videos, and other large files
  • Delete old cache files frequently
  • Provide a cache button for the user to decide whether to cache

Cache refresh

In the sample code, the user checks to see if the file is cached before requesting the network. If cached, use cached files. This is great offline, but it also means that when connected, users may not get the latest data.

Static files, such as images and videos, do not often change resources, do long caching is not a big problem. You can set cache-control in the HTTP header to Cache files for a year (31,536,000 seconds) :

Cache-Control: max-age=31536000Copy the code

Pages, CSS, and script files change frequently, so you should set a very short cache time, say 24 hours, and verify with server files when connected:

Cache-Control: must-revalidate, max-age=86400Copy the code

Retrofit Your Website as a Progressive Web App