« Full-js » et « no-js » avec React + Redux

Publié le Mis à jour le Par

Cet article est relatif à l’intervention de Clever Age sur l’application EasyTv pour Canal+ Overseas (en savoir plus).

Le JavaScript (ou « js ») est largement utilisé coté client pour dynamiser le rendu. Lorsqu’il n’est pas disponible, les sites web qui restent fonctionnels sont rares ! Pour y palier, on peut mettre en place une approche isomorphe pour calculer le rendu en JavaScript côté serveur. Cela permet un fonctionnement avec ou sans JavaScript sans ajouter beaucoup de code spécifique.

Introduction

Petit retour sur le développement full-js (serveur node et client autonome) d’une petite application qui aura occupé un intégrateur et deux développeurs pendant trois semaines. Oui, trois semaines pour une application complète, c’est court !

D’autant que :

  • l’application utilise des outils très récents et fluctuants (React qui n’existe que depuis l’été 2013, Redux depuis mai 2015, Babel depuis début 2015) ;
  • l’application comporte une Google Map avec filtres de points d’intérêts;
  • l’application doit permettre l’affichage d’un programme TV avec des heures correspondant au fuseau horaire local ;
  • une partie des données est contribuée sous forme de fichiers JSON;
  • l’autre partie des données est récupérée via des appels REST récursifs à des API (le premier appel donne les URLs suivantes) ;
  • enfin, l’application doit être utilisable si JavaScript n’est pas activé, y compris pour les horaires et la Google Map.

Le but de cet article n’est pas de rentrer dans les détails de la mise en œuvre de Redux et React, je vais cependant expliquer quelques concepts fondamentaux pour comprendre les mécaniques en œuvre.

React est un outil de rendu. Il prend des données, calcule le rendu de composants HTML et attache les événements du DOM côté client pour les rendre interactifs.

Redux est une implémentation de Flux, une architecture de gestion des flux de données. Son principe est de centraliser l’état de l’application en un seul endroit et d’exprimer cet état de la façon la plus minimale mais exhaustive. Cet état (state) ne peut être modifié autrement qu’en envoyant des actions à travers des fonctions appelées reducers. Les reducers sont des fonctions pures qui calculent un nouvel état à partir du précédent et d’une action tel que newState = reducers(state,action).

Au final tout fonctionne en suivant le cycle : événement => action => mise à jour du state => rendu du nouveau state.

L’événement peut être par exemple une interaction utilisateur ou la réponse d’un webservice.

Le partage avant tout

L’essentiel du code de l’application est partagé entre le client et le serveur. Dans la boucle « événement ==> action => mise à jour du state => rendu du nouveau state », seule la partie « événement » est spécifique au contexte, car dès lors qu’une action est déclenchée la suite du déroulement est synchrone, prédictible (car toutes les fonctions impliquées sont pures), et universelle (exécutable côté serveur comme côté client).

Quelques chiffres issus du projet pour démontrer mes propos :

Total2528 lignes de code source
spécifique client (app.js)75 lignes de code source
spécifique serveur (server.js)174 lignes de code source

En réalité un peu plus de code est exécuté coté client qu’indiqué mais il est difficilement mesurable : le code est intégré dans les composants React qui sont utilisés conjointement. Typiquement, la méthode componentDidMount du composant sert à indiquer que le composant est disponible pour des interactions, ce qui ne survient et n’est utilisé que côté client.

Malgré du code source spécifique, certaines logiques sont similaires sur le serveur et sur le client :

  • monter un store Redux (qui contient le state), référent de l’état des données (donc toute la logique métier est partagée entre client et serveur) ;
  • calculer le rendu de la hiérarchie de composants React avec les données du store (donc toutes les routines de rendu sont partagées entre client et serveur).

Sur le client on trouvera aussi spécifiquement :

  • l’écoute des changements de pages pour mettre à jour les données non gérées par React (métadonnées, title, scroll pour suivi des ancres) ;
  • l’écoute des redimensionnements de la fenêtre du navigateur qui notifie le store d’un changement de la taille d’affichage ;
  • l’écoute au niveau du DOM de certains clics ;
  • l’activation des interactions utilisateurs dans les composants React.

Sur le serveur on trouvera aussi spécifiquement :

  • serveur http (oui, quand même) ;
  • routage des requêtes ;
  • mise à dispo d’un proxy pour certains appels d’API ;
  • mise en cache de certains appels d’API ;
  • enfin, lecture des fichiers contribués et rechargement à chaud si modification (CMS en fichiers JSON, et ouais !).

Pertinence de l’architecture pour le « no-js »

Lorsqu’une action est réalisée par l’utilisateur :

  • soit elle est interceptée par JavaScript côté client, l’action est alors traitée par la partie Redux de l’application pour mettre à jour les données, puis la vue est rendue grâce à React ;
  • soit elle est envoyée sous forme d’URL au serveur, l’action est alors traitée par la partie Redux de l’application pour mettre à jour les données, puis la vue est rendue grâce à React côté serveur avant d’être envoyée en pur HTML au client.

On remarque que la logique suivant l’interaction est identique, et ça tombe bien, le code aussi ! C’est beau l’isomorphisme ! Côté développement, la seule difficulté consiste à ce que chaque action puisse être véhiculée par l’URL pour que le serveur puisse faire le calcul à la place du client lorsque celui-ci n’a pas JavaScript activé. Cela nécessite donc d’associer, à chaque interaction utilisateur, une URL puis de router celle-ci correctement côté serveur pour réaliser l’action et le rendu.

Cas pratique : une Google Map JavaScript… sans JavaScript

Belle théorie, mais en pratique ? Et bien en pratique c’est encore plus simple lorsqu’on dispose d’un intégrateur habitué à travailler pour du « no-js ». En effet, une classe CSS no-js est placée dans le DOM lorsque Javascript est indisponible, et toutes les transitions de confort (affichage de contenu, affichage de menu) sont désactivées au profit d’un contenu affiché en permanence. Le serveur serait capable de simuler ces actions d’affichage mais il est plus rapide de tout afficher directement plutôt que de faire un aller-retour serveur chaque fois qu’un contenu doit être affiché ou masqué.

Restent uniquement les actions plus avancées de type navigation (mais des librairies comme react-router sont là pour nous aider), recherche et Google Maps. Ces actions nécessitent quelques ajustements spécifiques pour fonctionner en « no-js ».

Dans notre projet, la Google Map est dynamique côté client (création classique d’une google.maps.Map + infoWindows + Markers), et générée sous forme d’une image côté serveur. À titre indicatif, nous avons 26 lignes de code spécifiques serveur pour cette fonctionnalité et environ 60 lignes côté client. Le « no-js » n’est donc pas aussi facile pour cette fonctionnalité que pour le reste de l’application. Cela s’explique en partie par le fait qu’on affiche un composant dont le rendu n’est pas géré par React.

La fonctionnalité de recherche de point d’intérêt filtre instantanément les marqueurs et la liste de résultats lorsque JavaScript est activé. Lorsque JavaScript est indisponible, le filtre est transformé en formulaire avec un bouton de soumission. Le traitement spécifique de cette action par le serveur tient en 8 lignes de code (transformation des paramètres de la requête en action Redux), le reste se déroule de la même façon. La carte centrée autour des points d’intérêts filtrés est alors servie au client sous forme d’image.

Cas pratique : affichages d’heures localisées « js » et « no-js »

Notre application affiche une grille TV avec les heures des programmes correspondant au fuseau horaire (timezone) de l’utilisateur. Ici pas de miracle, le serveur est bien incapable de déterminer la timezone de son utilisateur et il faut que l’utilisateur la transmette. Mais si l’utilisateur n’a pas JavaScript d’activé, il est incapable de l’envoyer… et zut !

La solution retenue est la plus simple (mais pas forcément la meilleure) : l’utilisateur choisit le pays dans lequel il est, ce qui est nécessaire pour savoir les programmes auxquels il a accès dans tous les cas, et le fuseau horaire correspondant au pays est utilisé par le serveur. S’il y a plusieurs timezones dans le pays, une seule est retenue. C’est approximatif, mais c’est le plus simple pour avoir quelque chose d’opérationnel rapidement.

Si l’utilisateur a JavaScript activé, le calcul est (re)fait côté client de la même manière que côté serveur, sauf que cette fois on peut s’appuyer sur l’horloge de l’utilisateur ! Avec JavaScript, les horaires sont donc corrigés pour correspondre parfaitement.

Il serait possible d’améliorer les cas à la marge pour les utilisateurs n’ayant pas JavaScript d’activé qui ne seraient pas dans la timezone principale de leur pays en géolocalisant l’utilisateur par IP par exemple, mais cela peut également poser problème pour ceux qui passent par un proxy. Pour avoir un comportement parfaitement précis sans JavaScript, la seule solution restante est de demander la timezone à l’utilisateur par un formulaire et de faire persister l’information d’une façon ou d’une autre.

Conclusion

Travailler pour du « no-js » a ses problématiques propres, mais utiliser un flux de données unidirectionnel de type Redux + React aide énormément ! Toute la logique métier et la logique de rendu sont mises en communs. Les seuls développements spécifiques concernent les interactions et la transmission d’actions client vers serveur. Le fait que le client soit capable de calculer les vues par lui-même rend l’application ultra-réactive et autonome une fois chargée, allégeant par la même occasion la charge serveur.