progressive web app service worker web mobile offline - 05 Dec 2016

Le web n'est pas mort, la revanche par les Progressives Web Apps

Richard HANNA
Écrit par Richard HANNA

Les Progressives Web Apps ont pour objectif de rivaliser avec les apps natives. Voyons comment cela fonctionne et le gain que cela apporte à vos utilisateurs.

Temps de lecture : 15 minutes

En 2010, le magazine américain Wired titrait “The web is dead“ et prédisait que les apps allaient remplacer le web. Retournement de veste en 2014 lorsque ce même magazine annonce “The web is not dead”. L’installation d’apps n’a finalement pas pris le dessus sur l’utilisation du web. En réalité la plupart des gens n’installent ou n’utilisent que très peu d’apps, celles des messageries et des réseaux sociaux. Au contraire, l’usage du web en position de mobilité a explosé.

La plupart des sources citées dans cet article sont des sites Google ou des blogs de ses ingénieurs, tout simplement parce que les contenus sont de qualité. Cela s’explique car le mastondonte américain fait un lobby de dingue pour pousser les Progressives web apps et faire plier son rival Apple, qui est en retard en la matière. Et honnêtement, Google n’a pas tout à fait tort. Voici pourquoi.

Qu’est ce qu’une Progressive Web App ?

Des retours sur investissement assez impressionnants

Le Service Worker

Depuis plusieurs années, il existe une technologie planquée dans nos navigateurs permettant de gérer du cache et donc de faire du hors-ligne : Application Cache mais celle-ci est dépréciée au profit du Service Worker.

Comment ça marche ?

Un service worker est déclaré ainsi dans le code JavaScript de vos pages :

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

On dit alors que le Service Worker est “registered” dans le navigateur. De plus, le scope est très important : /service-worker.js à la racine du domaine signifie que le service worker est disponible pour l’ensemble du domaine. S’il était dans un répertoire /blog/service-worker.js, il ne fonctionnerait que sur les pages dont les urls commencent par /blog/.

Et notre service-worker.js dans tout ça ? Et bien, il contient des écouteurs d’évènements :

self.addEventListener('install', event => {
  console.log('Service worker install');
});

self.addEventListener('activate', event => {
  console.log('Service worker ready');
});

Gestion du cache

La mise en cache de ressources se fait ainsi dans notre Service Worker :

var CACHE_NAME = 'my-cache-v1';
var urlsToCache = [
  '/',
  '/styles/main.css',
  '/script/main.js'
];

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

La récupération de ressources en cache se fait en écoutant l’évènement “fetch”. Si la ressource n’est pas trouvée en cache, on tente notre chance via le réseau :

self.addEventListener('fetch', function(event) {
  event.respondWith(
    caches.match(event.request)
      .then(function(response) {
        if (response) {
          return response;
        }

        // use the network to fetch request
        return fetch(event.request);
      }
    )
  );
});

Modifions notre code précédent pour mettre en cache la ressource récupérée du réseau :

// we need to clone the response.
var fetchRequest = event.request.clone();

return fetch(fetchRequest).then(
  function(response) {
    // Check if we received a valid response
    if (!response || response.status !== 200 || response.type !== 'basic') {
      return response;
    }

    // we need to clone it so we have two streams.
    var responseToCache = response.clone();

    caches.open(CACHE_NAME)
      .then(function(cache) {
        cache.put(event.request, responseToCache);
      });

    return response;
  }
);

Il s’agit ici d’un exemple simple. La gestion du cache n’est pas forcément triviale. De nombreuses stratégies de gestion du cache existent. Jake Archibald, un des ingénieurs de Google a écrit un article complet à ce sujet : The offline cookbook. Il n’y a pas de “meilleure solution”; tout dépendra de votre besoin.

Mise à jour d’un Service Worker

Pour mettre à jour un Service Worker ou les ressources mises en cache par le Service Worker, il faut supprimer les ressources en cache.

self.addEventListener('activate', function(event) {
  var cacheWhitelist = ['pages-cache-v1', 'blog-posts-cache-v1'];

  event.waitUntil(
    caches.keys().then(function(cacheNames) {
      return Promise.all(
        cacheNames.map(function(cacheName) {
          if (cacheWhitelist.indexOf(cacheName) === -1) {
            return caches.delete(cacheName);
          }
        })
      );
    })
  );
});

Les nouvelles versions des fichiers seront alors récupérées au prochain chargement de la page, via l’évènement “install” vu plus haut dans cet article.

Architecture

Pour booster vos web apps, il est recommandé d’opter pour une architecture de type “App Shell”. C’est à dire que vous fournissez une coquille vide contenant la présentation de votre app (html, js, css, images). Ces ressources sont mises en cache et n’ont pas besoin d’être retéléchargées à chaque requête. Ensuite, les données sont récupérées via des fetch d’API par exemple et viennent s’insérer dans votre présentation à l’aide par exemple de votre framework Frontend préféré. Les requêtes à ces API peuvent elles-mêmes être mises en cache.

Appshell

C’est réservé aux apps mobiles ?

Il est vrai que le problème de connectivité, on l’a surtout en position de mobilité et grâce à la gestion du cache, une web app reste utilisable même en mode déconnecté. Mais rien ne vous empêche d’utiliser un Service Worker pour booster vos applications web desktop. Les mastodontes tels que Gmail ou Facebook les utilisent couramment.

Le Web App Manifest

Le Web App Manifest a pour but de permettre l’installation des applications Web sur l’écran d’accueil d’un appareil, notamment sur les smartphones, offrant aux utilisateurs un accès plus rapide.

L’ouverture du site dans le navigateur se présente comme une application native avec également un Splash Screen :

splashscreen

Ce Web App Manifest se présente sous forme d’un fichier json :

{
  "short_name": "Elao App",
  "name": "Elao, agence web agile",
  "icons": [
    {
      "src": "logo-icon-1x.png",
      "type": "image/png",
      "sizes": "48x48"
    },
    {
      "src": "logo-icon-2x.png",
      "type": "image/png",
      "sizes": "96x96"
    },
    {
      "src": "logo-icon-4x.png",
      "type": "image/png",
      "sizes": "192x192"
    }
  ],
  "start_url": "/?utm_source=homescreen",
  "background_color": "#2196F3",
  "display": "standalone"
}

À noter :

Dans votre <head> html, il suffit de déclarer votre manifest de la façon suivante :

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

Bannière d’installation sur l’écran d’accueil

À quel moment le “prompt” ou bannière d’installation sur l’écran d’accueil s’affiche ?

En tant que développeur, il n’est pas possible de déclencher cet évènement. C’est le navigateur qui décide de l’afficher sous certaines conditions. Par exemple, les conditions de Chrome sont (liste non exhaustive) :

Et ces conditions peuvent changer dans les futures versions des navigateurs !

En tant que développeur, on peut toutefois attraper cet évènement et l’afficher plus tard par exemple en attendant que l’utilisateur réalise une “action positive” sur notre application. Afin qu’il soit sollicité dans le bon timing. Par exemple, dans le code ci-dessous, nous allons sauvegarder le prompt en écoutant l’évènement “beforeinstallprompt” et différer l’affichage lorsque l’utilisateur aura cliqué sur un bouton :

var deferredPrompt;

window.addEventListener('beforeinstallprompt', function(e) {
  e.preventDefault();

  // Stash the event so it can be triggered later.
  deferredPrompt = e;

  return false;
});

btnSave.addEventListener('click', function() {
  if(deferredPrompt !== undefined) {
    deferredPrompt.prompt();

    deferredPrompt.userChoice.then(function(choiceResult) {
      if(choiceResult.outcome == 'accepted') {
        console.log('App added to home screen');
      } else {
        console.log('User cancelled home screen install');
      }

      deferredPrompt = null;
    });
  }
});

Push Notifications

Les Push et les Notifications sont deux technologies différentes mais complémentaires :

L’API Push ou demander à l’utilisateur de souscrire aux notifications

Cela se passe ainsi, non pas dans le Service Worker mais dans le code JavaScript de vos pages :

var swRegistration;
var isSubscribed = false;

if ('serviceWorker' in navigator && 'PushManager' in window) {
  // Register the Service Worker
  navigator.serviceWorker.register('/myserviceworker.js')
  .then(function(serviceWorkerRegistered) {
    swRegistration = serviceWorkerRegistered;
  })
  .catch(function(error) {
    console.error('Service Worker Error', error);
  });

  // Set the initial subscription value
  swRegistration.pushManager.getSubscription()
  .then(function(subscription) {
    isSubscribed = !(subscription === null);

    if (isSubscribed) {
      console.log('User IS subscribed.');
    } else {
      console.log('User is NOT subscribed.');
    }
  });

  // On click on a "Subscribe to notification" button, call the pushManager to subscribe the user
  myButton.addEventListener('click', function() {
    if (swRegistration !== undefined) {
      return;
    }

    const applicationServerKey = urlB64ToUint8Array("yourApplicationServerPublicKey");

    swRegistration.pushManager.subscribe({
      userVisibleOnly: true,
      applicationServerKey: applicationServerKey
    })
    .then(function(subscription) {
      console.log('User is subscribed:', subscription);

      // We need to implement something to save the subscription, for example an API call to save it on our database
      // updateSubscriptionOnServer(subscription);

      isSubscribed = true;
    })
    .catch(function(error) {
      console.log('Failed to subscribe the user: ', error);
    });
  }
}

Le paramètre “userVisibleOnly: true” est une option mais il est en réalité requis. Cela permet de s’engager qu’une notification sera affichée à chaque fois qu’il y aura un Push.

Et pour en savoir plus sur comment obtenir le paramètre applicationServerKey, consultez cet article : Generating the applicationServerKey.

Exemple d’objet Subscription généré par le navigateur :

{  
  "endpoint": "https://example.com/push-service/send/dbDqU8xX10w:APA91b...",  
  "keys": {  
    "auth": "qLAYRzG9TnUwbprns6H2Ew==",  
    "p256dh": "BILXd-c1-zuEQYXH\\_tc3qmLq52cggfqqTr\\_ZclwqYl6A7-RX2J0NG3icsw..."  
  }  
}

Le endpoint dépend du navigateur utilisé et c’est lui même qui vous le fourni. Par exemple pour Chrome, c’est un endpoint qui ressemble à ça : https://android.googleapis.com/gcm/send/APA91bHPffi...

L’API Notifications

Pour générer une notification Push, votre serveur devra utiliser l’objet Subscription et donc le endpoint fourni. Pour voir en détail comment gérer cela, consultez cet article : Sending Messages.

La prise en compte d’une notification est réalisée par le Service Worker, en écoutant un évènement “push” :

self.addEventListener('push', event => {
  event.waitUntil(
    // Display a notification
    self.registration.showNotification('You got a notification!');
  );
});

Une notification ne requis qu’un titre, mais d’autres options sont possibles :

self.registration.showNotification('You got a notification!', {
  "body": "Souhaitez-vous confirmer le rendez-vous du 20/11/2016 avec M. Martin ?",
  "icon": "/images/meeting.png",
  "tag": "meeting",
  "actions": [
    { "action": "yes", "title": "Yes", "icon": "images/yes.png" },
    { "action": "no", "title": "No", "icon": "images/no.png" }
  ]
 });

Comme vous pouvez le voir, outre le contenu et l’icone de la notification, il est possible :

Deux autres écouteurs sont disponibles pour réaliser des actions lorsque l’utilisateur ouvre ou ferme la notification :

self.addEventListener('notificationclick', event => {  
  // Do something with the event  
  event.notification.close();  
});

self.addEventListener('notificationclose', event => {  
  // Do something with the event  
});

Pour en savoir plus sur la gestion des notifications :

Démos

Outils

Le panel “Application” de Developer Tools de Chrome est déjà assez riche en fonctionnalités pour débuguer toutes les facettes d’une Progressive Web App.

Il permet de consulter le Cache Storage :

Cache storage

Vérifier le contenu du Web App Manifest et simuler l’ajout à l’écran d’accueil. Sous Chrome desktop, l’ajout sera effectivement fait dans les onglets Applications.

service worker

Consulter le Service Worker utilisé et avoir différentes options parmi lesquelles :

service worker

Enfin, une fonction “Clear storage” permet de tout réinitialiser :

service worker

Pour en savoir plus, voir cet article Debug Progressive Web Apps.

De plus, voici d’autres outils - tous propulsés par Google - pour faciliter le développement d’une Progressive Web App :

Ils en sont où Safari et iOS ?

L’arrivée des Progressive Web Apps met clairement en danger le modèle du store d’Apple. La firme à la pomme traine sans doute volontairement des pieds.

Une Progressive Web App fonctionne sous iOS, mais les utilisateurs ne profitent ni du Service Worker, ni donc de la mise en cache et des notifications Push. Ils voient un site web responsive “normal”.

L’implémentation du Service Worker dans WebKit, le moteur de rendu de Safari, est “under consideration”.

Inutile d’utiliser un navigateur Chrome sur votre iPhone ou votre iPad pour profiter des Progressive Web App, cela ne fonctionnera pas. En effet, Chrome sous iOS est en réalité du packaging Google autour d’un WebKit :)

Du côté de Microsoft Edge, bonne nouvelle, le Service Worker est en cours d’implémentation.

En bref, en cette fin 2016, tout le potentiel des Progressives Web Apps n’est exploité que sous Android + Chrome, Firefox ou Opera (oui Opera, vous avez bien lu).

Ce n’est pas une raison d’attendre pour vous mettre aux Progressives Web Apps ; ces technologies sont en cours de propagation ; les utilisateurs Android sont majoritaires par rapport à tous les autres OS, autant les adresser maintenant et les autres en profiteront dès que ces technologies seront supportées.

Pour suivre l’avancement de l’implémentation de Service Worker, un site : Is Service Worker Ready?

Et après ?

Que peut-on ajouter à notre Progressive Web Apps pour améliorer encore plus l’expérience utilisateur ?

Devinez-quoi ? Ces technologies ne sont disponibles que dans les dernières versions de Chrome. Cependant c’est très prometteur. A suivre donc de près !