Cross Platform Progressive Web Apps

Simon MacDonald

@macdonst

What is a Progressive Web App?

Progressive Web Apps use modern web capabilities to deliver an app-like user experience.

They evolve from pages in browser tabs to immersive, top-level apps, maintaining the web's low friction at every moment.

So they're just web pages?

Progressive

Works for every user, regardless of browser choice because it's built with progressive enhancement as a core tenet.

Responsive

Fits any form factor: desktop, mobile, tablet, or whatever is next.

Connectivity Independent

Enhanced with service workers to work offline or on low-quality networks.

© Google

App Like

Feels like an app, because the app shell model separates the application functionality from application content.

Shell

Content

Safe

Served via HTTPS to prevent snooping and to ensure content hasn't been tampered with

NOT!

Re-engagable

Makes re-engagement easy through features like push notifications.

Installable

Allows users to add apps they find most useful to their home screen without the hassle of an app store.

Linkable

Easily share the application via URL, does not require complex installation.

Discoverable

Is identifiable as an "application" thanks to W3C manifest and service worker registration scope, allowing search engines to find it.

// index.html
<link rel="manifest" href="/manifest.json">

// manifest.json
{
  "short_name": "AirHorner",
  "name": "Kinlan's AirHorner of Infamy",
  "icons": [
    {
      "src": "launcher-icon-1x.png",
      "type": "image/png",
      "sizes": "48x48"
    },
    {
      "src": "launcher-icon-2x.png",
      "type": "image/png",
      "sizes": "96x96"
    },
    {
      "src": "launcher-icon-4x.png",
      "type": "image/png",
      "sizes": "192x192"
    }
  ],
  "start_url": "index.html?launcher=true"
}

It all starts with

<link rel="manifest" href="manifest.json">

{
  "short_name": "PWA Test",
  "name": "PWA Test",
  "start_url": "index.html",
  "display": "standalone",
  "background_color": "#fff",
  "theme_color": "#2196F3",
  "description": "A sample PWA.",
  "icons": [
    {
      "src": "img/logo-192.png",
      "sizes": "192x192",
      "type": "image/png"
    },
    {
      "src": "img/logo-512.png",
      "sizes": "512x512",
      "type": "image/png"
    }
  ]
}

manifest.json

{
  "short_name": "PWA Test",
  "name": "PWA Test",
  "start_url": "index.html",
  "display": "standalone",
  "background_color": "#fff",
  "theme_color": "#2196F3",
  "description": "A sample PWA.",
  "icons": [
    {
      "src": "img/logo-192.png",
      "sizes": "192x192",
      "type": "image/png"
    },
    {
      "src": "img/logo-512.png",
      "sizes": "512x512",
      "type": "image/png"
    }
  ]
}

manifest.json

{
  "short_name": "PWA Test",
  "name": "PWA Test",
  "start_url": "index.html",
  "display": "standalone",
  "background_color": "#fff",
  "theme_color": "#2196F3",
  "description": "A sample PWA.",
  "icons": [
    {
      "src": "img/logo-192.png",
      "sizes": "192x192",
      "type": "image/png"
    },
    {
      "src": "img/logo-512.png",
      "sizes": "512x512",
      "type": "image/png"
    }
  ]
}

manifest.json

{
  "short_name": "PWA Test",
  "name": "PWA Test",
  "start_url": "index.html",
  "display": "standalone",
  "background_color": "#fff",
  "theme_color": "#2196F3",
  "description": "A sample PWA.",
  "icons": [
    {
      "src": "img/logo-192.png",
      "sizes": "192x192",
      "type": "image/png"
    },
    {
      "src": "img/logo-512.png",
      "sizes": "512x512",
      "type": "image/png"
    }
  ]
}

manifest.json

{
  "short_name": "PWA Test",
  "name": "PWA Test",
  "start_url": "index.html?start=a2hs",
  "display": "standalone",
  "background_color": "#fff",
  "theme_color": "#2196F3",
  "description": "A sample PWA.",
  "icons": [
    {
      "src": "img/logo-192.png",
      "sizes": "192x192",
      "type": "image/png"
    },
    {
      "src": "img/logo-512.png",
      "sizes": "512x512",
      "type": "image/png"
    }
  ]
}
{
  "short_name": "PWA Test",
  "name": "PWA Test",
  "start_url": "index.html",
  "display": "standalone",
  "background_color": "#fff",
  "theme_color": "#2196F3",
  "description": "A sample PWA.",
  "icons": [
    {
      "src": "img/logo-192.png",
      "sizes": "192x192",
      "type": "image/png"
    },
    {
      "src": "img/logo-512.png",
      "sizes": "512x512",
      "type": "image/png"
    }
  ]
}

manifest.json

{
  "short_name": "PWA Test",
  "name": "PWA Test",
  "start_url": "index.html",
  "display": "standalone",
  "background_color": "#fff",
  "theme_color": "#2196F3",
  "description": "A sample PWA.",
  "icons": [
    {
      "src": "img/logo-192.png",
      "sizes": "192x192",
      "type": "image/png"
    },
    {
      "src": "img/logo-512.png",
      "sizes": "512x512",
      "type": "image/png"
    }
  ]
}

manifest.json

{
  "short_name": "PWA Test",
  "name": "PWA Test",
  "start_url": "index.html",
  "display": "standalone",
  "background_color": "#fff",
  "theme_color": "#2196F3",
  "description": "A sample PWA.",
  "icons": [
    {
      "src": "img/logo-192.png",
      "sizes": "192x192",
      "type": "image/png"
    },
    {
      "src": "img/logo-512.png",
      "sizes": "512x512",
      "type": "image/png"
    }
  ]
}

manifest.json

{
  "short_name": "PWA Test",
  "name": "PWA Test",
  "start_url": "index.html",
  "display": "standalone",
  "background_color": "#fff",
  "theme_color": "#2196F3",
  "description": "A sample PWA.",
  "icons": [
    {
      "src": "img/logo-192.png",
      "sizes": "192x192",
      "type": "image/png"
    },
    {
      "src": "img/logo-512.png",
      "sizes": "512x512",
      "type": "image/png"
    }
  ]
}

manifest.json

App Shell Architecture

© Google

Service Workers

A service worker is a script that your browser runs in the background, separate from a web page

Registration

if ('serviceWorker' in navigator) {
  window.addEventListener('load', function() {
    navigator.serviceWorker.register('/service-worker.js');
  });
}

Installation

var CACHE_VERSION = 'v1';
var CACHE_LIST = ['index.html', 'css/main.css', 'js/main.js'];

self.addEventListener('install', function(event) {
    event.waitUntil(caches.open(CACHE_VERSION)
    .then(function(cache) {
        return cache.addAll(CACHE_LIST);
    }));
});

Activation

self.addEventListener('activate', function(event) {
    console.log("service worker has been activated.");
});

Fetch

self.addEventListener('fetch', function(event) {
 console.log('Handling fetch event for ' + event.request.url);
     event.respondWith(
         caches.match(event.request).then(function(response) {
             if (response) {
                 console.log('Found response in cache:', response);
                 return response;
             }
             console.log('No response found in cache. Fetch from network...');
             return fetch(event.request);
         })
     );
 });

index.html

{
  "short_name": "PWA Test",
  "name": "PWA Test",
  "start_url": "index.html",
  "display": "standalone",
  "background_color": "#fff",
  "theme_color": "#2196F3",
  "description": "A sample PWA.",
  "icons": [
    {
      "src": "img/logo-192.png",
      "sizes": "192x192",
      "type": "image/png"
    },
    {
      "src": "img/logo-512.png",
      "sizes": "512x512",
      "type": "image/png"
    }
  ]
}

manifest.json

<meta 
 name="apple-mobile-web-app-title" 
 content="PWA Test">

index.html

{
  "short_name": "PWA Test",
  "name": "PWA Test",
  "start_url": "index.html",
  "display": "standalone",
  "background_color": "#fff",
  "theme_color": "#2196F3",
  "description": "A sample PWA.",
  "icons": [
    {
      "src": "img/logo-192.png",
      "sizes": "192x192",
      "type": "image/png"
    },
    {
      "src": "img/logo-512.png",
      "sizes": "512x512",
      "type": "image/png"
    }
  ]
}

manifest.json

<meta 
 name="apple-mobile-web-app-capable" 
 content="yes">

index.html

{
  "short_name": "PWA Test",
  "name": "PWA Test",
  "start_url": "index.html",
  "display": "standalone",
  "background_color": "#fff",
  "theme_color": "#2196F3",
  "description": "A sample PWA.",
  "icons": [
    {
      "src": "img/logo-192.png",
      "sizes": "192x192",
      "type": "image/png"
    },
    {
      "src": "img/logo-512.png",
      "sizes": "512x512",
      "type": "image/png"
    }
  ]
}

manifest.json

<meta name=
 "apple-mobile-web-app-status-bar-style" 
 content="black">

index.html

{
  "short_name": "PWA Test",
  "name": "PWA Test",
  "start_url": "index.html",
  "display": "standalone",
  "background_color": "#fff",
  "theme_color": "#2196F3",
  "description": "A sample PWA.",
  "icons": [
    {
      "src": "img/logo-192.png",
      "sizes": "192x192",
      "type": "image/png"
    },
    {
      "src": "img/logo-512.png",
      "sizes": "512x512",
      "type": "image/png"
    }
  ]
}

manifest.json

<link 
 rel="apple-touch-icon" 
 href="img/icons/apple-touch-icon.png">

index.html

{
  "short_name": "PWA Test",
  "name": "PWA Test",
  "start_url": "index.html",
  "display": "standalone",
  "background_color": "#fff",
  "theme_color": "#2196F3",
  "description": "A sample PWA.",
  "icons": [
    {
      "src": "img/logo-192.png",
      "sizes": "192x192",
      "type": "image/png"
    },
    {
      "src": "img/logo-512.png",
      "sizes": "512x512",
      "type": "image/png"
    }
  ]
}

manifest.json

<link 
 rel="apple-touch-startup-image" 
 href="img/Default-Portrait.png">

SWWebView

cordova plugin add https://github.com/phonegap/phonegap-plugin-service-worker

 <preference name="ServiceWorker" value="service-worker.js" />
 <preference name="CacheCordovaAssets" value="false" />
cordova create myApp --template=<PATH TO ASSETS>

Create an App from your assets

Add the Service Worker plugin

Modify config.xml

cordova run ios

Run your app

npm install -g cordova

Install the Cordova CLI

You're Awesome

The End

Recommended Reading

Cross-Platform-PWA

By Simon MacDonald

Cross-Platform-PWA

FITC 2017

  • 4,910