Blog ENI : Toute la veille numérique !
En raison d'une opération de maintenance, le site Editions ENI sera inaccessible le mardi 10 décembre, en début de journée. Nous vous invitons à anticiper vos achats. Nous nous excusons pour la gêne occasionnée
En raison d'une opération de maintenance, le site Editions ENI sera inaccessible le mardi 10 décembre, en début de journée. Nous vous invitons à anticiper vos achats. Nous nous excusons pour la gêne occasionnée
  1. Livres et vidéos
  2. Développement et architecture des Applications Web Modernes
  3. Développer une application monopage
Extrait - Développement et architecture des Applications Web Modernes Retrouver les fondamentaux
Extraits du livre
Développement et architecture des Applications Web Modernes Retrouver les fondamentaux Revenir à la page d'achat du livre

Développer une application monopage

Structure et objectifs

1. Les défauts des sites web multipages

Le Web s’étant construit autour du modèle des publications de recherche, il était logique initialement d’articuler la navigation autour d’un système hypertexte simpliste. Des pages autonomes, des liens entre elles, et rien de plus. Le Web était alors un unique et immense ouvrage, où chacun de nos choix nous portait vers une page différente.

Mais le Web évoluant, les sites ont peu à peu pris le pas sur les simples pages. Telle une œuvre gigantesque donnant lieu à une entière collection composée de très nombreux tomes, la toile nous a amenés progressivement à passer plus de temps sur un espace dédié au sujet de notre choix, avant de passer à un autre.

Dans ce contexte, le concept de page et de site conçu comme de simple regroupement de pages a commencé à atteindre ses limites. Le passage d’une page à une autre est une opération coûteuse pour un navigateur : il doit appeler le serveur HTTP, qui lui communique une nouvelle page HTML, entraînant une nouvelle analyse complète de cette dernière avant d’en effectuer le rendu. Toutes ces opérations prennent du temps. Pour finir de filer notre métaphore, le lecteur se retrouve alors dans la situation de devoir rechercher sur l’étagère cette nouvelle page à chaque fois, alors qu’il préférerait continuer paisiblement sa lecture d’un seul et unique tome depuis son canapé.

L’utilisation d’un site web peut donc être vue comme une séquence de clics sur des liens. Chacun de ces clics aboutit à la récupération et au rendu d’un nouveau fichier HTML. Le diagramme suivant présente cette séquence pour un blog dont la page d’accueil est purement statique, et les articles dynamiquement générés côté serveur.

images/05DP01.png

Séquence de chargement d’un site web

Cette séquence se répète pour chaque nouvelle page. Redondance et lourdeur sont ici de mise.

2. Offrir une meilleure expérience utilisateur

De nombreux acteurs du Web se sont penchés sur ce problème très rapidement. Dès 2003, deux employés...

Principes essentiels du routage

Outre une expérience utilisateur peu satisfaisante dans beaucoup de contextes, diviser un site web en plusieurs pages amène, pour tout développeur, à devoir se poser la question de la réutilisabilité. Comment faire en sorte, par exemple, de ne pas copier-coller le code d’un menu sur chaque page ? Les SPA, en inversant cette logique, ont solutionné ce problème… ou plutôt, l’ont déplacé.

Car si travailler à partir d’une page HTML unique offre de plus grandes possibilités de réutilisabilité, cela diminue par la même occasion les possibilités de séparation des responsabilités (ou SoC - Separation of Concerns). S’il devient par exemple possible de définir un menu dès le départ dans l’unique page HTML (index.html), il devient plus complexe de gérer l’affichage de sous-menus en fonction du contexte.

La séparation des responsabilités (parfois également appelée "séparation des préoccupations") est un principe fondamental de la conception logicielle. Suivant ce principe, un programme doit être divisé en éléments portant la "responsabilité" du traitement d’une problématique précise. Ou encore, pour le dire plus clairement, un programme...

Manipuler l’URL du document

Pour bien comprendre ce que nous devons ici reproduire et manipuler, voyons exactement ce qui se cache derrière le terme "URL" (Uniform Resource Locator, localisateur uniforme de ressource) : une chaîne de caractères dont la forme est constante et bien définie (donc uniforme) et qui nous permet de localiser une ressource sur le Web. Pour pouvoir être localisée, chaque ressource doit donc disposer d’un identifiant unique, suivant le principe : un document, une URL.

Dans ce chapitre, nous omettons volontairement d’évoquer le chargement des ressources annexes (CSS, JavaScript, images, etc.). Il faut donc comprendre "URL d’un document HTML, indiquée dans la barre d’adresse" quand le terme URL est utilisé dans ces pages, car c’est pour l’heure le seul sujet qui nous intéresse.

Dans la terminologie des standards du Web, l’environnement dans lequel le navigateur affiche un document est désigné comme un contexte de navigation. Du fait de l’unicité des URL, un changement d’URL amène systématiquement à un changement de document, et donc à une navigation5, ce qui implique essentiellement :

1. de construire et exécuter une nouvelle requête HTTP,

2. de décharger le document actuellement affiché,

3. d’analyser le document HTML,

4. d’afficher le nouveau résultat.

La deuxième étape, amenant au "rechargement" de la page, est précisément celle que le modèle SPA vise à éviter.

Puisqu’une SPA n’est constituée que d’une seule et unique page HTML à proprement parler, le navigateur indique toujours la même URL quel que soit l’écran affiché : celle de ce fichier HTML (https://domain.ext/index.html ou tout simplement https://domain.ext). Dans ce contexte, nous n’avons plus aucune possibilité de directement consulter un écran particulier sans passer par l’écran d’accueil.

Il est donc nécessaire de simuler ces changements de page, en modifiant directement l’URL.

1. L’interface Location

Premier obstacle : à l’origine, ce type de comportement n’était pas prévu par les navigateurs. Seules les propriétés...

Définir des routes

Si nous simplifions à l’extrême, un site web classique dispose d’un routage très sommaire. À la réception d’une requête GET, un serveur HTTP recherche un fichier ou un dossier correspondant à l’URL reçue. Si un fichier correspondant existe, il est renvoyé, sinon une erreur est levée et, en fonction de la configuration, une réponse correspondante est retournée (par exemple, une page "Page introuvable" pour une erreur 404).

Comme dit précédemment, il est donc nécessaire de renvoyer toutes les requêtes vers le fichier index.html. Nous devons donc à présent reproduire ce comportement côté client, en gérant les différents chemins programmatiquement.

Dans un premier temps, nous devons identifier une association chemin-action (une route) pour remplacer l’association chemin-fichier du serveur HTTP. Une approche courante consiste à définir l’ensemble des routes dans un enregistrement central, que nous devrons parcourir le moment venu.

Toutes les possibilités sont offertes pour représenter cet enregistrement. La plus évidente (et donc la plus courante) étant de définir une collection d’objets associant un chemin à un code à exécuter.

Attention, parcourir un objet volumineux est toujours coûteux. Dans le cadre d’une SPA développée sans l’aide de frameworks, mettre en place ce type de solution peut donc, dans certains cas, s’avérer un mauvais choix.

En termes de performance, utiliser une Map plutôt qu’une liste est généralement un meilleur choix. Il est également préférable dans la majorité des cas de permettre un "découpage" de cette collection reflétant sa modularité.

Mais même alors, retrouver la route associée à un chemin au sein d’une ou de plusieurs collections conséquentes peut nécessiter des optimisations complexes ou avoir un impact important sur les performances de l’application.

Il est donc toujours important de bien évaluer les gains qu’apporte la centralisation de la définition des routes en termes de maintenabilité et de lisibilité par rapport...

Transmettre des données

Il existe de nombreuses manières de transmettre des données à un composant. Techniquement, faire transiter de telles données par les routes est donc loin d’être indispensable. Cela dit, associer routes et données permettra dans certains cas d’offrir une bien meilleure réutilisabilité et maintenabilité.

Pour reprendre nos exemples précédents, nous avons récupéré une telle donnée de manière asynchrone pour les routes /post/<id>. Nous avons ainsi pu constater que la technique utilisée pour récupérer et transmettre cette donnée pouvait avoir un impact important sur le mécanisme de routage lui-même. Mécanisme qui guidera fortement le comportement de la SPA, et donc ses performances.

Pour l’heure, le mécanisme de routage que nous avons mis en place est peu maintenable. Nous pouvons cependant résoudre ce problème par l’ajout d’une simple fonctionnalité.

1. Paramètres de route

Reprenons donc nos deux routes /post1/ et /post/2 précédentes :

{ 
  path: "/post/1", 
  renderer: Post, 
  data: () => getPostData(1) 
}, 
{ 
  path: "/post/2", 
  renderer: Post, 
  data: () => getPostData(2) 
} 

Nous pouvons directement constater que la route /post/2 est une copie parfaite de la route /post/1. En généralisant ce cas, nous obtenons donc le motif : /" + "post" + "/" + nombre + "/" ou ""

Nous pouvons traduire ce motif par l’expression régulière :

^/post/\d+/?$

path: new RegExp("^/post/(\\d+)/?$"), 

Si une expression régulière peut également s’écrire en JavaScript sous la forme /expression/, il est généralement plus pratique, pour une route, de la créer via le constructeur de RegExp, étant donné que celui-ci n’exige pas d’’’échapper’’ la barre oblique /.

Attention cependant : dans ce cas, la barre oblique inversée \ doit, au contraire être ’’échappée’’. Dans cet exemple, nous obtiendrions...

Navigation et changement de route

Au fil de ce chapitre, nous avons pu reproduire quasi intégralement le comportement de navigation d’un site web classique dans une SPA.

Un aspect demeure cependant différent. Jusqu’à présent, nous n’avons utilisé que le gestionnaire d’évènement onclick pour directement appeler notre fonction navigate. Cette méthode peut répondre à un grand nombre de besoins, mais s’éloigne des usages habituels de HTML, et reste peu pratique, lisible et maintenable.

Une approche de prime abord des plus naturelles et logiques serait, en lieu et place, d’utiliser directement l’élément d’ancre <a> pour représenter nos liens, comme prévu par le standard. Malheureusement, cet élément a, lui aussi, été créé et standardisé bien avant l’arrivée des SPA et est donc prévu pour effectuer une navigation de page en page.

images/05DP10.png

L’Interface HTMLAnchorElement telle que définie par son IDL dans le standard URL

Si nous remplaçons donc nos boutons dans le fichier index.html (pour rappel : <button onclick="navigate(’/post/1’)">Article 1 </button>) par un élément d’ancre (<a href="/post/1">Article 1</a>), non seulement nous n’obtenons pas de rendu de nos écrans, mais la page est également rechargée, comme lorsque nous utilisons la méthode Location.assign

Nous pouvons cependant annuler ce comportement par défaut en utilisant Event.preventDefault() et ajouter un appel à notre fonction navigate à la gestion d’évènement click.

<a onclick="linkCallback('/post/1', event)" href> 
  Article 1 
</a> 
window.linkCallback = (path, event) => { 
  event.preventDefault(); 
  navigate(path); 
}; 

Ici, un attribut href est nécessaire afin que le navigateur considère effectivement cet élément comme un lien.

1. Gestion globale

Pour éviter de devoir utiliser l’attribut onclick sur chaque élément d’ancre associé à une route de notre SPA, deux solutions s’offrent à nous. La première...

Au-delà du routage

Une grande majorité d’applications web étant aujourd’hui des SPA, les routeurs sont désormais incontournables. Au point que leur utilisation est devenue un quasi-automatisme, qui influence grandement l’ensemble de notre approche du développement applicatif.

1. Modularité

Au-delà de la programmation orientée composant, un projet conséquent a besoin de définir différents niveaux supérieurs de modularité. Du fait de l’omniprésence des routeurs, il est courant de définir ainsi des modules, rassemblant divers composants et helpers, en fonction des routes créées.

De fait, cette approche a deux avantages : permettre de mettre l’accent sur l’utilisateur, la hiérarchie des routes devant être représentative de son parcours, et mettre à profit les ES modules, pour améliorer les performances.

Sur ce dernier point, les routes aident en effet beaucoup à découper notre code en un ensemble d’éléments cohérents, pouvant être regroupés (via un bundler), "nettoyés" (via le tree-shaking, permettant la suppression des ES modules non utilisés lors du bundling) et chargés dynamiquement (lazy loading), via la fonction import().

De manière simplifiée, la fonction navigate() précédente pourrait être reprise dans une forme proche de la suivante afin de permettre de charger dynamiquement des modules qui définissent leurs propres sous-routes et qui sont associés à une route d’entrée via un paramètre pathToModule.

async function navigate(path, redirection = false) { 
  const route = matchRoute(path, baseRoutes); 
  if (route.redirect) { ...

Références

1. GALLI M., OESCHGER I., SOARES R. Inner-browsing extending the browser navigation paradigm [En ligne]. 16 mai 2003. Disponible sur : https://developer.mozilla.org/en-US/docs/Archive/Inner-browsing_extending_the_browser_navigation_paradigm (consulté le 27 novembre 2020).

2. whatwg-fetch [En ligne]. [s.l.] : [s.n.], [s.d.]. Disponible sur : https://www.npmjs.com/package/whatwg-fetch (consulté le 27 novembre 2020).

3. MDN CONTRIBUTORS. « Utiliser les promesses ». In : Documentation du Web - MDN [En ligne]. [s.l.] : [s.n.], [s.d.]. Disponible sur : https://developer.mozilla.org/fr/docs/Web/JavaScript/Guide/Utiliser_les_promesses (consulté le 27 novembre 2020).

4. AKSIT M., TEKINERDOGAN B., BERGMANS L. « The Six concerns for Separation of Concerns ». [s.l.] : [s.n.], 2001. Disponible sur : https://www.researchgate.net/publication/216884785_The_Six_concerns_for_Separation_of_Concerns

5. « HTML Standard - 7.10.1 Navigating across documents ». [s.l.] : [s.n.], [s.d.]. Disponible sur : https://html.spec.whatwg.org/multipage/browsing-the-web.html#navigating-across-documents (consulté le 28 novembre 2020).

6. PIETERS S., ÉD. HTML5 Differences from HTML4 [En ligne]. 9 décembre 2014. Disponible sur : https://www.w3.org/TR/html5-diff/ (consulté le 28 novembre 2020).

7. JACKSON M. ReactTraining/history [En ligne]. [s.l.] : React Training, 2020. Disponible sur : https://github.com/ReactTraining/history (consulté le 28 novembre 2020).

8. « React Router : Declarative Routing for React »....