PWAs avancées. Ce qu’il faut savoir pour une appli en production

construction workers
Fields workers need PWAs
Photo by Scott Blake on Unsplash

Ce que j’ai appris en créant des applications métier mobiles

Cet article a pour but de vous donner un aperçu technique général des principaux points à prendre en compte lors de la création PWA métier.

PWA, une excellente technologie pour :

Applications mobiles pour travailleurs de première ligne et collaborateurs sur le terrain

Les premiers à avoir fait bouger les choses dans le domaine des PWA a été la presse en ligne, qui s’est rendu compte que les gens n’étaient pas si impatients de télécharger une application dans un app store. Puis le commerce électronique est entré dans la danse. Aujourd’hui, d’autres secteurs d’activité commencent à voir l’avantage de créer une PWA au lieu de développer une application native pour deux ou trois plateformes différentes.

Les applications métier pour les travailleurs de première ligne et les collaborateurs “qui ne sont pas devant leur bureau” est un cas qui me semble être bien adapté aux les Application Web Progressive.
Vous pouvez créer une application mobile fonctionnelle rapidement, en utilisant uniquement les technologies du web : HTML, CSS, Javascript.
Et vous n’avez pas besoin de passer par une procédure de soumission à l’App Store.

Si vous voulez en savoir plus sur les raisons pour lesquelles vous devriez envisager de créer une application mobile, cliquez pour lire.

Vous n’avez pas besoin d’une application “Single page”.

Vous n’avez pas besoin d’un framework obésiciel à la React, Angular, Vue…

  • Faites-en une Application Multi-Page. Pourquoi ? Parce qu’un service worker vous donnera un routeur gratuitement pour gérer vos routes!
    Vous gagnez en plus d’éliminer tout le code qui sert à gérer et maintenir l’état de l’app.
  • Ça vous permettra de limiter le volume de code à envoyer sur le réseau et qui devra être analyser par le client. C’est bon pour la performance et la planète.
  • Les applications mobiles métier sont une variété d’application CRUD.
    • Votre cas d’usage c’est de créer des objets, de les stocker, de les éditer parfois et de les synchroniser avec le serveur.
    • Ça veut dire des routes du genre : someObject/new, someObject/<objectID>/edit, someObject/<objectID>/show, someObjects/list
    • Précachez les pages avec tout le HTML dont vous avez besoin, y compris les formulaires.
    • Laissez le service worker faire le pré-rendu de la vue liste (surtout si vous avez besoin d’agréger certaines données d’autres objectStore)
    • Habituellement, la partie suppression se fait côté serveur. Lors de la synchronisation de l’application de votre utilisateur, l’objectID sera supprimé.
  • Une autre techique interessante est de streamer des morceaux de réponse
    pour construire une page complète sans appeler la page complète depuis le serveur. Je l’utilise pour les documents internes.
    Le header, la navigation et le footer sont mis en cache à l’installation.
    Lorsque l’utilisateur navigue vers le document, le serveur ne répondra qu’avec le <body> HTML. Celui-ci sera mis en cache pour une utilisation ultérieure.
    La réponse du fetch utilisera alors une streamedResponse pour compiler les différents morceaux et servir la page.

Votre utilisateur sera hors ligne. Souvent. C’est comme ça.

On peut être hors ligne pour plein de raisons : il y a encore beaucoup d’endroits, même dans les pays riches, qui ont une très faible connectivité Internet / réseau mobile. Votre utilisateur peut être dans le train, le métro, à bord d’un avion, au milieu de la mer, peut ne pas avoir assez de batterie pour désactiver le mode avion. Il n’y a tout simplement pas de 3G/4G.

  • Concevez votre application pour qu’elle soit hors ligne par défaut.
    Le coté positif, c’est que ça permet de raisonner sur les objets de la même manière qu’on le fait dans une application côté serveur. N’oubliez juste pas d’ajouter la couche de synchronisation :)
  • Lors d’un appel fetch(), si vous êtes hors ligne, le fetch échouera immédiatement. Traitez ce problème séparément de celui de l’échec d’une réponse – response.ok == false.
try{
    const response = await fetch(url, init)
    return await response.json();
} catch (fetchError){
    // see https://github.com/github/fetch/issues/201#issuecomment-308213104 for fetch fail
    console.log('Tu es hors ligne, désolé!')
    throw new Error('transmission_failed')
}

Utilisez workbox.

Un point c’est tout :)

Cette bibliothèque vous fournit plein de méthodes bien pensées. Vous pouvez organiser votre logique pour le routage, la mise en cache, …

La synchronisation avec un serveur distant n’est pas toujours facile.

L’événement background-sync n’est pas toujours disponible (iOS) ni fiable (parfois background-sync ne se déclenche pas, ou bien il épuise les 3 tentatives sans atteindre le serveur).

  • Implémentez une méthode en back-up : un bouton, une synchronisation automatique au démarrage du service, ou n’utilisez pas du tout background-sync :)
  • Le plugin backgroundSync de workbox ne répondra pas forcément à tous vos besoin. Surtout si vous avez déjà stocké vos objet et vos photos/fichiers/…
  • Utilisez une propriété ‘state’ sur l’objet qui doit être synchronisé. ‘sync:pending’, ‘sync:failed’, ‘sync:done’
  • En première approximation, la logique ‘optimistic locking’ sur le serveur distant (avec un numéro de version) fonctionne assez bien.
  • Vous rencontrerez des cas :
    1. lors du téléchargement d’une photo, le serveur distant fera un time-out. Il n’y a pas grand-chose à faire, si ce n’est augmenter la valeur du délai d’attente sur le serveur et faire savoir à l’utilisateur qu’il y a encore un objet à synchroniser
    2. Le serveur distant enregistrera l’objet synchronisé mais la réponse n’atteindra pas l’application de l’utilisateur. Du coup à la prochaine synchro l’app demandera à créer de nouveau la ressource, ce qui provoquera une erreur de duplication de ligne dans la base de donnée distante
      Ma façon de m’en sortir :
// pseudo code
try { 
    <DB handler>::insert('myDB', myData);
} catch (UniqueConstraintViolationException $e) {
   // retouner un état significatif pour pouvoir mettre à jour le syncState coté client 
    retun ['errorContent' => [ 
      'myObject' => [
        'uuid' => $myObject->uuid(),
        'msg' => 'Duplicate myObject'
      ]]];
}

Vous aurez besoin d’indexedDB pour stocker vos données.

  • indexedDB est asynchrone :
    • le service worker y a accès. Super !
    • Utilisez un wrapper pour le “promisifier”. iDB c’est bien.
  • Ça marche très bien pour stocker des gros fichiers comme les photos.

Réfléchissez bien lorsque vous créez un objet Store

  • Comment vous allez l’indexer ? C’est quoi la clé primaire ? Vous allez faire quelle requête sur l’objectStore ?
  • indexedDB est un bâtard pour toute opération légèrement sophistiquée – comme la recherche avec plus d’un critère ou le tri.
    Si vous pensez que c’est votre cas, envisagez de stocker l’objet complet ou d’utiliser Dexie.

L’exceptionnalisme d’iOS

iOS Safari présente certaine bizarreries … intéressantes

  • Une que j’ai mis plusieurs heures à comprendre : FormData n’est pas disponible dans le service worker pour Safari iOS (ça l’était en décembre 2019. J’ai pas revérifié dernièrement). Vous riquez d’en avoir besoin lorsque vous téléverserez des fichiers sur le serveur distant.
    • utiliser un shim npm installer formdata-polyfill
    • ou vous pouvez passer au binaire : MDN Traitement des données binaires paul.kinlan.me/shiminig-request-formdata-in-safari/
  • iOS ne vous permet pas de stocker des blob dans indexedDB. Convertissez en ArrayBuffer. Une méthode pour le “promisifier” :
function blobToArrayBuffer(blob)
{
    return new Promise((resolve, reject) => {
        const reader = new FileReader();
        reader.addEventListener('loadend', (e) => {
            resolve(reader.result);
        });
    reader.addEventListener('error', reject);
    reader.readAsArrayBuffer(blob);
    });
}

Soignez particulièrement l’UI

  • Vos boutons soigneusement stylés seront trop petits. Toujours.
    La norme est de 40px minimum de largeur. Encore trop petit.
    Faites-les au moins 60px. Le mieux c’est le double.
  • Les travailleurs de première ligne – pensez ouvriers sur les chantiers, camionneurs, livreurs de repas à domicile, soignants, etc. Facilitez-leur la vie en simplifiant et fiabilisant l’interface.
  • La préoccupation majeure de tous c’est la facilité avec laquelle ils peuvent faire ce qu’ils sont censés faire sur l’appli. N’oubliez pas que ce n’est pas leur travail principal.
    Habituellement, cela concerne la production de rapports, de métadonnées pour la base de données principale,…
    C’est le genre de tâche qui peut être perturbatrice ou ennuyeuse. Soyez particulièrement attentif au fonctionnement de l’interface.
  • Testez l’accessibilité. Levez-vous de votre chaise, allez observer quelques utilisateurs utiliser l’application. Vous allez récolter une tonne métrique de faits. Et c’est passionnant. Vraiment.
    Vous découvrirez toutes sortes de métiers dingues !
  • N’importe quel utilisateur en déplacement est principalement concerné par : l’épuisement de la batterie.
    Aucun degré de sophistication – réfléchissez, possesseurs d’iPhone – ne pourra apaiser cette inquiétude.
    Soyez très attentif aux appels de votre applli au serveur.
    J’ai l’habitude de mettre un timestamp quelque part pour la dernière fois qu’une opération de synchronisation a été effectuée, et n’en permet une nouvelle qu’au moins après 5 minutes.
  • Instrumentez votre application. L’inconvénient des applications mobiles est qu’il est possible qu’aucune erreur n’atteigne le serveur. Un service que j’adore mais qui coûte cher : TrackJS. Un autre : Rollbar est gratuit pour 5000 hits / mois, j’ai entendu dire que Sentry est bon aussi.

Autres trucs pénibles

Les constructeurs de téléphones portables ont la fâcheuse habitude de ne pas permettre à l’utilisateur de paramétrer le poids de la photo prise.
Il n’est pas rare qu’une photo pèse environ 9-10Mo.
Bonne chance pour le téléchargement de ce monstre sur une connexion vacillante.
Et le redimensionnement côté client est juste… une tâche qui demande beaucoup de resource :/ Voir le vidage de la batterie ci-dessus.

Voilà, vous en savez assez pour aller construire des PWA extraordianires !