Donner le pouvoir de la donnée aux webapps

Donner le pouvoir de la donnée aux webapps

La plupart des applications web font principalement de l'affichage. Lorsqu'il est nécessaire de traiter des données, l'application délègue ce travail à un serveur à travers des API. Chez Cozy Cloud, nous avons mis en place la démarche inverse : déléguer le plus de responsabilités possibles sur le client. Pour cela, l'enjeu est de voir comment stocker, synchroniser et gérer les données. Le jeu en vaut la chandelle : une application 100% fonctionnelle et autonome, même en mode hors ligne.

Qu'en avons-nous appris ? Comment gérer les données dans ce cas ?

Retour d'expérience sur la mise en place de ce système utilisant CouchDB, React, PouchDB... par Cyrille Perois, un des développeurs Front End en charge de l'application Cozy Banks.

Qu'est-ce qu'une webapp ?

Voici la définition d'une webapp que nous donne Wikipédia :

Une application web est une application manipulable directement en ligne grâce à un navigateur web et qui ne nécessite donc pas d'installation sur les machines clientes.
Application Web (Wikipédia)

Chez Cozy Cloud, nous faisons aussi des applications hybrides, qui ne sont ni plus ni moins que des webapps s'exécutant dans un contexte un peu particulier :

Une application hybride est une application utilisant le navigateur web intégré du support et les technologies Web (HTML, CSS et Javascript) pour fonctionner sur différents OS.
Application hybride (Wikipédia)

Lorsque nous parlerons de webapp, nous parlerons donc de ces deux types d'applications à la fois.

Comment une webapp accède-t-elle à ses données ?

La plupart des webapps utilisent le même schéma pour accéder à leurs données. Elles n'embarquent pas de code métier, mais se reposent sur une API exposée par un serveur pour manipuler ses données.

Schéma API-CozyCloud

Cette architecture est très pratique lorsqu'on souhaite développer des applications natives pour différents supports en plus d'une webapp. Les applications pourront elles aussi venir s'appuyer sur l'API, et ne pas avoir à réimplémenter toute la logique métier autour des données.

« T'as internet, toi ? »

Dans cette configuration, chaque action de l'utilisateur résulte en un appel à l'API. Que ce soit pour l'affichage, la création, la modification ou la suppression de données. Par conséquent, une question s'impose : que se passe-t-il lorsque la connexion internet de l'utilisateur est instable, voir inexistante ?

Le résultat sera le même à chaque fois : l'indicateur de chargement restera affiché longtemps. Très longtemps... Avant d'afficher éventuellement une erreur indiquant que l'action souhaitée n'a pas pu être réalisée, faute de réponse de la part du serveur.

On peut mettre plusieurs choses en place pour éviter à l'utilisateur d'attendre et/ou de voir apparaître un message d'erreur.

On peut mettre en place un cache côté client dans lequel on stockera le résultat des requêtes GET. Ainsi, lorsque l'utilisateur demandera à afficher du contenu qu'il a déjà visualisé, on pourra lui servir le contenu mis en cache dans le cas où il n'a pas de connexion. Il est possible d'implémenter diverses stratégies de cache. Voici quelques exemples :

  • Cache puis réseau : on affiche les données en cache et on envoie une requête sur le réseau au même moment. On met à jour les données affichées lorsqu'on reçoit la réponse
  • Réseau puis cache : on envoie une requête sur le réseau, et si la réponse est trop longue pour arriver, alors on affiche les données du cache
  • Course cache/réseau : on envoie une requête sur le réseau et on récupère les données dans le cache. Le plus rapide à répondre gagne.

D'autres stratégies possibles sont listées dans l'excellent article The offline cookbook de Jake Archibald.

Plusieurs API permettent d'implémenter ce système de cache :

Ce cache local fonctionne pour les données en consultation, mais il nous reste le problème de l'ajout, de la modification et de la suppression de données. Dans ces cas-là, aucune mise en cache n'est possible. Si l'accès au réseau est nécessaire pour créer un nouvel item de todo list, alors cette action est impossible à faire hors ligne.

Plutôt que de faire attendre l'utilisateur alors qu'on sait pertinement qu'une erreur va survenir, il est possible de détecter l'absence de connexion en amont (grâce à l'API Network Information), de désactiver certains boutons et de dire à l'utilisateur que certaines actions sont impossibles à réaliser tant qu'il n'a pas de connexion Internet (via une tooltip ou un bandeau, par exemple).

disabled-tooltip

Toutefois, on constate que même en ayant fournit des efforts, rien n'a réellement changé : la fonctionnalité est inutilisable sans connexion Internet.

Redonner du pouvoir et de l'autonomie aux webapps

Chez Cozy Cloud, nous avons l'ambition de rendre chacune de nos applications « offline-first ». Nous avons donc réfléchi à ces implications et comment nous pouvions arriver à ce résultat. Nous sommes arrivés à la conclusion que pour être « offline-first », une application doit être autonome et responsable de ses propres données.

Déplacer du code métier côté client

Pour qu'une application puisse manipuler elle-même la donnée, il faut y déplacer du code qui est actuellement côté serveur. Ainsi, l'application pourra créer des objets elle-même et les stocker dans le stockage du navigateur lorsque l'utilisateur fera des actions dans l'application.

Les données seront alors toujours manipulées et accessibles en local. L'application répondra aux actions de l'utilisateur très rapidement et fonctionnera sans connexion Internet.

Toutefois, en déplaçant ce code métier du côté du client, le problème inverse à celui qu'on cherche à régler apparait : les données vivent sur la machine de l'utilisateur, mais ne sont plus synchronisées avec le serveur. Si l'utilisateur change de machine, il n'aura plus accès à ses données. Il est possible, en plus de garder les données en local, de les envoyer au serveur en gardant les appels à l'API. Mais cela voudrait dire que le code métier serait dupliqué. De plus, il faudrait gérer une queue d'actions à envoyer au serveur lorsque des actions sont effectuées en mode hors ligne. Actions pouvant générer des conflits sur des données partagées.
On voit la galère arriver...
Faire tout ça à la main va nous demander un effort monstrueux. On comprend vite pourquoi, dans cette configuration, il n'y a pas plus d'applications "offline-first".

Utiliser des outils pour nous faciliter la gestion des données entre client et serveur

Vous l'aurez compris, gérer tous ces problèmes « à la main » est complexe, long et coûteux. Il est donc de bon ton d'aller voir quelles sont les technologies à notre disposition pour arriver à notre but. Chez Cozy Cloud, nous nous sommes intéressés à CouchDB. La description de ce système de gestion de bases de données orienté documents est très évocatrice :

Seamless multi-master sync, that scales from Big Data to Mobile,with an Intuitive HTTP/JSON API and designed for Reliability.

La synchronisation de données est au coeur de CouchDB. En parcourant un petit peu plus la documentation, on lit :

CouchDB Replication Protocol lets your data flow seamlessly between server clusters to mobile phones and web browsers, enabling a compelling offline-first user-experience while maintaining high performance and strong reliability

CouchDB est donc capable de synchroniser plusieurs serveurs entre eux, et son protocole de réplication peut être implémenté dans n'importe quel langage. Il nous faudrait donc une implémentation de ce protocole en JavaScript, qui serait capable de directement répliquer les données stockées dans le navigateur et celles stockées sur le serveur CouchDB... La chance nous guette, puisque cette implémentation existe : elle s'appelle PouchDB.

Notre application va donc pouvoir stocker ses données en local, dans des bases gérées par PouchDB, et laisser ce dernier discuter avec CouchDB pour synchroniser les données entre eux.

04

Par défaut, PouchDB stocke les données via IndexedDB, mais une pléthore d'adapteurs existe pour lui permettre d'utiliser n'importe quelle API. La compatibilité navigateurs est donc excellente. Les performances dépendront quant à elles évidemment de l'API utilisée.

Enfin, CouchDB est conçu pour permettre de gérer les conflits lors de modifications d'un même document par plusieurs clients. Lorsqu'un document est créé, CouchDB lui ajoute automatiquement une propriété _rev, dont la valeur ressemble à quelque chose comme 1-95eba0c67ef3 (un numéro de révision qui s'incrémentera, et un hash). Cette propriété _rev devra être spécifiée lors de chaque modification du document. Ainsi, si CouchDB reçoit une demande de modification sur une révision passée du document, celui-ci peut indiquer au client qu'il y a un conflit. À la charge du client de récupérer la dernière version du document et de retenter sa modification, après avoir réglé le conflit comme bon lui semble.

conflit-application

Ces problématiques génériques sont donc gérées par le couple CouchDB/PouchDB.

Développer une couche supplémentaire pour les problèmes spécifiques

Au-delà de ces problématiques auxquelles CouchDB et PouchDB répondent très bien, nous avons rencontré d'autres problèmes auxquels nous avons dû nous attaquer nous-mêmes.

Relations entre documents

Nous stockons chaque type de document dans une base de données CouchDB. Nous appelons cela des doctypes. Ces doctypes peuvent avoir des relations les uns avec les autres (une opération bancaire est reliée à un compte bancaire, par exemple). Toutefois, en tant que base de données orientée documents, CouchDB n'a pas de gestion des relations. Nous avons fait le choix d'utiliser un format proche de la spécification JSON API. Un document peut donc spécifier ses relations grâce à une syntaxe spéciale :

{
  "_id": "1eab3c4",
  "_rev": "1-95eba0c67ef3",
  "label": "Say « bonsoir Paris Web »",
  "done": false,
  "relationships": {
    "author": {
      "data": {
        "_id": "b1df87162e",
        "_type": "io.cozy.people"
      }
    }
  }
}

Ici, ce document déclare avoir une relation vers le document de type io.cozy.people portant l'ID b1df87162e. Il devient ainsi possible de gérer les relations entre documents de manière générique. D'autres implémentations auraient été possibles. Nous avons décidé de nous appuyer sur un standard.

Authentification

PouchDB est capable de se connecter à un serveur CouchDB via un système d'authentification classique via HTTP. Toutefois, Cozy a une petite spécificité à ce niveau : chaque application se connectant à une instance Cozy doit spécifier l'ensemble des doctypes dont elle a besoin dans les permissions de son fichier manifest.webapp. Cela se matérialise par un flow OAuth dans lequel l'application demande à la cozy-stack un jeton ne lui donnant accès qu'aux doctypes déclarés dans ses permissions. Par la suite, l'application ne se connectera pas directement au serveur CouchDB, mais à la stack, en lui passant son jeton. La stack vérifiera la validité de ce jeton et redirigera la requête vers CouchDB si tout est bon, ou renverra une erreur dans le cas contraire.

Nous avons donc dû développer une couche d'authentification spécifique pour prendre en compte cette étape supplémentaire côté client, afin de récupérer un jeton valide nécessaire pour pouvoir lancer la réplication entre PouchDB et CouchDB.

Première synchronisation PouchDB/CouchDB

Lorsque l'utilisateur vient de se connecter sur l'application, la synchronisation PouchDB/CouchDB est initiée directement. Cette synchronisation n'est pas aussi simple qu'une requête à une API avec un retour des données en JSON. Elle prend donc un temps variable à être effectuée.

Pour pouvoir présenter des données rapidement à l'utilisateur, nous avons décidé d'ignorer les bases PouchDB pendant le temps de cette synchronisation initiale, pour récupérer directement auprès de la stack les données minimales permettant d'afficher les vues. Cela nécessite une connexion Internet, mais puisque l'utilisateur vient de s'identifier, cela veut dire qu'il en a une disponible. Quand bien même celle-ci serait faible, cet appel à la stack sera plus rapide que la synchronisation de l'ensemble des données de l'application.

Pour gérer cela, nous avons mis en place une chaîne de liens. Le fonctionnement est le suivant :

  • Une requête est envoyée dans la chaîne de liens
  • Chaque lien analyse la requête et décide soit d'y répondre, soit de la passer au lien suivant
  • Une réponse est récupérée en sortie de la chaîne

Cela nous permet de brancher des liens qui sont chacun responsable d'une source de données à cette chaîne.

Dans le schéma ci-dessus, le PouchLink reçoit en premier la requête. Si il est en cours de synchronisation initiale sur le doctype demandé, ou qu'il n'a pas été configuré pour le synchroniser, alors celui-ci peut passer la main au StackLink, qui fera un appel HTTP à la stack.

Les liens pour PouchDB et la stack sont les seuls existants aujourd'hui, mais le système de lien étant générique, n'importe quelle autre source de données nécessaire au fonctionnement d'une application peut être branchée à la chaîne.

Toutes ces fonctionnalités (gestion des relations entre documents, de l'authentification, de la première synchronisation et bien d'autres) étant communes à toutes les applications Cozy, nous les avons regroupées dans cozy-client. Ainsi, n'importe quel développeur souhaitant développer une application Cozy peut s'appuyer sur ce module. Cozy Client embarque des composants React permettant de connecter son UI aux données d'un Cozy. Mais il n'est absolument pas réduit à une utilisation avec React, et peut être utilisé avec ou sans framework.

Notre conclusion

Nous avons donc remarqué que concevoir et développer des webapps de manière à ce qu'elles fonctionnent sans connexion Internet nécessite de remettre en question la pertinence du modèle dominant d'applications « coquille vide » déléguant la manipulation de données à une API dans votre contexte.
Cela nécessite de mettre en place l'outillage adéquat, mais aussi parfois de développer des choses de manière adhoc. Toutefois, à la fin, c'est l'expérience utilisateur qui est gagnante : plus (ou en tous cas moins) d'indicateurs de chargement, une meilleure réactivité et un service rendu à l'utilisateur sans qu'il ait à se soucier de l'état de sa connexion Internet.
C'est cette philosophie chez Cozy Cloud que nous nous efforçons de mettre en place dans nos applications.

Ce retour d'expérience a été partagé lors de la conférence Paris WEB en octobre 2018. La vidéo est disponible 👇

Comment contribuer à Cozy ?

community-doc-banner@2x

► Pour les développeurs·ses, plusieurs possibilités s'offrent à vous :

  1. Développer une application accessible depuis votre Cozy (notes, calendrier, musique...)
  2. Développer un connecteur pour récupérer automatiquement vos données (factures, documents administratifs, attestations de santé, fiches de paie...)
  3. Traduire, aider d'autres utilisateurs à adopter Cozy sur notre forum

► Pour suivre nos aventures, rejoignez-nous sur les réseaux sociaux :

Twitter : https://twitter.com/CozyCloud
Mastodon : https://framapiaf.org/@CozyCloud
Facebook : https://www.facebook.com/mycozycloud
LinkedIn : https://www.linkedin.com/company/cozy-cloud/

► Pour nous rejoindre 👉 des postes Tech sont actuellement ouverts.

Sinon, rendez-vous sur cozy.io pour créer votre espace Cozy hébergé en France, respectueux de votre vie privée et gratuit jusqu'à 5 Go de stockage (et même vous auto-héberger).