Gestion des routes

Auteur :
  • François Brucker

Gestion des routes avec un serveur web.

Définition

Le but d'un serveur web est de répondre quelque chose à partir d'une requête constituée d'une url et d'une méthode (GET ou POST). Ces différentes url auxquelles peut répondre un serveur sont appelées routes.

Certaines routes vont nécessiter du travail comme faire une requête en base de données, calculer des choses, etc, et d'autres consisteront seulement à rendre (on dit servir) un fichier html, css ou encore javascript qui sera exécuté par le navigateur.

Il est crucial de bien organiser les routes d'un serveur pour pouvoir le modifier et l'agrandir facilement.

Préparation

Reprenons le serveur de la partie précédente :

import http from "http";

const hostname = "127.0.0.1";
const port = 3000;

const server = http.createServer((req, res) => {
  res.statusCode = 200;
  res.setHeader("Content-Type", "text/plain");
  res.end("Hello World\n");
});

server.listen(port, hostname, () => {
  console.log(`Server running at http://${hostname}:${port}/`);
});

Et lançons notre serveur avec la commande :

node index.js

Racine du site

Modifions le code du serveur pour qu'il lise un fichier html :

import http from "http";

const hostname = "127.0.0.1";
const port = 3000;

const server = http.createServer((req, res) => {
  res.statusCode = 200;
  res.setHeader("Content-Type", "text/html");
  res.end(`
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <title>Une page</title>
  </head>
  <body>
    <p>
    Coucou !
    </p>
  </body>
</html>
    `);
});

server.listen(port, hostname, () => {
  console.log(`Server running at http://${hostname}:${port}/`);
});

On teste le serveur et on voit que ça ne marche qu'à peut prêt.

En effet, on rend toujours la même chose : le fichier index.html même si le client demande autre chose. Regardons ce que demande le client en ajoutant une ligne au début de notre serveur :

// ...

const server = http.createServer((req, res) => {
    console.log(req.url)

    // ...

// ...

Re-testons le serveur et regardons le résultat dans la console lorsque l'on charge une page :

Server running at http://127.0.0.1:3000/
/

Si vous utilisez le navigateur chrome, à chaque chargement de page, il demande en plus une requête flavicon 0 la suite de la première requête.

404

Notre serveur est vraiment frustre, il ne permet de rendre que d'un fichier html. Si on tape n'importe quelle autre route que / on obtient tout dem même notre fichier html... Remédions à ça en créant rendant un statut 404 si l'url demandée n'est pas une route valide :

// ...

const server = http.createServer((req, res) => {
  console.log(req.url);
  if (req.url === "/" || req.url === "/index.html") {
    console.log("index");
    res.statusCode = 200;
    res.setHeader("Content-Type", "text/html");

    res.end(`
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <title>Une page</title>
  </head>
  <body>
    <p>
    Coucou !
    </p>
  </body>
</html>
        `);
  } else {
    res.statusCode = 404;
    res.setHeader("Content-Type", "text/plain");
    res.end();
  }
});

// ...

Le res.end() est indispensable dans la gestion du 404, sinon le navigateur attendra la réponse du serveur jusqu'à la fin des temps (ou un timeout).

Fichiers statiques

Les fichier statiques sont les fichiers qui seront pris directement sur le disque dur du serveur et envoyés au navigateur sont appelées fichiers statiques. Comme notre fichier index.html.

Plus un serveur a de fichiers statiques, mieux c'est puisque ces fichiers ne changent pas au cours du temps. On peut utiliser sur eux des méthodes de cache (côté client et serveur) ou encore de load-balancing pour accélérer le résultat (le réseau coûte toujours du temps).

On essayera toujours de déplacer le travail du serveur (unique) aux clients (multiples).

De là, on maximisera les fichiers statiques remplis de javascript qui construiront la page côté client ou qui ne feront des appels serveur pour récupérer ou transmettre des données.

Servir des fichiers statiques

L'usage veut que tous les fichiers statiques soient servies à partir d'une url reconnaissable. Dans notre cas, nous allons faire commencer toutes les url servant un fichier statique par /static/

Déplacez vos fichiers html, css et js dans un dossier que vous nommerez static/ dans le dossier de votre projet.

import http from "http";
import fs from "fs";

const hostname = "127.0.0.1";
const port = 3000;

const server = http.createServer((req, res) => {
  console.log(req.url);

  if (req.url.startsWith("/static/")) {
    res.statusCode = 200;
    res.setHeader("Content-Type", "text/html");

    let fichier = fs.readFileSync("." + req.url, { encoding: "utf8" });
    res.end(fichier);
  } else {
    res.statusCode = 404;
    res.setHeader("Content-Type", "text/plain");
    res.end();
  }
});

server.listen(port, hostname, () => {
  console.log(`Server running at http://${hostname}:${port}/`);
});

Si l'url est du type /static/[fin de l'url], le serveur essaye de lire le fichier nommé static/[fin de l'url] dans le répertoire courant, c'est à dire un fichier qui se trouve dans le dossier static/.

Ceci est effectué en :

  1. prenant la variable req.url
  2. en concaténant avec . puis en chargeant ce fichier. Dans notre cas, on cherche à lire le fichier ./static/[fin de l'url]
  3. Une fois ce fichier lu avec la méthode fs.readFileSync, il est envoyé par le serveur

La lecture de fichiers se fait par flux en node. Nous reviendrons là-dessus un oeu plus tard dans le cours.

On commence à voir deux besoins indispensables dans la gestion des routes :

Accédez au fichier index.html des deux façons possibles.

Redirection

Le code ci-après effectue une redirection pour que l'adresse / du serveur permette d'afficher l'index situé à l'adresse /static/index.html.

import http from "http";
import fs from "fs";

const hostname = "127.0.0.1";
const port = 3000;

const server = http.createServer((req, res) => {
  console.log(req.url);

  if (req.url === "/") {
    res.writeHead(302, {
      Location: "/static/index.html",
      "Content-Type": "text/html",
    });
    res.end();
  } else if (req.url.startsWith("/static/")) {
    res.statusCode = 200;
    res.setHeader("Content-Type", "text/html");

    let fichier = fs.readFileSync("." + req.url, { encoding: "utf8" });
    res.end(fichier);
  } else {
    res.statusCode = 404;
    res.setHeader("Content-Type", "text/plain");
    res.end();
  }
});

server.listen(port, hostname, () => {
  console.log(`Server running at http://${hostname}:${port}/`);
});

Nous avons utilisé une redirection de la requête / vers la requête http://127.0.0.1:3000/static/index.html (la variable req.headers['host']js contient le nom du serveur et son port. Affichez là dans la console pour vous en convaincre).

Ainsi, lorsque le serveur fait une requête http://127.0.0.1:3000 :

  1. le serveur lui répond que la ressource demandée est maintenant en http://127.0.0.1:3000/static/index.html en rendant un stats de 301 et la nouvelle localisation de la ressource
  2. le client va refaire une requête avec la nouvelle localisation
  3. cette nouvelle requête tombe dans le if des fichiers statique, et le fichier index.html pourra être servi.

Faire l'expérience de la redirection en demandant depuis un navigateur la route http://127.0.0.1:3000 et voir dans la console du serveur les deux appels : la requête initiale et la redirection.

headers

On peut utiliser res.writeHead(200, {'Content-Type': 'text/html'}) pour écrire en une fois le status et le type de la réponse.

Jardinons un peu le code

La version ci-après du serveur est une version améliorée du précédent qui :

import http from "http";
import fs from "fs";
import path from "path";

import { fileURLToPath } from "url";

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

const hostname = "127.0.0.1";
const port = 3000;

const server = http.createServer((req, res) => {
  console.log(req.url);

  if (req.url === "/") {
    console.log("redirection");

    res.writeHead(302, {
      Location: "/static/index.html",
      "Content-Type": "text/html",
    });
    res.end();
  } else if (req.url.startsWith("/static/")) {
    console.log("fichier statique");
    let fichier = path.join(__dirname, req.url);

    res.writeHead(200, { "Content-Type": "text/html" });
    fichier = fs.readFileSync(fichier, { encoding: "utf8" });
    res.end(fichier);
  } else {
    res.writeHead(404, { "Content-Type": "text/plain" });
    res.end();
  }
});

server.listen(port, hostname, () => {
  console.log(`Server running at http://${hostname}:${port}/`);
});

Localisation des fichiers

La localisation d'un fichier sur le disque dur est une opération :

C'est très problématique pour les serveur web puisque l'on veut que le code fonctionne sur la machine du (des) développeur(s) et sur la (les) machine(s) de production. Chacune ayant un autre disque dur et une autre architecture.

La seule chose donc on peut être sur c'est que le projet est identique sur toutes ces machines. On utilise alors une notation relative pour trouver le fichier.

Nous avons utiliser la notation "./index.html" pour indiquer que le fichier index.html se trouve dans le dossier courant. Mais qu'est-ce que le dossier courant ?

Le dossier courant est le dossier dans lequel se trouve le terminal qui a exécuté la commande node. De là, notre serveur :

Il ne faut donc pas prendre comme référence le dossier courant mais le dossier où est index.js

Node nous permet de faire ça en définissant les constantes __filename et __dirname en début de programme. Ces deux constantes contiennent respectivement le nom et le dossier du fichier exécuté (ici notre serveur). On utlise ensuite ce dossier comme racien de notre projet.

Enfin, on ne concatène jamais des fichiers à la main. On utilise toujours une bibliothèque pour cela (sinon c'est bad karma : ça va forcément vous sauter à la tête un jour) qui traite tous les cas particulier pour vous. En node, c'est la bibliothèque path qui s'occupe de ça.

A vous

Ajoutez une route nommée http://127.0.0.1/API/hasard qui affiche (en texte) un réel aléatoire entre 0 et 1.

Vous pourrez utiliser la méthode Math.random pour obtenir un réel entre 0 et 1 et le transformer en chaîne de caractère avec : String : String(Math.random()).

solution

Bloc à ajouter juste avant le bloc du 404 :

    // ...

    else if (req.url.startsWith("/API/hasard")) {
        console.log("API");

        res.writeHead(200,  {'Content-Type': 'text/plain'});
        res.end(String(Math.random()));
    }

    // ...