Préambule

  • PWA : Prgressive Web Application
  • Une application PWA est un type assez récent d'application Web, dont l'objectif est de fournir à l'utilisateur une expérience proche de celle d'une application native, spécialement sous mobile/tablette.
  • Cela implique principalement qu'une PWA :
    • doit être installable comme une application native, par exemple avec un icône sur le bureau/écran d'accueil,
    • peut s’accommoder d'une perte de réseau,
    • doit être sécurisée,
    • peut si besoin utiliser les périphériques de la plateforme, tels qu'une caméra, le GPS, ... grâce aux capacités natives du navigateur.
    • doit être responsive,
  • Les 4 premiers points s'appuient sur les nouvelles capacités incluses dans la plupart des navigateurs modernes.
  • Par exemple, la perte du réseau peut être gérée grâce à la mise en cache de certains fichiers et/ou données, via des processus qui tournent en parallèle de l'application : les services worker.
  • La plupart du temps, le rôle des services workers est de :
    • charger en cache les fichiers principaux de l'application lors du premier accès au serveur sur lequel elle est hébergée,
    • d'intercepter les requêtes pour aller chercher des ressources et, en fonction de la stratégie choisie, d'aller reprendre ces ressources sur le réseau, ou bien en cache.
    • de recevoir des "push messages" de la part d'un serveur, ce qui permet ensuite de signaler à l'utilisateur qu'il faut rafraîchir les données de l'application (via un bouton, glissement vers le bas, ...), ou encore d'afficher une notification à l'écran.

 

1°/ Création d'un projet PWA avec @vue/cli

1.1°/ Initialisation d'un nouveau projet

  • Le plus simple est de partir d'un projet d'exemple créé avec vue/cli, en mode paramètres manuels :
vue create mon_projet
  • Choisir le 3ème item du menu : Manually select Features
  • Utiliser la barre d'espace pour cocher la "case" : Progressive Web App (PWA) Support
  • Idem pour les autres plugins (vue-router, vuex, ...) que vous souhaiter intégrer.

1.2°/ Initialisation d'un projet existant

  • Si le projet existe déjà, il suffit de taper :
vue add pwa
  • Normalement, cela ne "casse" pas la structure du projet existant, à part le fichier main.js qui est modifié.

1.3°/ Création du service worker.

  • Après la création du projet, un fichier src/registerServiceWorker.js a été ajouté, par rapport à un projet non PWA.
  • Ce fichier permet de faciliter l'enregistrement du service worker auprès du navigateur.
  • Dans ce fichier, on remarque que l'enregistrement utilise comme fichier par défaut : service-worker.js.
  • C'est ce fichier qu'il faut créer dans src et cela s'avère relativement compliqué quand on ne connait rien aux services worker.
  • En effet, il existe un nombre conséquent d'exemples sur le net, qui ne fonctionnent pas du tout, notamment tous ceux qui expliquent comment créer automatiquement une première ébauche de ce fichier, pour ensuite le modifier.
  • Il semble que les plugins qui aident à la génération et/ou à l'écriture du fichier, aient changés puisqu'en suivant ces mauvais tutoriaux, on se retrouve à présent avec un fichier JS compacté et donc inexploitable.
  • En revanche, il est tout de même possible d'utiliser ces plugins pour automatiser certaines parties, notamment la mise en cache initiale des fichiers de l'application.
  • Pour cela, on utilise le plugin workbox-webpack-plugin, qui est installé lorsque l'on intègre le support PWA à l'application (cf. sections précédentes)
  • Pour que cette mise en cache soit gérée automatiquement, il faut déjà indiquer au plugin le chemin d'accès au fichier du service worker et ce qu'il doit faire.
  • Ceci se fait en modifiant le fichier vue.config.js, à la racine du projet, avec par exemple :
const { defineConfig } = require('@vue/cli-service')
module.exports = defineConfig({
    transpileDependencies: true,
    pwa: {
    name: 'My PWA App',
    appleMobileWebAppCapable: 'yes',
    appleMobileWebAppStatusBarStyle: 'black',
    workboxPluginMode: "InjectManifest",
    workboxOptions: {
      swSrc: "./src/service-worker.js",
        exclude: [/_redirect/, /\.map$/, /_headers/],
    },  
  }
})
  • La première option importante est  workboxPluginMode, qui précise que le plugin doit "injecter" automatiquement le contenu du "manifest" dans le fichier service worker. Un peu comme un include en PHP, ou un import en JS.
  • Le manifest est un fichier JSON qui est créé automatiquement par webpack lorsque l'on construit l'application en mode production. Il ne faut donc pas le créer soi-même.
  • NB 1 : le contenu du manifeste est une suite de propriétés de l'application, avec son nom, ses icônes, des couleurs, etc.
  • NB 2 : Il existe une autre valeur pour cette option : GenerateSW, qui permet de générer automatiquement un fichier service-worker.js, mais qui est compacté mais inexploitable.
  • La deuxième option importante est  workboxOptions.swSRC, qui précise le chemin d'accès au fichier service worker que l'on va écrire. Normalement, on le met toujours dans ./src/service-worker.js.

 

  • L'étape suivante est de créer src/service-worker.js. ATTENTION ! Ce fichier n'est qu'un "canevas" qui sera modifié/complété par webpack lors du build de production.
  • Pour faire uniquement la mise en cache automatique, on écrit :
import {setCacheNameDetails} from 'workbox-core';
import {precacheAndRoute} from 'workbox-precaching';

setCacheNameDetails({prefix: "testpwa"});

// put all important files in cache, as soon as the worker is ready
precacheAndRoute(self.__WB_MANIFEST);
  • Grâce à la 3ème ligne, le nom du cache commencera par testpwa, ce qui se constate dans l'inspecteur.
  • Le mot-clé self.___WB_MANIFEST sera effectivement remplacé par la liste des fichiers importants à mettre en cache automatiquement (cf. démonstration).

 

  • Après ces instructions, on peut ajouter celles qui mettent en place une stratégie de mise en cache.
  • Il est possible d'écrire du JS utilisant les fonctions natives des services workers, ou bien d'utiliser des fonctions de plus haut niveau fournies par les paquets workbox.
  • NB : là encore, la plupart des exemples disponibles sur le net basés sur workbox ne fonctionnent pas correctement , voire du tout !
  • Si on utilise du JS natif, cela consiste principalement à mettre en place des écouteur d'événements avec un callback pour traiter l'événement.

 

Exemple 1 : créer un cache nommé mycache, après l'installation du service worker dans le navigateur.

self.addEventListener('install', evt =>
    evt.waitUntil(
        caches.open("mycache").then(() => {
            console.log("cache created");
        })
    )
);

 Exemple 2 : intercepter les requêtes HTTP vers myapi.org afin de faire un traitement spécial (par exemple mettre en cache la réponse)

self.addEventListener('fetch', evt => {
    if (evt.request.url.startsWith(('https://myapi.org/'))) {
        return evt.respondWith(...)
    }
});
  •  Dans l'exemple ci-dessus les ... doivent être remplacés par l'appel à une fonction qui crée une réponse à la requête. Cette réponse peut très bien être créée de toute pièce, ou simplement en relançant la requête mais en plaçant le résultat en cache (cf. démonstration).

ATTENTION ! Quand un service worker intercepte une requête vers une API externe, pour faire un fetch lui-même vers cette API, il faut être sur qu'elle possède des certificats valides. Sinon, le fetch du service worker échouera.

NB : bizarrement, sous firefox, pas besoin de certificat valide à partir du moment où le navigateur a enregistré une exception pour accéder à l'API. Mais ce n'est pas du tout sécurisé !

 

 1.4°/ Permettre l'installation

  • Il y a certaines conditions pour qu'une application puisse être installable, notamment le fait qu'elle a un manifest et un service worker qui gère les requêtes fetch.
  • Toute la mécanique d'installation est fournie par le navigateur.
  • Elle consiste globalement à mettre en place un écouteur de l'événement beforeinstallprompt qui doit sauvegarder cet événement puis fournir à l'utilisateur un moyen de lancer l'installation.
  • Cela peut être par exemple un bouton sur lequel l'utilisateur clique.
  • Ce clic permet d'appeler une fonction qui appelle la fonction prompt() de  l'événement sauvegardé. Le navigateur demande donc de confirmer ou infirmer l'installation.
  • En cas d'acceptation, la navigateur demande à l'OS de créer un icone de lancement sur le bureau.
  • Il est également possible de récupérer ce qu'a répondu l'utilisateur pour faire des traitements supplémentaires (cf. démonstration).

 

1.5°/ Accéder au matériel

  • Selon le navigateur et la plateforme d'exécution, il est possible d'accéder à plus ou moins de périphériques.
  • Des exemples de code sont disponibles sur https://whatwebcando.today/. Il sont écrits en pur JS. Il faut donc les adapter un peu pour les intégrer dans une application vue.
  • Le code de démonstration fourni un exemple d'accès au GPS et à la caméra.

2°/ Code de démonstration

  • L'archive du projet complet (excepté les modules node) est téléchargeable [ ici ]
  • L'application est en gros un clone de celle utilisée dans le TD 1 : elle permet de récupérer des villes, personnages et items auprès de l'API apidemo.iut-bm.univ-fcomte.fr.
  • Après extraction, aller dans le répertoire testpwa et lancer :
npm install
npm install -g http-server
  • La première instruction installe tous les paquets node du projet. La seconde installe de quoi servir l'application web localement.

Remarques sur le code :

  • Dans le code de src/service-worker.js, on remarque que l'on intercepte les requêtes fetch, afin de les relancer immédiatement si l'URL demandée est bien celle de l'API. Si ce n'est pas le cas, l'appel initial à fecth se poursuit.
  • La relance de la requête se fait dans la méthode networkFirst() qui utilise 2 fonctions successivement :
    • la première pour relancer la requête sur le réseau avec fetch. Si cela réussit, le résultat est mis en cache avant d'être renvoyé à l'application.
    • la deuxième, qui est utilisée en cas d'échec de la première, pour aller chercher dans le cache le résultat de la requête.
  • Ces deux fonctions utilisent la console pour déboguer facilement leur fonctionnement.
  • Dans un but pédagogique, il y a 2 principes différents d'accès à l'API :
    • un service axios, comme celui du TD 1,
    • un service basé sur l'API native fetch. NB : on remarque que la gestion de l'envoi du corps de la requête, du résultat ou bien des erreurs est moins "pratique" qu'avec axios. L'avantage est que fetch est intégré au navigateur.
  • Le services qui s'occupe des villes utilise axios, alors que les deux autres utilisent fetch.
  • Dans le suite, on remarquera que cela n'a aucune influence sur la capacité du service worker à intercepter des requêtes.

 

  • L'application peut être testée dans un mode développement avec npm run serve, MAIS dans ce cas, le service worker n'est pas utilisé. On ne peut donc pas tester la mise en cache.
  • Pour construire et tester la version de production, en étant à la racine du projet :
npm run build
http-server dist
  • La deuxième instruction lancer un serveur web local, qui par défaut sert l'application en localhost:8080
  • Ouvrir un navigateur, ouvrir l'inspecteur puis aller à cette URL.
  • On remarque que le composant qui propose d'installer l'application est bien visible. Pour l'instant, refuser.
  • Dans l'inspecteur, ouvrir l'onglet Application, et dans le menu choisir Service workers.
  • Il y a bien un worker qui a été reçu et activé.
  • Dans le menu, choisir Cache > Espace de stockage du cache. On constate qu'il y a une entrée qui correspond au cache des fichiers importants.
  • Dans l'application, cliquer sur le bouton "Villes", puis "Obtenir la liste des villes".
  • Dans le console, on remarque que cela provoque une requête qui est bien intercepté par le service worker, puisque l'on retrouve les messages qu'il affiche dans ce cas.
  • Dans le cache, on remarque qu'il y a une nouvelle entrée, avec dedans un objet représentant la requête et son résultat.
  • Cliquer sur le bouton "Items", puis "Obtenir la liste des items".
  • Le cache contient un nouvel objet.
  • Dans le menu Service Workers, cochez la case "Hors connexion"
  • Retourner sur la page des villes, où la liste a disparue (NB : dans App.vue, on appelle des mutations pour réinitialiser le store lorsque l'on change de page).
  • Cliquer sur "Obtenir la liste des villes". La requête est toujours interceptée mais cette fois, la relance du fetch sur le réseau échoue et les données sont prises dans le cache.
  • Vider le cache, puis aller sur les items. Si on clique sur le bouton "Obtenir la liste des items", il n'y a ni réseau ni cache donc impossible d'obtenir la liste, comme l'indique les messages dans la console.

 

  • Si la démonstration est faite sur un ordinateur avec caméra, la page Personnages permet de lancer la caméra et prendre une photo (cf. code de PersoList.vue)
  • Pour le GPS, on peut obtenir une position même sans puce GPS (mais comment, mystère ?)

 

  • Si on retourne dans la barre blanche de l'URL et que l'on appuie sur entrée, cela revient à redemander l'application.
  • Dans ce cas, tant que l'on est en développement, on a intérêt a cocher la case "Mettre à jour lors de l'actualisation" dans le menu Service workers.
  • Dans ce cas, le service worker est rechargé, ce qui permet de tenir compte d'éventuelles modifications dans son code et que l'on a reconstruit la version de production.
  • Sinon, le navigateur se contente de prendre le service worker actuel s'il est encore valide.

 

  • Si on accepte l'installation, on obtient normalement une icône sur le bureau qui permet de lancer l'application, en dehors d'un navigateur "normal".
  • Il est possible ensuite de la désinstaller (menu ... en haut à droite)