Progressive Web App

Présentation

Les Progressive Web Apps (PWA) sont des applications modernes, hors connexion et axées sur les mobiles, pouvant rivaliser avec les meilleures applications natives. Elles sont supposées devenir la nouvelle référence du Web pour mobile.
Le but de cet article est de présenter cette nouvelle norme, les fonctionnalités offertes, le modèle économique associé et sa mise en oeuvre à partir d'une Application Web actuelle.

Intérêt des Progressive Web Apps

Les PWA sont situées à mi-chemin entre la Web App et les applications natives. Côté développement, elles reprennent l'intérêt premier des applications hybrides, à savoir un seul développement pour plusieurs systèmes d'exploitation (en théorie), sans nécessiter la génération d'un build dédié et les coûts de déploiements associés. D’un point de vue fonctionnel, l’installation et les mises à jour de l’application sont transparentes pour l’utilisateur, accès en mode déconnecté, etc.
Les PWA sont directement liées à l'essor de l'HTML 5 et des fonctionnalités associées, dont les Web-Workers, scripts Javascript exécutés dans des threads en arrière plan. Ces derniers offrent une liste grandissante de possibilités initialement associées aux applications natives, mais sur le Web.
Progressive Web Apps

Progressive web apps are web applications which are designed to work even more seamlessly on mobile devices than native mobile apps

Le terme PWA correspond au final à une liste de recommandations qu'une application web (Web App) devrait fournir à l'utilisateur pour lui offrir une expérience d'application native (mode déconnecté, notifications, Icône sur l'écran d'accueil, etc). La notion "Progressive" souligne aussi la succession d'optimisations en cours et à venir, pour lesquelles les API sont en avance sur les possibilités offertes par les navigateurs actuels.

Le récapitulatif ci-dessous présente les fonctionnalités distinguant une application web récente (à destination des mobiles) d'une PWA :

  • Web App
    • Responsive
    • Lightweight
    • Géolocalisation (via navigator.geolocation HTML5)
    • Etc
  • Progressive Web App :
    • Toutes les caractéristiques d’une Web App
    • Service Worker
      • Cache
      • Notifications
    • Web App Manifest (Add to Home Screen)
    • Etc (Credential API, Payment API, ...)

Google a rédigé une liste de pré-requis définissant selon eux les fonctionnalités à implémenter pour une PWA : https://developers.google.com/web/progressive-web-apps/checklist ; cette liste est amenée à évoluer en parallèle des API à venir pour échanger avec les devices et ainsi apporter une expérience d'application native via son navigateur web.

Modèle économique et Discovery

Comme expliqué plus haut, en comparaison des application hybrides (Ionic, Cordova), les PWA permettent de s'affranchir des builds et déploiements dans chaque marché d'application dédié (Play Store, Apple Store, Windows Store) et donc des coût associées :

  • Frais de licence
    • iOS : 99 € par an (!)
    • Android : 25 $
    • Windows : 20 $
  • Frais d'équipement
    • Téléphone(s) / Tablette pour tester les différents SDK (tests en condition réel, en complément des émulateurs)
    • Remarque : le service Device Farm d’AWS fournit une solution complète au problème de diversité des plateformes, avec une tarification adaptée à l’utilisation.
  • Frais de Maintenance
    • La maintenance corrective et évolutive est d'autant plus coûteuse qu'il y a de plateformes à maintenir

Mais cette gratuité a un coût. En effet, le positionnement au sein des Stores de chaque système d'exploitation est devenu un vrai challenge, synonyme de succès pour son application. Pour information, le Play Store d'Android contient à l'heure actuelle 86% des applications mobiles du marché.
Stores d'applications mobiles

Dans ce cas, comment promouvoir sa Progressive Web App ?

Les PWA n'ont peut être pas de marché dédié, elles restent cependant des applications Web. Et à ce titre, de nombreux moyens existent pour être référencé (SEO-friendly) ; chaque page de l’application étant indexée dans les moteurs de recherche, il est alors possible de s’affranchir des stores natifs.

De plus, de nombreuses applications sont maintenant découvertes naturellement via les réseaux sociaux ou en naviguant sur internet. C'est à ce moment là qu'intervient la fonctionnalité "Add to Home Screen", ne nécessitant aucun téléchargement supplémentaire, avec une expérience utilisateur native grâce à l'ajout d'une icône de l'application sur son téléphone et d'un lancement dans un contexte isolé, en plein écran.

Enfin, le Web étant la source de toutes les PWA, un site permettant de les référencer a logiquement été créé. Il s’agit du premier store des PWA avec une soixantaine d’applications promues.

PWA Flow

The directions are clear: visit a URL to get their app

Update 19/01/2018
Les dernières nouvelles en matière de SEO Google font pencher la balance vers les PWA avec un meilleur #rank pour les sites adaptés aux mobiles proposant une vitesse de chargement optimale ; plus d'informations ici

PWA Compliant ?

Android & Chrome

Le projet PWA étant en partie soutenu par Google, Android fait bonne figure dans le monde des navigateurs avec leur navigateur Chrome.
Ainsi, l'ensemble des fonctionnalités propres aux Progresives Web Apps sont déjà implémentées et continues d'être intégrées au fur et à mesure de leurs spécifications.

iOS & Safari

Comme expliqué plus haut, l’un des pré-requis aux PWA consiste à utiliser l'API Service Worker... non supportée par Safari.
https://jakearchibald.github.io/isserviceworkerready/

Concernant la fonctionnalité "Add to Home Screen", Apple se limite à l'utilisation de meta tags de type apple-touch-icon, apple-mobile-web-app-title, etc.
L’écran de chargement intégré à la PWA, initialement paramétrable et visible en plein écran a mystérieusement stoppé de fonctionner ; le choix d'installer une Web App sur son écran d'accueil nécessite donc pour le moment une action manuelle côté utilisateur Safari.

Add to Home Screen

Cette stratégie, en partie liée au risque de perte de revenus (frais de license, taxe sur chaque vente, etc.) et de part de marché du store mobile est cependant en train de changer, vu les mises à jour du moteur WebKit en cours suite aux réclamations répétées des développeurs

Safari a déjà inclus les Service Worker comme fonctionnalité expérimentale, confirmant ce changement de cap de la part d'Apple.

Et Windows ?

Microsoft prévoit de son côté l’intégration des PWA au sein de son store Windows 10 Redstone 4 (mars 2018), afin de promouvoir les applications développées sous cette technologie.
Son navigateur fétiche Edge n’est pour le moment pas en mesure de prendre en compte les PWA. Cependant, comme pour iOS, des développements sont en cours pour offrir une compatibilité d’ici l’intégration de leur store dédié.

Mise en pratique

Service Worker

Un Service Worker est un worker événementiel enregistré auprès d'une origine et d'un chemin. Il prend la forme d'un fichier JavaScript qui peut contrôler la page ou le site web auquel il est associé, en interceptant et en modifiant la navigation et les requêtes de ressources, et en mettant en cache les ressources selon une granularité très fine pour vous donner une maîtrise complète de la manière dont doit se comporter votre application dans certaines situations (l'une des plus évidentes étant l'indisponibilité du réseau). Il peut être assimilé à un serveur proxy, permettant de modifier les requêtes et les réponses, de les remplacer par des éléments de son propre cache, et bien plus.

Un Service Worker fonctionne dans le contexte d'un worker :

  • pas d'accès au DOM
  • non bloquant
  • asynchrone (promises)
  • limité au HTTPS

Ci-dessous un schéma présentant le cycle de vie d'un Service Worker :

Cycle de vie d'un Service Worker

Gestion du cache

Le premier intérêt réside donc dans la gestion du cache, afin d'offrir une expérience de mode déconnecté, Offline First, avant de récupérer davantage de données si possible, comme le font déjà les applications natives.

L'API de stockage utilisée, Cache, est une évolution de AppCache, indexant les ressources fournies par le serveur, en fonction des requêtes associées. Cette API fonctionne d'une manière similaire au cache standard du navigateur, mais le cache demeure spécifique au domaine. Il persiste jusqu'à ce qu'il en soit décidé autrement — de nouveau, le contrôle reste total.

  1. Chargement du script lié au Service Worker (ici, 'service-worker.js')
    1. Ce chargement peut être fait directement au sein de balise <script> du fichier HTML d'entrée
    2. Important : La visibilité du Service Worker est limitée à son scope, c'est-à-dire son emplacement au sein de l'arborescence du serveur. Les requêtes en dehors de ce scope ne seront pas interceptées. Le positionnement du fichier 'service-worker.js' (dans cet exemple) est donc important.
navigator.serviceWorker.register('service-worker.js')
  1. Configuration basique du Service Worker
    1. Utilisation de Service Worker sw-toolbox pour simplifier la syntaxe
    2. Nom du cache : ippon-cache (utile pour le supprimer au besoin lors d'une mise à jour)
    3. Pre-cache (utilise la stratégie CacheFirst pour les futures requêtes à venir)
self.toolbox.options.cache = {
    name: 'ippon-cache'
};

// pre-cache our key assets
self.toolbox.precache([
    './html/index.html',
    './manifest.json',
    './assets/img/launcher-icon-4x.png'
]);
  1. Déclaration des règles du Service Worker
    1. Définition des règles de cache pour chaque route ; CacheFirst pour toutes les pages, networkFirst pour le reste
// dynamically cache any other local assets
self.toolbox.router.any('/*', self.toolbox.cacheFirst);
// for any other requests go to the network, cache,
// and then only use that cached resource if your user goes offline
self.toolbox.router.default = self.toolbox.networkFirst;
  1. Gestion des évènements du Service Worker
    1. Écoute des événements liés au cycle de vie du Service Worker
// events listener
self.addEventListener('install', function(event) {
	console.log('[ServiceWorker] Install', event);
});
self.addEventListener('activate', function(event) {
	console.log('[ServiceWorker] Activate', event);
});
self.addEventListener('message', function(event) {
	console.log('[ServiceWorker] Message', event);
});
self.addEventListener('fetch', function(event) {
	console.log('[ServiceWorker] Fetch', event);
});
self.addEventListener('sync', function(event) {
	console.log('[ServiceWorker] Sync', event);
});

Voici un exemple d'implémentation de la règle cacheFirst (3), au sein de l'événement Fetch (4), dans le cas où la librairie sw-toolbox n'est pas utilisée :

self.addEventListener('fetch', function(event) {
  console.log('[ServiceWorker] Fetch', event);

  event.respondWith(caches.match(event.request).then(function(response) {
      // caches.match() always resolves
      // but in case of success response will have value
      if (response !== undefined) {
        return response;
      } else {
        return fetch(event.request).then(function (response) {
          // response may be used only once
          // we need to save clone to put one copy in cache
          // and serve second one
          let responseClone = response.clone();

          caches.open('ippon-cache').then(function (cache) {
            cache.put(event.request, responseClone);
          });
          return response;
        }).catch(function () {
          return caches.match('./assets/img/default.png');
        });
      }
  }));
    
});

Comme expliqué plus haut, il est possible de définir un nombre illimité de règles en fonction de l'application concernée ; un très bon résumé des différentes stratégies de cache possibles est décrit ici.

Exemple de règle

Utilisation du PushManager

L'interface PushManager Push API offre la possibilité de recevoir des notifications envoyées par un serveur tiers, que l'application soit lancée ou non. Cette fonctionnalité permet donc aux développeurs de délivrer des notifications de façon asynchrone, mais non intrusive (nécessité pour l'utilisateur d'autoriser les notifications au préalable), renforçant ainsi la confiance et l'engagement avec l'arrivée de nouveaux contenus.

Cette interface est intégrée au Service Worker, et peut être récupérée via la propriété ServiceWorkerRegistration.pushManager.

La première étape consiste donc en la souscription de l'utilisateur :

function subscribe() {
console.log('Retrieving PushManager for Subscription');

    // retrieve Service Worker 
navigator.serviceWorker.ready.then(function(serviceWorkerRegistration) {

    // subscribe Push Server
    let pushManager = serviceWorkerRegistration.pushManager;
    pushManager.subscribe({
        userVisibleOnly: true,
        applicationServerKey: applicationServerKey
    }).then(function(subscription) {
        console.log('User is subscribed.');
        updateSubscriptionOnServer(subscription);
    }, function(error) {
        console.error('Failed to subscribe the user', error);
        updateButtonStatus('subscribe_btn_id', false);
    });
}, function(error) {
            console.error('Failed to retrieve PushManager', error);
    });
}

Notification

Côté serveur, l'utilisation de webPush simplifie l'envoi de la notification à FCM Fire Cloud Messaging, avec les informations de souscription récupérées lors de l'enregistrement de l'utilisateur. Ici, nous avons réalisé le serveur avec NodeJS.

app.post('/sendNotification', function(req, res) {

  let lastFeedTitle;
  let lastFeedUrl;
  // retrieve last Ippon blog
  parser.parseURL('https://blog.ippon.fr/rss/', function(err, parsed) {
      lastFeedTitle = parsed.feed.entries[0].title;
      lastFeedUrl = parsed.feed.entries[0].link;
      
      // send FCM notification for client device
      webPush.sendNotification({
          endpoint: req.body.endpoint,
          keys: {
              p256dh: req.body.p256dh,
              auth: req.body.auth
          }
        // payload with blog informations
      }, new Buffer.from(JSON.stringify({
          title: lastFeedTitle,
          url: lastFeedUrl
      }))).then(function(data) {
          console.log("Notification sent : " + req.body.endpoint);
          res.status(200).send({
              status: "OK"
          });
      }).catch(function(error) {
          console.error("ERROR", error);
          res.status(500).send({
              status: "ERR",
              detail: error
          });
      });
  });

});

Update 19/11/2024
Depuis la création de cet article, un nouveau protocol, VAPID, permet d'authentifier le serveur à l'origine du push, sans passer par des services tiers (FCM, APNs), en restant dans les standards W3C.

Lors de la réception d'une notification, l’événement push du Service Worker est alors déclenché. Il est alors possible de notifier l'utilisateur avec une icône dédiée, un titre, un contenu, voir l'utilisation du vibreur du téléphone.

self.addEventListener('push', function(event) {
    console.log('[ServiceWorker] Push', event);

    // retrieve notification payload
    var payload = event.data.json();
    event.waitUntil(self.registration.showNotification('Ippon Push', {
        body: payload.title,
        icon: './assets/img/launcher-icon-0-75x.png',
        vibrate: [200, 100, 200, 100, 200, 100, 200],
        data: {
            url: payload.url
        }
    }));
});

Notification received

Web App Manifest (Add to Home Screen)

Le manifest JSON offre au développeur un moyen de décrire son application, afin de proposer à l’utilisateur une expérience d'application native lors du prochain lancement de la PWA :

  • Icône dédiée sur la Home (lien URL)
  • Lancement en plein écran, dans un contexte isolé, etc.

Ce fichier est à déposer à la racine du serveur (souvent placé avec le fichier responsable du Service Worker)

{
  "name": "IPPON Progressive Web App",
  "short_name": "IpponPWA",
  "start_url": "/index.html",
  "icons": [
	  {
		"src": "assets/img/launcher-icon-0-75x.png",
		"sizes": "36x36",
		"type": "image/png"
	  },
	  {
		"src": "assets/img/launcher-icon-1x.png",
		"sizes": "48x48",
		"type": "image/png"
	  },
	  {
		"src": "assets/img/launcher-icon-1-5x.png",
		"sizes": "72x72",
		"type": "image/png"
	  },
	  {
		"src": "assets/img/launcher-icon-2x.png",
		"sizes": "96x96",
		"type": "image/png"
	  },
	  {
		"src": "assets/img/launcher-icon-3x.png",
		"sizes": "144x144",
		"type": "image/png"
	  },
	  {
		"src": "assets/img/launcher-icon-4x.png",
		"sizes": "192x192",
		"type": "image/png"
	  }
  ],
  "display": "standalone",
  "background_color": "#4e8ef7",
  "theme_color": "#4e8ef7"
}

Il doit être référencé au niveau du fichier index.html

<!DOCTYPE html>
<html lang="en" dir="ltr">
  <head>
      <meta charset="UTF-8">
      <title>Ippon App</title>

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

     [...]

 </head>
  <body>
    [...]
  </body>
</html>

Add to Home Screen

Added !!

Synthèse de l’effort

Pour résumer, l’effort à fournir pour transformer une Web App en PWA classique (une règle de cache par défaut, installation sur la Home et écoute des notifications) ne prend que quelques minutes ; ci-dessous un résumé des points clés.

  1. Déclaration du service Worker
<script>
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('service-worker.js')
}
</script>
  1. Utilisation du cache (règle Network First)
importScripts('./assets/script/sw-toolbox.js');
self.toolbox.options.cache = {	name: 'ippon-cache'};
self.toolbox.router.default = self.toolbox.networkFirst;
  1. Souscription aux notifications
function subscribe() {
	navigator.serviceWorker.ready.then(function(serviceWorkerRegistration) {
		let pushManager = serviceWorkerRegistration.pushManager;
		pushManager.subscribe({
			userVisibleOnly: true,
			applicationServerKey: applicationServerKey
		}).then(function(subscription) {
			updateSubscriptionOnServer(subscription);
		}));
	});
}
  1. Affichage d'une notification
self.addEventListener('push', function(event) {
	var payload = event.data.json();
	event.waitUntil(self.registration.showNotification('Ippon Push', {
		body: payload.title,
		icon: './assets/img/launcher-icon-0-75x.png',
		data: {
			url: payload.url
		}
	}));
});
  1. Installation sur la Home et exécution en plein écran
{
  "name": "IPPON Progressive Web App",
  "short_name": "IpponPWA",
  "start_url": "/index.html",
  "icons": [
	  ...
  ],
  "display": "standalone",
  "background_color": "#4e8ef7",
  "theme_color": "#4e8ef7"
}

Au final, la migration d'une Web App en Progressive Web App ne présente aucune difficulté ni effort conséquent.
L’investissement réside dans la réflexion à avoir pour la mise en place du cache, en fonction de l’application et des attentes du client (mode déconnecté, etc.) . Du côté des notifications, leur interception est simple, la majorité des développements étant situés côté serveur.

Exemples

Dans le World Wide Web

Beaucoup de sites ont déjà pris le virage des Progressive Web App :

Chez Ippon

Un POC présentant les différentes fonctionnalités décrites plus haut (Cache Network First, Resources Pre-Cached, Push Notifications, Add to Home Screen) est disponible ici

Conclusion

L’état actuel de spécification et de compatibilité des Progressive Web App ne permet pas encore de proposer au client l'utilisation de cette technologie ; pour autant, cela ne doit pas être un frein pour leur intégration au sein des Web App déjà déployées ou en cours de développement.

L’investissement nécessaire pour transformer une Application Web en PWA, au regard des gains proposés, fonctionnels comme économiques, est à prendre dès maintenant en considération.

Sources

https://developers.google.com/web/progressive-web-apps/checklist
https://developer.mozilla.org/fr/docs/Web/API/Service_Worker_API
https://bugs.webkit.org/attachment.cgi?id=317095&action=prettypatch
https://github.com/mdn/sw-test/
https://www.powertrafic.fr/progressive-web-apps-web-mobile/
https://m.phillydevshop.com/apples-refusal-to-support-progressive-web-apps-is-a-serious-detriment-to-future-of-the-web-e81b2be29676
https://medium.com/@slightlylate/progressive-apps-escaping-tabs-without-losing-our-soul-3b93a8561955
https://cloudfour.com/thinks/the-business-case-for-progressive-web-apps/#fn-4737-1
https://progressive-web-apps.fr/microsoft-prend-virage-progressives-web-apps