Progressive Web Apps & Services Workers pour votre site

(Cette page n'existe actuellement qu'en anglais)
PWA are fast, off-line, push-notification, responsive, installable web apps…

Why?

Let's say you'r goal is to run one or more websites. We'll assume you know enough of Linux to set up a dedicated server, event though we won't. Compared to a traditional dedicated server we'll also get:

  • Progressive - Works for every user, regardless of browser choice because they’re built with progressive enhancement as a core tenant.
  • 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.
  • App-like - Splash screen, use Material style for navigations and interactions..
  • Fresh - Always up-to-date thanks to the Service Workers update process.
  • Safe - Served via TLS to prevent snooping and ensure content hasn’t been tampered with.
  • Discoverable - Are identifiable as “applications” thanks to W3C manifests and Service Workers registration scope allowing search engines to find them.
  • Re-engageable - Make re-engagement easy through features like push notifications.
  • Installable - Allow users to “keep” apps they find most useful on their home screen without the hassle of an app store (automatic toast shown under certain conditions).
  • Linkable - Easily share via URL and not require complex installation.

Example showcases

Google includes a list of showcases. Just to take one, you can open Flipkart.com on your mobile or with mobile emulation turned on so get an idea, and even after that turn on flight mode to see it work offline.

How?

Off-line, instant loading, app-like… sounds really cool but for that I need to rewrite my website from scratch, right? Wrong! It's actually really easy to make your existing website support some these features. Basically you'll need to add like 3 lines to your existing HTML and create 2 files: /manifest.json and /service-worker.js (rename them if you like). We're going to see how to have most of this in sections below. For a responsive website, there are enough resources and we won't cover that.

Web App Manifest

That's a simple JSON file that controls how your app appears (for example the device home screen) and what the user can launch and more importantly how they can launch it.

Allows features:

  • Icons
  • Splash screen (using name, background_color, icons closest to 128dpi which should be precached via Service Workers).
  • Theme color

Create a file like: /manifest.json


# Note: All fields are optional.
# TODO: Remove all comments: Json doesn't support comments.
{
  "short_name": "Lorem Ipsum",  # Keep it short, appears below app icon on home screen.
  "name": "Lorem ipsum: et dolerest colearboum",
  "start_url": "/",
  "lang": "en",  # Language in which name and short_name are written.
  "icons": [
    {
      "src": "/images/icon-192x192.png",
      "sizes": "192x192",  # Minimum size required for the install toast and splash icon.
      "type": "image/png"
    },
    {
      "src": "/images/icon-256x256.png",
      "sizes": "256x256",
      "type": "image/png"
    },
    {
      "src": "/images/icon-384x384.png",
      "sizes": "384x384",
      "type": "image/png"
    },
    {
      "src": "/images/icon-512x512.png",
      "sizes": "512x512",
      "type": "image/png"
    }
  ],
  "background_color": "#FAFAFA", # See how it affects splash/launch.
  "theme_color": "#512DA8", # e.g. Outline color in Android’s task switcher.
  "display": "standalone",
  "orientation": "portrait" # Allows to force a certain orientation.
}

Now just update your page's HTML to add this line in your <head> section:

some-page.html


<head>
  …
  <link rel="manifest" href="/manifest.json">
  …
</head>

For details see:

Service Workers

Allows features:

Requirements and important notes:

  • Requires a modern browser (but can Polyfill downgrades nicely).
  • Site must be served using HTTPS! (You can only use a self-signed certificate if the clients installed it as trusted certificate so it's only useful during development; after that use something like Let's Encrypt to get a free certificate).
  • Even if a single byte of the service worker .js script changes, the browser will reinstall the Service Worker upon page reload.

We're going to use some libraries instead of directly using the native JavaScript API; if you're interested in writing directly without using third party code, you may watch Jake Archibald | JSConf EU 2014 – YouTube. Two libraries are very commonly used: sw-precache and sw-toolbox. sw-precache contains sw-toolbox and provides high level common features (pre-caching). It can be along side sw-toolbox to perform more advanced stuff which allows off-line for dynamic content and more.

Precaching with sw-precache

sw-precache generates a service-worker.js file at build time and makes sure that most static resources are cached, and that they are kept up to date (refreshes if they change by generating a hash). You can be run sw-precache in a Gulp or Grunt build script, or directly from the command-line. We'll make a Gulp script.

gulpfile.js


'use strict';

let gulp = require('gulp');
let swPrecache = require('sw-precache');

gulp.task('default', function(done) {
  let config = {
    // Set handleFetch to false during development so that the service worker
    // will precache resources but won't actually serve them (so you always get
    // the latest version and not the cached one).
    //handleFetch: false,
    // Pre-cache static pages generated from one or more server-side templates:
    // (do NOT put the URL of a page with dynamic content changing over time)
    dynamicUrlToDependencies: {
      'dynamic/page1': [
        'views/layout.jade',
        'views/page1.jade'
      ],
      'dynamic/page2': [
        'views/layout.jade',
        'views/page2.jade'
      ]
    },
    staticFileGlobs: [
      'www/css/**.css',
      'www/**.html',
      'www/images/**.*',
      'www/js/**.js'
    ],
    stripPrefix: 'www/'
  };

  swPrecache.write('www/service-worker.js', config, done);
});

Now let's generate a service-worker.js file that'll also contain the sw-toolbox library code inlined:

npm init
npm install --save-dev gulp sw-precache
$(npm bin)/gulp

And let's update your page's HTML to add this line in your <head> section (doesn’t need to be the first script but it must be at your website root so that its scope it your entire website):

some-page.html


<head>
  …
  <script>
    if ('serviceWorker' in navigator) {
      navigator.serviceWorker.register('/service-worker.js').then(function(registration) {
        // (Optional code) Registration was successful.
        console.log('ServiceWorker registration successful with scope:', registration.scope);
      }).catch(function(err) {
        // (Optional code) Registration failed.
        console.log('ServiceWorker registration failed: ', err);
      });
    }
  </script>
  …
</head>

Now your website will pre-cache all files matching the patterns we gave, meaning they'll be downloaded right away and kept in the browser cache for a long time (browsers decide when to remove something from the cache due to space). Even if you reload the page, those resource will not be downloaded again! That's why you should only include relatively small static files here (i.e. files not changing until you deploy a new version of your website). One technique is to cache the website shell and populate with a script.

Offline for dynamic content using sw-toolbox

This heavily depends on each website. Below is simple example (see also sw-toolbox reference guide). Let's modify our gulpfile.js:


  …
  let config = {
    …
    importScripts: [
      '/my-sw-toolbox-code.js'
    ]
  };
  …

/my-sw-toolbox-code.js


// Network-first: Uses network and caches response, falls back to cached response.
toolbox.router.get('/experiment/(.+)', toolbox.networkFirst);

// Cache-first, falls back to network, falls back to default image.
const DEFAULT_PROFILE_IMAGE = 'images/default-profile-image.png';

toolbox.precache([
  DEFAULT_PROFILE_IMAGE
]);
toolbox.router.get('/(.+)/images/speakers/(.*)',
                   function(request) {
                     return toolbox.cacheFirst(request).catch(function() {
                       return toolbox.cacheOnly(new Request(DEFAULT_PROFILE_IMAGE));
                     });
                   },
                   {origin: /.*\.googleapis\.com/});

// Tries to use network and falls back to using the cached version.
toolbox.router.get('/analytics.js',
                   toolbox.networkFirst,
                   {origin: ORIGIN});

// Simple trick to have very basic cached off-line for your website.
toolbox.router.get(/.*/, toolbox.networkFirst);

You should tune for each resource and let users known when what they see may not be the latest version. There are 5 built-in handlers to cover the most common network strategies (for more information about offline strategies see the Offline Cookbook):

toolbox.networkFirst
Tries to handle the request by fetching from the network. If it succeeds, it stores the response in the cache. Otherwise, try to fulfill the request from the cache. This is the strategy to use for basic read-through caching. It's also good for API requests where you always want the freshest data when it is available but would rather have stale data than no data.
toolbox.cacheFirst
If the request matches a cache entry, it responds with that. Otherwise tries to fetch the resource from the network. If the network request succeeds, it updates the cache. This option is good for resources that don't change, or have some other update mechanism.
toolbox.fastest
Requests the resource from both the cache and the network in parallel. Responds with whichever returns first. Usually this will be the cached version, if there is one. On the one hand this strategy will always make a network request, even if the resource is cached. On the other hand, if/when the network request completes the cache is updated, so that future cache reads will be more up-to-date.
toolbox.cacheOnly
Resolves the request from the cache, or fail. This option is good for when you need to guarantee that no network request will be made, for example saving battery on mobile.
toolbox.networkOnly
Handles the request by trying to fetch the URL from the network. If the fetch fails, it fails the request. Essentially the same as not creating a route for the URL at all.

You should tune for each resource and let users known when what they see may not be the latest version. There are 5 built-in handlers to cover the most common network strategies. For more information about offline strategies see the Offline Cookbook (see also sw-toolbox reference guide).

Going futher

We covered the basics which would allow your website to work offline, and that would allow users to add it to their home screen as if it was a native app.

What you want to do is first make sure your website is responsive, meaning it renders really well on mobiles. You also want to style it so it feels more like an application than a website; using Material Design Lite is a great way to achieve that. You can later use more advanced features like background sync to keep it refresh and push notifications to re-engage users, etc. You may not even need a native app (most users will likely not even want to install one in most cases), but if you really want, you can promote your native app in a nice way in your manifest.json.

If you like this article, please post and re-share on your social networks.

See also:

— Werner BEROUX, updated on June 16, 2016