Dans cette vidéo, nous allons continuer le développement de notre back-end sous Node.js/Express et intégrer un modèle de page (template) pour que toutes les pages de notre site utilisent la même structure et partagent le même en-tête et le même pied de page.

Regarder la vidéo sur YouTube

Cet article est la 7e partie d'une série qui explique comment réaliser un site Internet de A à Z. Vous n'avez pas besoin d'avoir lu ou vu les parties précédentes pour comprendre cette partie. Les autres épisodes sont disponibles ici.

Si vous ne pouvez pas regarder la vidéo, un compte rendu est proposé plus bas.

Résumé de la vidéo

Dans la vidéo précédente, nous avons commencé à développer le backend de notre site en utilisant la plateforme Node.js et le framework Express.

Nous avions ajouté uniquement une route à notre application : /. Cette route permettait à l'application d'écouter les requêtes à destination de la racine de notre site (http://localhost:6300/) et de répondre avec une autre requête en réponse. Cette réponse contient le contenu de notre page, qui sera affiché à l'écran de l'internaute.

Nous aimerions maintenant ajouter une seconde route, c'est-à-dire une deuxième page à notre site. Cette deuxième page, comme toutes les pages de notre site, aura le même en-tête et le même pied de page que la page d'accueil que l'on a déjà ajoutée.

Pour nous faciliter le travail, nous allons créer un modèle de page qui va contenir ses informations communes. Ce modèle sera utilisé pour générer les pages de notre site.

L'avantage d'un modèle de page est que la maintenance et la mise à jour des pages sont facilitées, car il y aura uniquement un seul document à changer, le modèle, si on veut, par la suite, ajouter une information ou corriger une erreur.

Réalisation du modèle et des pages HTML

Pour réaliser notre modèle, on peut partir de la page d'accueil de notre site et retirer toutes les informations spécifiques à la page d'accueil pour ne laisser plus que les informations communes à toutes les pages.

Le résultat ressemble à ca :

<!DOCTYPE html>
<html lang="fr">
<head>
  <meta charset="utf-8">
  <link rel="stylesheet" href="styles/global.css">
  {{EN-TETE}}
</head>
<body>
  <!-- En-tête -->
  <header class="entete">
    <a href="/" class="entete-text">Accueil</a>
  </header>

  <!-- Corps -->
  <main>
    {{CORPS}}
  </main>

  <!-- Pied de page -->
  <footer class="pied">
    <!-- [...] -->
  </footer>
</body>
</html>

On a ajouté deux balises particulières dans notre modèle : {{EN-TETE}} et {{CORPS}}. Ces balises seront utilisées par la suite pour injecter le code HTML spécifique de chaque page.

Nous pouvons enregistrer ce modèle dans un document, par exemple : modele.html.

Nous allons aussi créer deux autres documents : Un premier qui contient les en-têtes spécifiques à la page d'accueil (accueil.head.html) et un second qui contient le corps de la page d'accueil (accueil.body.html). Le contenu de ces deux documents correspondent simplement aux informations que l'on a retirées pour réaliser notre modèle.

Le contenu de la page accueil.head.html ressemble à ca :

<title>Ma page d'accueil</title>

Le contenu de la page accueil.head.html ressemble à ca :

<div class="intro">
  <div class="wrap">
    <p>Bonjour ! ...</p>
    <!-- [...] -->
  </div>
  <div class="home-timeline">
    <h1 class="home-titre">Derniers posts</h1>
    <ul>
      <!-- [...] -->
    </ul>
  </div>
</div>

Maintenant que les préparations sont faites, on peut retourner à notre backend Node.js et réaliser la logique qui permet de générer les pages à partir de ce modèle.

Générer une parge à partir d'un modèle sous Node.js

Jusqu'à présent, notre page était récupérée à partir d'un document stocké sur notre machine.

Désormais, nous allons récupéré les trois document que l'on a préparés : le modèle (modele.html), les en-têtes de la page d'accueil (accueil.head.html), et le corps de la page d'accueil (accueil.body.html).

On peut ensuite remplacé dans notre modèle la balise {{EN-TETE}} avec les en-têtes de la page d'accueil, et la balise {{CORPS}} avec le corps de la page d'accueil.

const { join } = require('path')

const { readFile } = require('fs')
const { promisify } = require('util')
const readFileAsync = promisify(readFile)

const READ_OPTIONS = { encoding: 'UTF-8' }
const PATH = 'C:/dev/youtube/blog/code/'

const lireFichier = file =>
  readFileAsync(join(PATH, file), READ_OPTIONS)

module.exports = async() => {
  // Récupérer le contenu des fichiers
  const modeleHtml = await lireFichier('modele.html')
  const accueilEnteteHtml = await lireFichier('accueil.head.html')
  const accueilCorpsHtml = await lireFichier('accueil.body.html')

  // Remplace les éléments spécifiques à la page dans le modèle
  const accueilHtml = modeleHtm
    .replace('{{EN-TETE}}', accueilEnteteHtml)
    .replace('{{CORPS}}', accueilCorpsHtml)

  // Retourner la page HTML
  return accueilHtml
}

On peut redémarrer notre application et constater que la page d'accueil est affichée correctement à l'écran, de la même façon que précédemment. L'internaute ne devrait constater aucune différence, car la page finale qui lui est envoyée est inchangée.

Optimiser le chargement des fichiers

On peut améliorer le chargement de la page en modifiant la façon dont nos trois documents sont lus.

Pour le moment les trois documents sont chargés séquentiellement, les uns après les autres. On peut modifier ce point et faire en sorte que le chargement se fasse en parallèle en utilisant la fonction Promise.all() (docs) :

const [
  modeleHtml,
  accueilEnteteHtml,
  accueilCorpsHtml
] = await Promise.all([
  lireFichier('modele.html'),
  lireFichier('accueil.head.html'),
  lireFichier('accueil.body.html')
])

Encore une fois, le résultat est indentique pour l'utilisateur, et le chargement est plus rapide, même si la différence est minime.

Généraliser le modèle à X pages

Notre fonction permet de récupérer uniquement la page d'accueil, mais on peut la rendre générique pour qu'elle puisse générer d'autres pages.

On peut, par exemple, rajouter un paramètre correspondant au nom de la page à générer.

module.exports = async page => {
  // Récupérer le contenu des fichiers
  const [
    modeleHtml,
    pageEnteteHtml,
    pageCorpsHtml
  ] = await Promise.all([
    lireFichier('modele.html'),
    lireFichier(`${page}.head.html`),
    lireFichier(`${page}.body.html`)
  ])

  // Remplace les éléments spécifiques à la page dans le modèle
  const pageHtml = modeleHtm
    .replace('{{EN-TETE}}', pageEnteteHtml)
    .replace('{{CORPS}}', pageCorpsHtml)

  // Retourner la page HTML
  return pageHtml
}

Notre route correspondant à la page d'accueil doit aussi être modifiée en conséquence et deviendrait :

app.get('/', async(req, res) => {
  const indexHtml = await genererPage('accueil')

  res.send(indexHtml)
})

A présent, on pourrait très simplement ajouter une seconde page, donc une seconde route, pour une page de contact par exemple :

app.get('/contact', async(req, res) => {
  const indexHtml = await genererPage('contact')

  res.send(indexHtml)
})

La page de contact s'afficherait alors à l'adresse http://localhost:6300/contact, à condition, bien sûr, d'avoir préalablement ajouté le contenu de l'en-tête et le contenu du corps de la page dans les documents contact.head.html et contact.head.html, de la même façon que l'on a fait pour la page d'accueil.

Routes dynamiques et expression régulières

Au lieu de créer deux routes statiques et de préciser / et /contact, on pourrait aussi créer une route dynamique en utilisant des expression régulières.

Les expressions régulières permettent de comparer un texte par rapport à un modèle de texte prédéfini.

On peut créer une expression régulière qui engloberait les deux routes : ^\/(|contact)$

L'expression est composée des conditions suivantes :

  • La route doit commencer par un slash : ^\/ ;
  • La route doit contenir soit un texte vide, soit le mot contact : (|contact) ;
  • La route ne doit rien contenir d'autres : $.

Si l'adresse de la requête correspond à tous ces critères, la route est choisie. Dans notre cas, seules les adresses / et /contact devraient être valides.

Lorsqu'on ajoute des éléments entre parenthèse, Express nous donne la possibilité de récupérer son contenu à partir de la propriété req.params. Cette propriété est un objet qui contient dans la propriété 0 le contenu de l'élément entre paranthèse. Dans notre cas, la valeur peut être soit un texte vide, soit le texte contact.

On peut adapter le contenu de notre réponse pour récupérer cette valeur et générer la page adaptée :

app.get(/^\/(|contact)$/, async(req, res) => {
  const nomPage = req.params[0] || 'index'

  const indexHtml = await genererModele(nomPage)

  res.send(indexHtml)
})

On se retrouve alors avec une seule route et une seule fonction pour générer les deux pages.

Note: Le prochain épisode est disponible ici et le précédent ici.