Progressive Web Apps!*

(a talk by Lemon)
ahoylemon.xyz
* This talk is actually called "How To Make Your Website A Progressive Web App (And Why You Might Want To)", but that's entirely too long

Progressive Web Apps!*

(a talk by Lemon)
ahoylemon.xyz
* This talk is actually called "How To Make Your Website A Progressive Web App (And Why You Might Want To)", but that's entirely too long

Hi!

My name is Lemon.

(I make websites.)

I work at

Savas Labs

Front End
Development Director

  • Mentoring
  • Shepherding
  • Documenting

I make websites.

websites != apps

Let me define my terms.

I want you to build me a website!

The Client

Sounds good.

me

I want you to build me a mobile app!

The Client

Okay, but based on what you're trying to do, a mobile app might not be the best solution. What about a website instead?

me

I want you to build me a mobile app, and I'll pay you a lot of money to do it.

The Client

Okay, let's build a mobile app.

me

Building an app in Cordova...

  1. Build an HTML website.
  2. Wrap it in a vendor-specific webview.
  3. Add plugins.
  4. Release to the store.*
  5. * herein lies the problem.

Actually, a few problems...

  1. app signing
  2. plugin versioning
  3. incompatable dependencies
  4. style & content rules
  5. changing rules
  6. possibility of rejection
  7. waiting

So, I built a mobile app...

...and then I made a small change.

From: Google Play Support
To: me

We have rejected your latest build from the Google Play Store, because it violates our policy on embedded YouTube videos.

Please see this webpage that has a whole lot of text, which kind of sort of describes what I'm talking about.

From: me
To: Google Play Support

Okay, thank you so much for your note. I am confused on the cause though, as my app does not have any embedded YouTube videos.

Can you please explain?

From: Google Play Support
To: me

The embedded YouTube videos in your app violate our policy on embedded YouTube videos.

From: me
To: Google Play Support

Right... Except the app doesn't have any YouTube videos, of any sort.

Please advise.

From: Google Play Support
To: me

We do not offer support.

From: Google Play Support
To: me

We do not offer support.

This is a specific problem.
pointing to a greater concern.

The Internet is not a walled garden.

So, when you say you want a mobile app, what do you really want?

what I should have said

“I want something that...

  • has an app-like interface
  • can be installed on a user's machine
  • will work offline
  • uses camera & microphone
  • geolocation
  • has push notifications

A spec written by...

Supported by

What is a PWA?

Potentially a lot of things.

What can be in a PWA?

  • installable
  • works offline
  • notifications
  • managed caching
  • runs chromeless
  • native sharing
  • wake control
  • multitouch
  • bluetooth
  • quite a bit more.

What do you need?

  1. HTTPS connection
  2. manifest.json
  3. service-worker.js

Where can I get a HTTPS certificate?

PS: For free?

  • GitHub Pages
  • Let's Encrypt
  • Cloudflare
  • Many cPanel-based hosting platforms
    (personal rec's: Green Geeks & InMotion)

Regardless: get a cert, turn it on, and direct all HTTP traffic to HTTPS.

manifest.json

A json file located in the root of your project, which will define some values...

  • Name & Description
  • Color Scheme
  • Languages
  • Orientation
  • URL scope
  • Icons
service-worker.js

A single javascript file (can be called anything), which will set up functions for installation, caching, permissions, and features.

Reference both these files in your index.html

The service worker gets its own lifecycle.

Now, let's see some real-world examples

partypartypartyparty.party

I need to be able to party efficiently when I don't have internet.

a friend

That's an excellent point!

me
service-worker.js
const offlineUrl = '/index.html';
const offlineFiles = [
  '/index.html',
  '/manifest.json',
  '/css/party.css',
  '/js/min/jquery.min.js',
  '/js/min/howler.min.js',
  '/js/min/party.min.js',
  '/audio/party.mp3',
  '/audio/venga.mp3'
];

/*****
On install, the service worker will take all our
required files, and store those locally. */
self.addEventListener('install', function(event) {
  var offlineRequest = new Request(offlineFiles);
  event.waitUntil(
    fetch(offlineRequest).then(function(response) {
      return caches.open('offline').then(function(cache) {
        return cache.put(offlineRequest, response);
      });
    })
  );
});

/************
Then, on a fetch request, the service worker 
will check for response.
If everything seems to be offline, serve the
offline files instead.  */
self.addEventListener('fetch', function(event) {
  var request = event.request;
  if (request.method === 'GET') {
    event.respondWith(
      fetch(request).catch(function(error) {
        console.error(
          '[onfetch] Failed. Serving cached offline fallback ' +
          error
        );
        return caches.open('offline').then(function(cache) {
          return cache.match(offlineUrl);
        });
      })
    );
  }
});
manifest.json
{
  "manifest_version": 2,
  "name": "PARTY PARTY PARTY PARTY PARTY",
  "short_name": "PARTY",
  "description": "PARTY PARTY PARTY PARTY PARTY PARTY PARTY PARTY PARTY PARTY PARTY PARTY PARTY",
  "default_locale": "en-us",
  "display": "standalone",
  "scope": "/",
  "start_url": "/",
  "theme_color": "#d1d1d1",
  "background_color": "#d1d1d1",
  "icons": [
    {
      "src": "/icon-36x36.png",
      "sizes": "36x36",
      "type": "image/png"
    },
    {
      "src": "/icon-48x48.png",
      "sizes": "48x48",
      "type": "image/png"
    },
    {
      "src": "/icon-72x72.png",
      "sizes": "72x72",
      "type": "image/png"
    },
    {
      "src": "/icon-96x96.png",
      "sizes": "96x96",
      "type": "image/png"
    },
    {
      "src": "/icon-144x144.png",
      "sizes": "144x144",
      "type": "image/png"
    },
    {
      "src": "/icon-192x192.png",
      "sizes": "192x192",
      "type": "image/png"
    },
    {
      "src": "/icon-256x256.png",
      "sizes": "256x256",
      "type": "image/png"
    },
    {
      "src": "/icon-384x384.png",
      "sizes": "384x384",
      "type": "image/png"
    },
    {
      "src": "/icon-512x512.png",
      "sizes": "512x512",
      "type": "image/png"
    }
  ]
}
index.html
// Register the service worker
if ('serviceWorker' in navigator) {
  navigator.serviceWorker.register('/service-worker.js').then(function(registration) {
    // Registration was successful
  }).catch(function(err) {
    // registration failed :(
  });
}
web.dev/measure

Strategy for Feature Rollout

  1. Test it in Lighthouse
  2. Get it working on your machine(s)
  3. Get it working on friends/coworkers machine(s)
  4. THEN try it out on strangers
service-worker.js
const cacheName = 'v2.17';
const offlineUrl = '/offline.html';
const offlineFiles = [
  '/index.html',
  '/offline.html',
  '/manifest.json',
  '/css/damn.css',
  '/js/libraries/vue.min.js',
  '/js/min/damn.min.js',
  '/svg/offline-dog.svg'
];

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

self.addEventListener('fetch', function(event) {
  // Only fall back for HTML documents.
  var request = event.request;
  // && request.headers.get('accept').includes('text/html')
  if (request.method === 'GET') {
    // `fetch()` will use the cache when possible, to this examples
    // depends on cache-busting URL parameter to avoid the cache.
    event.respondWith(
      fetch(request).catch(function(error) {
        // `fetch()` throws an exception when the server is unreachable but not
        // for valid HTTP responses, even `4xx` or `5xx` range.
        console.error(
          '[onfetch] Failed. Serving cached offline fallback ' +
          error
        );
        return caches.open('offline').then(function(cache) {
          return cache.match(offlineUrl);
        });
      })
    );
  }
  // Any other handlers come here. Without calls to `event.respondWith()` the
  // request will be handled without the ServiceWorker.
});

The Notification API

service-worker.js
// After the install code...

Notification.requestPermission(result => {
  if (result === 'granted') {
    //permission granted.
    navigator.serviceWorker.ready.then(registration => {
      registration.showNotification(
        'Damn Dog has been updated!', {
        icon: '/android-chrome-96x96.png',
        //vibrate: [500, 100, 500],
        body: '7 new rounds have been added to damn.dog,
                bringing the total up to 358.',
        tag: 'game-updated'
      });
    });
  }
});

Q: How do I push to users not currently on the website?

A: The same way you'd handle any other kind of push subscription

  • Firebase
  • Twilio
  • OneSignal
  • etc.

Where do you go from here?

Get cooking!

by Microsoft
by Google

What PWA Can
Do Today

by Danny Moerkerke
by Dave Geddes

When thinking of

Progressive Web Apps

^^^^^^^^^^                      

Escape the walled garden.

Make things better.  It's your responsibility.

And your pleasure.

Thank you.

dub.sh/lemontalk