CM4 : Architecture client/serveur

Un site Web est généralement constitué d'au moins 2 parties :

Les échanges entre le client et le serveur Web utilisent le protocole HTTP(S) :

Les requêtes et réponses HTTP contiennent :

Contenus dynamiques

Afin de tester nos pages Web en TP, nous utilisions jusqu'à présent Live Server comme serveur Web. Son fonctionnement est très simple : la racine du site Web est associée à un dossier du serveur. Chaque URL est alors associée à un fichier du serveur. Live Server est ainsi un serveur web statique (static web server) distribuant des fichiers/ressources statiques (static files/assets). Ainsi, lorsqu'il reçoit une requête HTTP, il retourne le fichier demandé, e.g. :

est associé au fichier .

Il est cependant possible d'avoir des serveurs dynamiques (dynamic server) générant le contenu renvoyé en fonction de la requête HTTP reçue. Imaginez un site vendant 5,000 produits différents. Il est évident qu'on ne va pas s'amuser à créer 5,000 pages Web à la main. À la place, on voudrait générer dynamiquement les pages Web à partir :

Il serait alors très aisé d'ajouter un nouveau produit en ajoutant une entrée dans la base de donnée, ou de modifier l'affichage des pages produits en modifiant le modèle. On aurait ainsi deux parties :

Les données ne sont cependant pas directement accessibles à partir de la page Web. En effet, si les accès à la base de données (e.g. SQL) étaient effectués sur la page Web, i.e. sur le navigateur/côté client, n'importe quel visiteur pourrait alors arbitrairement modifier les requêtes effectuées, ou récupérer/réutiliser les identifiants utilisés pour se connecter à la base de données.

Il convient alors envoyer des requêtes au serveur Web, qui se chargera de manipuler la base de données côté serveur, puis en retournera les résultats. Le serveur Web fournira ainsi une API.

Les API REST

REST (REpresentational State Transfer) est un ensemble de principes permettant d'architecturer proprement une API Web de sorte à éviter qu'elle devienne chaotique :

Uniformiser les URL

Comme vous le savez déjà, les URL identifient les ressources de manière unique.

Afin d'éviter les collisions, et rendre les URL plus explicites/lisibles, il convient de les uniformiser en suivant un format unique. Pour cela on conserve une logique d'arborescence avec des collections, qui sont des ressources contenant elles-mêmes d'autres ressources (≈dossiers).

Par exemple, un produit sera identifié par (et manipulé via) l'URL /produits/{$ID_PRODUIT}. L'ensemble des URL correspondant à ce format est appelé route et sera alors traité par la même fonction (handler), avec $ID_PRODUIT comme paramètre. La collection /produits/ est alors la liste des produits, et permettra des manipulations d'ensemble.

💡 Il est fréquent de préfixer les chemins par /api/v1/ et /static/ afin d'aisément distinguer l'API REST, des ressources statiques. Le numéro de version permet d'assurer la rétro-compatibilité pour les applications utilisant d'anciennes versions de l'API.

💡 La structuration des URL en routes permet d'aisément visualiser les ressources accessibles via l'API, ainsi que de gérer plus facilement les droits d'accès aux données, en autorisant/interdisant l'accès à certaines routes.

Uniformiser les requêtes

Les API REST utilisent 5 méthodes HTTP :

Il est ainsi d'usage d'utiliser les requêtes suivantes afin d'indiquer le type d'opération effectué sur la ressource :

⚠ Les requêtes GET et DELETE ne peuvent contenir de corps (body).

Query strings

Les URL peuvent aussi être suffixées par une chaîne de requête (query string), indiquée par un ?. Les query strings sont des paires clefs=valeurs séparées par un &, e.g. : ?limit=10&export=csv.

Elles sont principalement utilisées sur les requêtes GET afin de :

⚠ Il est fréquent que les serveurs Web loggent les URL demandées. Il est ainsi important de ne pas inclure d'informations sensibles dans les query strings.

Les query strings sont manipulées via la classe URLSearchParams :

Opération URLSearchParams
Créer
Générer
Lister
Obtenir
Ajouter
Supprimer
Contient ?

💡 L'URL de la page est stockée dans .

Données structurées

Le corps des requêtes/réponses REST sont usuellement au format JSON, mais peuvent utiliser n'importe quel format, e.g. :

Le format utilisé/à utiliser est potentiellement indiqué par la requête REST. Il est cependant important de conserver des données structurées (i.e. éviter des réponses au format HTML) afin de permettre leur réutilisation pour d'autres usages (et potentiellement leur mise en cache).

Notamment, il est fréquent d'offrir la possibilité aux développeurs tiers d'exploiter les données du site via une API REST (e.g. Open Data avec stats INSEE, données météo, etc). Bien évidemment, certaines API peuvent être payantes, e.g. en fonction du nombre de requêtes autorisées à la seconde.

Sans États

Les API REST se doivent d'être sans état (stateless), cela signifie que :

  1. le serveur ne stocke aucun état de la connexion/session HTTP actuelle ;
  2. une requête contient toutes les données nécessaires à sa réponse ;
  3. une requête ne dépend pas d'autres requêtes ;
  4. les requêtes identiques produisent des réponses identiques (sauf si les données ont été modifiées).

Cela comporte de nombreux avantages :

API JS

Fetch

Une requête HTTP peut être effectuée au sein de la page Web via fetch():

Le corps peut être de différent type :

💡 Pour envoyer des données au format, JSON, il convient de les convertir en :

💡 indique le type des données contenu dans le corps de la requête :

fetch() retourne un Response, dont le contenu peut être lu via :

⚠ Le corps d'une réponse ne peut être lue qu'une seule fois.

Status

Response indique aussi si la requête s'est bien effectuée :

Les codes de status HTTP se divisent en 5 catégories :

🚩 [TODO] : outils network

SSE

Contrairement aux WebSockets, les Server Send Events ne permettent qu'une communication unidirectionnelle du serveur vers le client. Il est utilisé lorsque le serveur doit régulièrement envoyer des informations au client, sans attendre de réponses, e.g. envoyer des logs en temps réel.

Le principe est très simple, il s'agit d'une requête et d'une réponse HTTP normales, à l'exception que la réponse HTTP est maintenue en vie (keep-alive) et est écrite petit à petit (text/event-stream). Le corps de la réponse suit le format suivant :

Côté client, l'utilisation est très simple, il suffit d'écouter des événements d'un EventSource.

Upload/Download

=> qu'est-ce qu'un Blob/File ? => decode/encode

Websockets

https://developer.mozilla.org/en-US/docs/Web/API/WebSocket

Les formulaires

-> form -> input -> FormData / URLSearchParams. -> clear

-> validation + CSS

/!\ vérifier les donnés côté serveur.

Vous ne devez JAMAIS faire confiance client. En effet, il est très aisé d'envoyer des données arbitraires au serveur. Vous devez ainsi SYSTÉMATIQUEMENT vérifier la validité des données envoyées par le client (format, valeurs, autorisations, etc).

Gestion des données

Localstorage/sessionStorage/FileSystem API/cache.

Les différents type de serveurs Web

La distribution du contenu statique est relativement simple, le serveur lit les fichiers, puis les envoie au client. Bien évidemment, peut gérer des fonctionnalités plus avancées comme mettre les fichiers en RAM pour les distribuer plus rapidement, vérifier les droits d'accès aux fichiers (dont authentifications), réécrire les URL, transmettre la requête à un autre serveur, personnaliser l'en-tête de la réponse HTTP, etc.

On utilise alors des serveurs Web, e.g. Apache, Nginx, qui offrent de nombreuses fonctionnalités et possibilités de configurations :

Pour du contenu dynamique, i.e. généré sur demande par le serveur, il existe plusieurs façons de procéder :

Contrôle d'accès :

Afin de rendre le code plus lisible, il est fréquent que les frameworks représentent les routes par une arborescence de fichiers. Au démarrage, le framework va ainsi lire de manière récursive un dossier e.g. /routes/ et ajouter les différents gestionnaires en fonction des fichiers qui s'y trouvent. Ainsi, le fichiers/dossier /routes/dir/{PARAMS}/foo contiendra le gestionnaire à utiliser pour la route /dir/$PARAM/foo.

-> BDD -> execute shell commands -> redirections

-> 403/gestion authentification/.htaccess

Opti (CM5 ?)

Architecture client/serveur

L'affichage d'une page Web se déroule usuellement de la sorte :

  1. Le navigateur demande au serveur le fichier HTML correspondant à la page Web à afficher.

  2. Le navigateur commence à lire et à interpréter le fichier HTML reçu.

  3. Le navigateur lit la balise <script> et commence à télécharger le fichier correspondant. Comme la balise a l'attribut defer, le navigateur continue de lire et interpréter le fichier HTML.

  4. Le navigateur lit la balise <link> et commence à télécharger le fichier CSS correspondant.

  5. Le navigateur commence à construire l'arbre DOM à partir du contenu de <body>.

  6. Une fois l'arbre DOM construit, il execute le script qui était defer.

  7. Une fois l'exécution du script fini, le navigateur dessine la page Web pour la première fois.

Optimisations

-> séparer static/dynamique pour cache -> cache : client storage / RAM (dont sqlite) -> pré-générer vs on demand vs lazy -> calculs -> async -> webworker -> img

-> profiler / lighthouse -> hoist/+ les 2 autres trucs -> page parsing -> HTTP1vs2vs3

L'un des objectifs des développeur Web est de dessiner la page Web le plus tôt possible. Pour cela, il va user de diverses techniques :

⚠ L'optimisation prématurée est diabolique. Vous n'avez, à votre niveau, pas besoin d'optimiser vos sites Web. Vous n'avez pas non plus à implémenter vous-mêmes ces optimisations, de nombreux outils le font déjà pour vous (e.g. Webpack).

🚩 [TODO] : outils navigateur pour network / load performances (lighthouse)

🚩 [TODO] : archi projet dev vs prod.